mirror of
https://github.com/flarum/core.git
synced 2025-08-24 09:03:05 +02:00
Compare commits
17 Commits
dk/json-ap
...
sm/jas-no-
Author | SHA1 | Date | |
---|---|---|---|
|
96b8b92d42 | ||
|
a03104d61d | ||
|
7504e31399 | ||
|
aa39d0c11b | ||
|
d9e5ab4f11 | ||
|
ac27cd03dd | ||
|
a442aad3be | ||
|
51e2ab8502 | ||
|
a8777c6198 | ||
|
10514709f1 | ||
|
eb6e599df1 | ||
|
5ce1aeab47 | ||
|
389d004ddc | ||
|
72f89c0209 | ||
|
1e7eddb61e | ||
|
1302378141 | ||
|
29ede5aa27 |
@@ -23,3 +23,6 @@ indent_size = 2
|
||||
|
||||
[*.neon]
|
||||
indent_style = tab
|
||||
|
||||
[{install,update}.php]
|
||||
indent_size = 2
|
||||
|
45
.github/workflows/REUSABLE_backend.yml
vendored
45
.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"]'
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
|
||||
|
||||
php_ini_values:
|
||||
description: PHP ini values
|
||||
@@ -52,6 +52,12 @@ on:
|
||||
required: false
|
||||
default: error_reporting=E_ALL
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
composer_auth:
|
||||
description: The Composer auth tokens to use for private packages.
|
||||
@@ -65,7 +71,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -79,32 +85,49 @@ jobs:
|
||||
# Expands the matrix by naming DBs.
|
||||
- service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
- service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
- service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
- service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
- service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
|
||||
# Include Database prefix tests with only one PHP version.
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
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)
|
||||
|
||||
@@ -112,10 +135,22 @@ jobs:
|
||||
exclude:
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.0.30'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'sqlite:3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ matrix.service }}
|
||||
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
|
||||
@@ -138,6 +173,7 @@ jobs:
|
||||
ini-values: ${{ matrix.php_ini_values }}
|
||||
|
||||
- name: Create MySQL Database
|
||||
if: ${{ matrix.service != 'sqlite:3' }}
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||
@@ -167,10 +203,11 @@ jobs:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
DB_DRIVER: ${{ matrix.driver }}
|
||||
COMPOSER_PROCESS_TIMEOUT: 600
|
||||
|
||||
phpstan:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
8
.github/workflows/REUSABLE_frontend.yml
vendored
8
.github/workflows/REUSABLE_frontend.yml
vendored
@@ -86,6 +86,12 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token:
|
||||
description: The GitHub token to use for Bundlewatch.
|
||||
@@ -103,7 +109,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Checks & Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
if: >-
|
||||
((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || github.event_name != 'pull_request')
|
||||
|
@@ -162,7 +162,7 @@
|
||||
"symfony/postmark-mailer": "^6.3",
|
||||
"symfony/translation": "^6.3",
|
||||
"symfony/yaml": "^6.3",
|
||||
"flarum/json-api-server": "^1.0.0",
|
||||
"flarum/json-api-server": "^0.1.0",
|
||||
"wikimedia/less.php": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@@ -14,6 +14,7 @@ use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class Flag extends AbstractModel
|
||||
{
|
||||
use ScopeVisibilityTrait;
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
|
30
extensions/flags/src/FlagFactory.php
Normal file
30
extensions/flags/src/FlagFactory.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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\Flags;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class FlagFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'user',
|
||||
'post_id' => Post::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'reason' => $this->faker->sentence,
|
||||
'reason_detail' => $this->faker->sentence,
|
||||
'created_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
@@ -56,7 +57,7 @@ class ListTest extends TestCase
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'is_private' => true],
|
||||
],
|
||||
'flags' => [
|
||||
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],
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
@@ -83,7 +84,7 @@ class ListWithTagsTest extends TestCase
|
||||
['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
|
@@ -45,13 +45,13 @@ return [
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint(
|
||||
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
|
||||
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\EndpointInterface {
|
||||
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['likes']);
|
||||
}
|
||||
),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\EndpointInterface {
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['posts.likes']);
|
||||
}),
|
||||
|
||||
|
2
extensions/likes/js/dist/forum.js
generated
vendored
2
extensions/likes/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/likes/js/dist/forum.js.map
generated
vendored
2
extensions/likes/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,11 +2,15 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import LikesUserPage from './components/LikesUserPage';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
.add('user.likes', '/u/:username/likes', LikesUserPage),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postLiked', PostLikedNotification),
|
||||
|
||||
new Extend.Model(Post) //
|
||||
.hasMany<User>('likes')
|
||||
.attribute<number>('likesCount')
|
||||
|
@@ -3,14 +3,11 @@ import app from 'flarum/forum/app';
|
||||
|
||||
import addLikeAction from './addLikeAction';
|
||||
import addLikesList from './addLikesList';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
import addLikesTabToUserProfile from './addLikesTabToUserProfile';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-likes', () => {
|
||||
app.notificationComponents.postLiked = PostLikedNotification;
|
||||
|
||||
addLikeAction();
|
||||
addLikesList();
|
||||
addLikesTabToUserProfile();
|
||||
|
@@ -51,7 +51,7 @@ class PostResourceFields
|
||||
Schema\Relationship\ToMany::make('likes')
|
||||
->type('users')
|
||||
->includable()
|
||||
->constrain(function (Builder $query, Context $context) {
|
||||
->scope(function (Builder $query, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
|
2
extensions/lock/js/dist/forum.js
generated
vendored
2
extensions/lock/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>D});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","forum/components/Notification");var c=o.n(s);class a extends(c()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var l=o.n(i);const u=flarum.reg.get("core","common/components/Badge");var d=o.n(u);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var y=o.n(b);const _=flarum.reg.get("core","common/extenders");var v=o.n(_);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=flarum.reg.get("core","common/query/IGambit"),S=flarum.reg.get("core","common/app");var j=o.n(S);class w extends P.BooleanGambit{key(){return j().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",w);const D=[(new(v().Search)).gambit("discussions",w),(new(v().PostTypes)).add("discussionLocked",h),new(v().Model)(l()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,t.extend)(l().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(k(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(y(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
|
||||
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>N});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","common/models/Discussion");var c=o.n(s);const a=flarum.reg.get("core","common/components/Badge");var i=o.n(a);const l=flarum.reg.get("core","forum/utils/DiscussionControls");var u=o.n(l);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=o.n(d);const k=flarum.reg.get("core","common/components/Button");var g=o.n(k);const p=flarum.reg.get("core","common/extenders");var b=o.n(p);const y=flarum.reg.get("core","forum/components/EventPost");var _=o.n(y);class v extends(_()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",v);const x=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var h=o.n(L);class P extends x.BooleanGambit{key(){return h().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",P);const w=[(new(b().Search)).gambit("discussions",P)],S=flarum.reg.get("core","forum/components/Notification");var j=o.n(S);class D extends(j()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",D);const N=[...w,(new(b().PostTypes)).add("discussionLocked",v),(new(b().Notification)).add("discussionLocked",D),new(b().Model)(c()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{(0,t.extend)(c().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(i(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(u(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.isLocked()?"unlock":"lock","_button"))))})),u().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/lock/js/dist/forum.js.map
generated
vendored
2
extensions/lock/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import Discussion from 'flarum/common/models/Discussion';
|
||||
import DiscussionLockedPost from './components/DiscussionLockedPost';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
@@ -10,6 +11,9 @@ export default [
|
||||
new Extend.PostTypes() //
|
||||
.add('discussionLocked', DiscussionLockedPost),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('discussionLocked', DiscussionLockedNotification),
|
||||
|
||||
new Extend.Model(Discussion) //
|
||||
.attribute<boolean>('isLocked')
|
||||
.attribute<boolean>('canLock'),
|
||||
|
@@ -1,15 +1,12 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
import addLockBadge from './addLockBadge';
|
||||
import addLockControl from './addLockControl';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-lock', () => {
|
||||
app.notificationComponents.discussionLocked = DiscussionLockedNotification;
|
||||
|
||||
addLockBadge();
|
||||
addLockControl();
|
||||
|
||||
|
@@ -63,7 +63,7 @@ return [
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface {
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
@@ -137,7 +137,7 @@ return [
|
||||
}),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface {
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->eagerLoad(['mentionsTags']);
|
||||
}),
|
||||
]),
|
||||
|
2
extensions/mentions/js/dist/forum.js
generated
vendored
2
extensions/mentions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,6 +2,9 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import MentionsUserPage from './components/MentionsUserPage';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
@@ -11,6 +14,11 @@ export default [
|
||||
.hasMany<Post>('mentionedBy')
|
||||
.attribute<number>('mentionedByCount'),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postMentioned', PostMentionedNotification)
|
||||
.add('userMentioned', UserMentionedNotification)
|
||||
.add('groupMentioned', GroupMentionedNotification),
|
||||
|
||||
new Extend.Model(User) //
|
||||
.attribute<boolean>('canMentionGroups'),
|
||||
];
|
||||
|
@@ -9,9 +9,6 @@ import addMentionedByList from './addMentionedByList';
|
||||
import addPostReplyAction from './addPostReplyAction';
|
||||
import addPostQuoteButton from './addPostQuoteButton';
|
||||
import addComposerAutocomplete from './addComposerAutocomplete';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
import MentionFormats from './mentionables/formats/MentionFormats';
|
||||
import UserPage from 'flarum/forum/components/UserPage';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
@@ -40,10 +37,6 @@ app.initializers.add('flarum-mentions', function () {
|
||||
// posts or users that the user could mention.
|
||||
addComposerAutocomplete();
|
||||
|
||||
app.notificationComponents.postMentioned = PostMentionedNotification;
|
||||
app.notificationComponents.userMentioned = UserMentionedNotification;
|
||||
app.notificationComponents.groupMentioned = GroupMentionedNotification;
|
||||
|
||||
// Add notification preferences.
|
||||
extend('flarum/forum/components/NotificationGrid', 'notificationTypes', function (items) {
|
||||
items.add('postMentioned', {
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_post` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('post_mentions_post', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('post_mentions_user', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -25,7 +25,7 @@ class PostResourceFields
|
||||
Schema\Relationship\ToMany::make('mentionedBy')
|
||||
->type('posts')
|
||||
->includable()
|
||||
->constrain(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
->scope(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
Schema\Relationship\ToMany::make('mentionsPosts')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToMany::make('mentionsUsers')
|
||||
|
@@ -130,12 +130,16 @@ class ShowStatisticsData implements RequestHandlerInterface
|
||||
$endDate = new DateTime();
|
||||
}
|
||||
|
||||
// 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';
|
||||
$dbFormattedDatetime = match ($query->getConnection()->getDriverName()) {
|
||||
'sqlite' => 'strftime('.$format.', '.$column.')',
|
||||
default => 'DATE_FORMAT('.$column.', '.$format.')',
|
||||
};
|
||||
|
||||
$results = $query
|
||||
->selectRaw(
|
||||
'DATE_FORMAT(
|
||||
@date := '.$column.',
|
||||
IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 24 hours, group by hour
|
||||
) as time_group',
|
||||
$dbFormattedDatetime.' as time_group',
|
||||
[new DateTime('-25 hours')]
|
||||
)
|
||||
->selectRaw('COUNT(id) as count')
|
||||
|
@@ -102,7 +102,7 @@ class CanRequestCustomTimedStatisticsTest extends TestCase
|
||||
|
||||
$body = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body['errors'][0]['detail'] ?? '');
|
||||
|
||||
$this->assertEquals(
|
||||
$data,
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Sticky\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Update;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Sticky\Event\DiscussionWasStickied;
|
||||
@@ -23,7 +22,7 @@ class DiscussionResourceFields
|
||||
return [
|
||||
Schema\Boolean::make('isSticky')
|
||||
->writable(function (Discussion $discussion, Context $context) {
|
||||
return $context->endpoint instanceof Update
|
||||
return $context->updating()
|
||||
&& $context->getActor()->can('sticky', $discussion);
|
||||
})
|
||||
->set(function (Discussion $discussion, bool $isSticky, Context $context) {
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Sticky;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Tags\Search\Filter\TagFilter;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class PinStickiedDiscussionsToTop
|
||||
{
|
||||
@@ -45,22 +46,26 @@ class PinStickiedDiscussionsToTop
|
||||
$sticky->where('is_sticky', true);
|
||||
unset($sticky->orders);
|
||||
|
||||
/** @var Builder $q */
|
||||
foreach ([$sticky, $query] as $q) {
|
||||
$read = $q->newQuery()
|
||||
->selectRaw('1')
|
||||
->from('discussion_user as sticky')
|
||||
->whereColumn('sticky.discussion_id', 'id')
|
||||
->where('sticky.user_id', '=', $state->getActor()->id)
|
||||
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
|
||||
|
||||
// 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]));
|
||||
}
|
||||
|
||||
$query->union($sticky);
|
||||
|
||||
$read = $query->newQuery()
|
||||
->selectRaw('1')
|
||||
->from('discussion_user as sticky')
|
||||
->whereColumn('sticky.discussion_id', 'id')
|
||||
->where('sticky.user_id', '=', $state->getActor()->id)
|
||||
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
|
||||
$query->orderByDesc('is_unread_sticky');
|
||||
|
||||
// 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.
|
||||
$query->orderByRaw('is_sticky and not exists ('.$read->toSql().') and last_posted_at > ? desc')
|
||||
->addBinding(array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]), 'union');
|
||||
|
||||
$query->unionOrders = array_merge($query->unionOrders, $query->orders ?? []);
|
||||
$query->unionOrders = array_merge($query->unionOrders ?? [], $query->orders ?? []);
|
||||
$query->unionLimit = $query->limit;
|
||||
$query->unionOffset = $query->offset;
|
||||
|
||||
|
2
extensions/subscriptions/js/dist/forum.js
generated
vendored
2
extensions/subscriptions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/subscriptions/js/dist/forum.js.map
generated
vendored
2
extensions/subscriptions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import IndexPage from 'flarum/forum/components/IndexPage';
|
||||
import Discussion from 'flarum/common/models/Discussion';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import NewPostNotification from './components/NewPostNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
@@ -10,6 +11,9 @@ export default [
|
||||
new Extend.Routes() //
|
||||
.add('following', '/following', IndexPage),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('newPost', NewPostNotification),
|
||||
|
||||
new Extend.Model(Discussion) //
|
||||
.attribute('subscription'),
|
||||
];
|
||||
|
@@ -6,13 +6,9 @@ import addSubscriptionControls from './addSubscriptionControls';
|
||||
import addSubscriptionFilter from './addSubscriptionFilter';
|
||||
import addSubscriptionSettings from './addSubscriptionSettings';
|
||||
|
||||
import NewPostNotification from './components/NewPostNotification';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('subscriptions', function () {
|
||||
app.notificationComponents.newPost = NewPostNotification;
|
||||
|
||||
addSubscriptionBadge();
|
||||
addSubscriptionControls();
|
||||
addSubscriptionFilter();
|
||||
|
2
extensions/suspend/js/dist/forum.js
generated
vendored
2
extensions/suspend/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/suspend/js/dist/forum.js.map
generated
vendored
2
extensions/suspend/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -3,10 +3,16 @@ import User from 'flarum/common/models/User';
|
||||
import Model from 'flarum/common/Model';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import UserSuspendedNotification from './components/UserSuspendedNotification';
|
||||
import UserUnsuspendedNotification from './components/UserUnsuspendedNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('userSuspended', UserSuspendedNotification)
|
||||
.add('userUnsuspended', UserUnsuspendedNotification),
|
||||
|
||||
new Extend.Model(User)
|
||||
.attribute<Date | null | undefined, string | null | undefined>('suspendedUntil', Model.transformDate)
|
||||
.attribute<string | null | undefined>('suspendReason')
|
||||
|
@@ -6,16 +6,11 @@ import Badge from 'flarum/common/components/Badge';
|
||||
import User from 'flarum/common/models/User';
|
||||
|
||||
import SuspendUserModal from './components/SuspendUserModal';
|
||||
import UserSuspendedNotification from './components/UserSuspendedNotification';
|
||||
import UserUnsuspendedNotification from './components/UserUnsuspendedNotification';
|
||||
import checkForSuspension from './checkForSuspension';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-suspend', () => {
|
||||
app.notificationComponents.userSuspended = UserSuspendedNotification;
|
||||
app.notificationComponents.userUnsuspended = UserUnsuspendedNotification;
|
||||
|
||||
extend(UserControls, 'moderationControls', (items, user) => {
|
||||
if (user.canSuspend()) {
|
||||
items.add(
|
||||
|
@@ -14,9 +14,14 @@ return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('discussions_count', 'discussion_count');
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('last_time', 'last_posted_at');
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('last_discussion_id', 'last_posted_discussion_id');
|
||||
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->integer('parent_id')->unsigned()->nullable()->change();
|
||||
|
||||
$table->integer('last_posted_user_id')->unsigned()->nullable();
|
||||
@@ -26,11 +31,17 @@ return [
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->dropColumn('last_posted_user_id');
|
||||
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->integer('parent_id')->nullable()->change();
|
||||
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('discussion_count', 'discussions_count');
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('last_posted_at', 'last_time');
|
||||
});
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('last_posted_discussion_id', 'last_discussion_id');
|
||||
});
|
||||
}
|
||||
|
@@ -17,17 +17,15 @@ return [
|
||||
$table->timestamp('updated_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}tags` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$connection->statement("ALTER TABLE `{$prefix}tags` MODIFY updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrent()->useCurrentOnUpdate()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->dropColumn('created_at');
|
||||
$table->dropColumn('updated_at');
|
||||
$table->dropColumn('created_at', 'updated_at');
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}discussion_tag` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('discussion_tag', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -91,7 +91,7 @@
|
||||
"symfony/translation": "^6.3",
|
||||
"symfony/translation-contracts": "^2.5",
|
||||
"symfony/yaml": "^6.3",
|
||||
"flarum/json-api-server": "^1.0.0",
|
||||
"flarum/json-api-server": "^0.1.0",
|
||||
"wikimedia/less.php": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
14
framework/core/js/dist-typings/common/extenders/Notification.d.ts
generated
vendored
Normal file
14
framework/core/js/dist-typings/common/extenders/Notification.d.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import IExtender, { IExtensionModule } from './IExtender';
|
||||
import type Application from '../Application';
|
||||
import type { NewComponent } from '../Application';
|
||||
export default class Notification implements IExtender {
|
||||
private notificationComponents;
|
||||
/**
|
||||
* Register a new notification component type.
|
||||
*
|
||||
* @param name The name of the notification type.
|
||||
* @param component The component class to render the notification.
|
||||
*/
|
||||
add(name: string, component: NewComponent<any>): Notification;
|
||||
extend(app: Application, extension: IExtensionModule): void;
|
||||
}
|
2
framework/core/js/dist-typings/common/extenders/index.d.ts
generated
vendored
2
framework/core/js/dist-typings/common/extenders/index.d.ts
generated
vendored
@@ -3,11 +3,13 @@ import PostTypes from './PostTypes';
|
||||
import Routes from './Routes';
|
||||
import Store from './Store';
|
||||
import Search from './Search';
|
||||
import Notification from './Notification';
|
||||
declare const extenders: {
|
||||
Model: typeof Model;
|
||||
PostTypes: typeof PostTypes;
|
||||
Routes: typeof Routes;
|
||||
Store: typeof Store;
|
||||
Search: typeof Search;
|
||||
Notification: typeof Notification;
|
||||
};
|
||||
export default extenders;
|
||||
|
2
framework/core/js/dist/admin.js
generated
vendored
2
framework/core/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/admin.js.map
generated
vendored
2
framework/core/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/forum.js
generated
vendored
2
framework/core/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/forum.js.map
generated
vendored
2
framework/core/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -96,7 +96,9 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
|
||||
const fields = this.driverFields![this.setting('mail_driver')()];
|
||||
const fieldKeys = Object.keys(fields);
|
||||
|
||||
items.add('status', this.status!.sending || <Alert dismissible={false}>{app.translator.trans('core.admin.email.not_sending_message')}</Alert>);
|
||||
if (this.status!.sending) {
|
||||
items.add('status', <Alert dismissible={false}>{app.translator.trans('core.admin.email.not_sending_message')}</Alert>);
|
||||
}
|
||||
|
||||
items.add(
|
||||
'mail_from',
|
||||
|
@@ -34,7 +34,7 @@ export default class StatusWidget extends DashboardWidget {
|
||||
|
||||
items.add('version-flarum', [<strong>Flarum</strong>, <br />, app.forum.attribute('version')], 100);
|
||||
items.add('version-php', [<strong>PHP</strong>, <br />, app.data.phpVersion], 90);
|
||||
items.add('version-mysql', [<strong>MySQL</strong>, <br />, app.data.mysqlVersion], 80);
|
||||
items.add('version-db', [<strong>{app.data.dbDriver}</strong>, <br />, app.data.dbVersion], 80);
|
||||
if (app.data.schedulerStatus) {
|
||||
items.add(
|
||||
'schedule-status',
|
||||
|
25
framework/core/js/src/common/extenders/Notification.ts
Normal file
25
framework/core/js/src/common/extenders/Notification.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import IExtender, { IExtensionModule } from './IExtender';
|
||||
import type Component from '../Component';
|
||||
import ForumApplication from '../../forum/ForumApplication';
|
||||
import type Application from '../Application';
|
||||
import type { NewComponent } from '../Application';
|
||||
|
||||
export default class Notification implements IExtender {
|
||||
private notificationComponents: Record<string, new () => Component> = {};
|
||||
|
||||
/**
|
||||
* Register a new notification component type.
|
||||
*
|
||||
* @param name The name of the notification type.
|
||||
* @param component The component class to render the notification.
|
||||
*/
|
||||
add(name: string, component: NewComponent<any>): Notification {
|
||||
this.notificationComponents[name] = component;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(app: Application, extension: IExtensionModule): void {
|
||||
Object.assign((app as unknown as ForumApplication).notificationComponents, this.notificationComponents);
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ import PostTypes from './PostTypes';
|
||||
import Routes from './Routes';
|
||||
import Store from './Store';
|
||||
import Search from './Search';
|
||||
import Notification from './Notification';
|
||||
|
||||
const extenders = {
|
||||
Model,
|
||||
@@ -10,6 +11,7 @@ const extenders = {
|
||||
Routes,
|
||||
Store,
|
||||
Search,
|
||||
Notification,
|
||||
};
|
||||
|
||||
export default extenders;
|
||||
|
@@ -34,7 +34,10 @@ return [
|
||||
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
|
||||
if ($connection->getDriverName() !== 'sqlite') {
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -12,10 +12,12 @@ use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->dropColumn('created_at', 'expires_at');
|
||||
});
|
||||
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->string('id', 40)->change();
|
||||
$table->dropColumn('created_at');
|
||||
$table->dropColumn('expires_at');
|
||||
$table->integer('last_activity');
|
||||
$table->integer('lifetime');
|
||||
});
|
||||
@@ -24,8 +26,7 @@ return [
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->string('id', 100)->change();
|
||||
$table->dropColumn('last_activity');
|
||||
$table->dropColumn('lifetime');
|
||||
$table->dropColumn('last_activity', 'lifetime');
|
||||
$table->timestamp('created_at');
|
||||
$table->timestamp('expires_at');
|
||||
});
|
||||
|
@@ -14,14 +14,20 @@ return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->renameColumn('id', 'token');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->renameColumn('lifetime', 'lifetime_seconds');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->renameColumn('last_activity', 'last_activity_at');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('created_at');
|
||||
$table->integer('user_id')->unsigned()->change();
|
||||
});
|
||||
|
||||
// Use a separate schema instance because this column gets renamed
|
||||
// in the first one.
|
||||
// in the previous one.
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('last_activity_at')->change();
|
||||
});
|
||||
@@ -31,12 +37,19 @@ return [
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->integer('last_activity_at')->change();
|
||||
});
|
||||
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->renameColumn('token', 'id');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->renameColumn('lifetime_seconds', 'lifetime');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->renameColumn('last_activity_at', 'last_activity');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->dropColumn('created_at');
|
||||
});
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->integer('user_id')->change();
|
||||
});
|
||||
}
|
||||
|
@@ -12,13 +12,7 @@ use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('api_keys', function (Blueprint $table) {
|
||||
$table->dropPrimary(['id']);
|
||||
$table->renameColumn('id', 'key');
|
||||
$table->unique('key');
|
||||
});
|
||||
|
||||
$schema->table('api_keys', function (Blueprint $table) {
|
||||
$definition = function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('allowed_ips')->nullable();
|
||||
$table->string('scopes')->nullable();
|
||||
@@ -27,7 +21,23 @@ return [
|
||||
$table->dateTime('last_activity_at')->nullable();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
};
|
||||
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$schema->table('api_keys', function (Blueprint $table) {
|
||||
$table->dropPrimary(['id']);
|
||||
$table->renameColumn('id', 'key');
|
||||
$table->unique('key');
|
||||
});
|
||||
|
||||
$schema->table('api_keys', $definition);
|
||||
} else {
|
||||
$schema->drop('api_keys');
|
||||
$schema->create('api_keys', function (Blueprint $table) use ($definition) {
|
||||
$table->string('key', 100)->unique();
|
||||
$definition($table);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
@@ -36,10 +46,13 @@ return [
|
||||
$table->dropColumn('id', 'allowed_ips', 'user_id', 'scopes', 'created_at');
|
||||
});
|
||||
|
||||
$schema->table('api_keys', function (Blueprint $table) {
|
||||
$schema->table('api_keys', function (Blueprint $table) use ($schema) {
|
||||
$table->dropUnique(['key']);
|
||||
$table->renameColumn('key', 'id');
|
||||
$table->primary('id');
|
||||
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$table->primary('id');
|
||||
}
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -7,19 +7,19 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE {$prefix}registration_tokens MODIFY created_at DATETIME");
|
||||
$schema->table('registration_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('created_at')->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE {$prefix}registration_tokens MODIFY created_at TIMESTAMP");
|
||||
$schema->table('registration_tokens', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->change();
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -12,13 +12,19 @@ use Illuminate\Database\Schema\Builder;
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = InnoDB');
|
||||
|
||||
if ($connection->getDriverName() === 'mysql') {
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = InnoDB');
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = MyISAM');
|
||||
|
||||
if ($connection->getDriverName() === 'mysql') {
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = MyISAM');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@@ -7,19 +7,19 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE {$prefix}email_tokens MODIFY created_at DATETIME");
|
||||
$schema->table('email_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('created_at')->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE {$prefix}email_tokens MODIFY created_at TIMESTAMP");
|
||||
$schema->table('email_tokens', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->change();
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -15,10 +15,14 @@ return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->dropColumn('subject_type');
|
||||
|
||||
});
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->renameColumn('time', 'created_at');
|
||||
});
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->renameColumn('sender_id', 'from_user_id');
|
||||
|
||||
});
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->dateTime('read_at')->nullable();
|
||||
});
|
||||
|
||||
@@ -36,8 +40,11 @@ return [
|
||||
$table->string('subject_type', 200)->nullable();
|
||||
|
||||
$table->renameColumn('created_at', 'time');
|
||||
});
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->renameColumn('from_user_id', 'sender_id');
|
||||
|
||||
});
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->boolean('is_read');
|
||||
});
|
||||
|
||||
|
@@ -7,19 +7,19 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE {$prefix}password_tokens MODIFY created_at DATETIME");
|
||||
$schema->table('password_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('created_at')->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE {$prefix}password_tokens MODIFY created_at TIMESTAMP");
|
||||
$schema->table('password_tokens', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->change();
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -13,7 +13,10 @@ return [
|
||||
'up' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)');
|
||||
|
||||
if ($connection->getDriverName() !== 'sqlite') {
|
||||
$connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)');
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -12,24 +12,42 @@ use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
// Replace primary key with unique index so we can create a new primary
|
||||
$table->dropPrimary('token');
|
||||
$table->unique('token');
|
||||
});
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
// Replace primary key with unique index so we can create a new primary
|
||||
$table->dropPrimary('token');
|
||||
$table->unique('token');
|
||||
});
|
||||
|
||||
// This needs to be done in a second statement because of the order Laravel runs operations in
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
// Introduce new increment-based ID
|
||||
$table->increments('id')->first();
|
||||
});
|
||||
// This needs to be done in a second statement because of the order Laravel runs operations in
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
// Introduce new increment-based ID
|
||||
$table->increments('id')->first();
|
||||
});
|
||||
} else {
|
||||
$schema->drop('access_tokens');
|
||||
$schema->create('access_tokens', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('token', 100)->unique();
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->dateTime('last_activity_at')->nullable();
|
||||
$table->dateTime('created_at');
|
||||
$table->string('type', 100)->nullable();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->index('type');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$schema->table('access_tokens', function (Blueprint $table) use ($schema) {
|
||||
$table->dropColumn('id');
|
||||
$table->dropIndex('token');
|
||||
$table->primary('token');
|
||||
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$table->primary('token');
|
||||
}
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -17,17 +17,15 @@ return [
|
||||
$table->timestamp('updated_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `${prefix}groups` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$connection->statement("ALTER TABLE `${prefix}groups` MODIFY updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
|
||||
$schema->table('groups', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('groups', function (Blueprint $table) {
|
||||
$table->dropColumn('created_at');
|
||||
$table->dropColumn('updated_at');
|
||||
$table->dropColumn('created_at', 'updated_at');
|
||||
});
|
||||
}
|
||||
];
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `${prefix}group_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('group_user', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `${prefix}group_permission` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('group_permission', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<?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\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
if ($schema->getConnection()->getDriverName() === 'sqlite') {
|
||||
$schema->getConnection()->statement('PRAGMA foreign_keys = OFF');
|
||||
$schema->getConnection()->statement('PRAGMA writable_schema = ON');
|
||||
|
||||
$prefix = $schema->getConnection()->getTablePrefix();
|
||||
|
||||
$foreignKeysSqlite = [
|
||||
'discussions' => <<<SQL
|
||||
foreign key("user_id") references "{$prefix}users"("id") on delete set null,
|
||||
foreign key("last_posted_user_id") references "{$prefix}users"("id") on delete set null,
|
||||
foreign key("hidden_user_id") references "{$prefix}users"("id") on delete set null,
|
||||
foreign key("first_post_id") references "{$prefix}posts"("id") on delete set null,
|
||||
foreign key("last_post_id") references "{$prefix}posts"("id") on delete set null
|
||||
SQL,
|
||||
'posts' => <<<SQL
|
||||
foreign key("discussion_id") references "{$prefix}discussions"("id") on delete cascade,
|
||||
foreign key("user_id") references "{$prefix}users"("id") on delete set null,
|
||||
foreign key("edited_user_id") references "{$prefix}users"("id") on delete set null,
|
||||
foreign key("hidden_user_id") references "{$prefix}users"("id") on delete set null
|
||||
SQL,
|
||||
];
|
||||
|
||||
foreach (['discussions', 'posts'] as $table) {
|
||||
$tableDefinition = $schema->getConnection()->select('SELECT sql FROM sqlite_master WHERE type = "table" AND name = "'.$prefix.$table.'"')[0]->sql;
|
||||
$modifiedTableDefinition = str($tableDefinition)->beforeLast(')')->append(', '.$foreignKeysSqlite[$table].')')->toString();
|
||||
$modifiedTableDefinitionWithEscapedQuotes = str($modifiedTableDefinition)->replace('"', '""')->toString();
|
||||
$schema->getConnection()->statement('UPDATE sqlite_master SET sql = "'.$modifiedTableDefinitionWithEscapedQuotes.'" WHERE type = "table" AND name = "'.$prefix.$table.'"');
|
||||
}
|
||||
|
||||
$schema->getConnection()->statement('PRAGMA writable_schema = OFF');
|
||||
$schema->getConnection()->statement('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
},
|
||||
];
|
132
framework/core/migrations/sqlite-install.dump
Normal file
132
framework/core/migrations/sqlite-install.dump
Normal file
@@ -0,0 +1,132 @@
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_migrations" ("id" integer primary key autoincrement not null, "migration" varchar not null, "extension" varchar);
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_groups" ("id" integer primary key autoincrement not null, "name_singular" varchar not null, "name_plural" varchar not null, "color" varchar, "icon" varchar, "is_hidden" tinyint(1) not null default '0', "created_at" datetime, "updated_at" datetime);
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_group_permission" ("group_id" integer not null, "permission" varchar not null, "created_at" datetime, primary key ("group_id", "permission"));
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_group_user" ("user_id" integer not null, "group_id" integer not null, "created_at" datetime, primary key ("user_id", "group_id"));
|
||||
CREATE TABLE db_prefix_settings ("key" VARCHAR(255) NOT NULL, value CLOB DEFAULT NULL, PRIMARY KEY("key"));
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_api_keys" ("key" varchar not null, "id" integer primary key autoincrement not null, "allowed_ips" varchar, "scopes" varchar, "user_id" integer, "created_at" datetime not null, "last_activity_at" datetime, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
|
||||
CREATE UNIQUE INDEX "db_prefix_api_keys_key_unique" on "db_prefix_api_keys" ("key");
|
||||
CREATE TABLE db_prefix_discussion_user (user_id INTEGER NOT NULL, discussion_id INTEGER NOT NULL, last_read_at DATETIME DEFAULT NULL, last_read_post_number INTEGER DEFAULT NULL, PRIMARY KEY(user_id, discussion_id));
|
||||
CREATE TABLE db_prefix_email_tokens (token VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL COLLATE "BINARY", user_id INTEGER NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(token));
|
||||
CREATE TABLE db_prefix_notifications (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, from_user_id INTEGER DEFAULT NULL, type VARCHAR(255) NOT NULL COLLATE "BINARY", subject_id INTEGER DEFAULT NULL, data BLOB DEFAULT NULL, created_at DATETIME NOT NULL, is_deleted BOOLEAN DEFAULT 0 NOT NULL, read_at DATETIME DEFAULT NULL);
|
||||
CREATE TABLE db_prefix_password_tokens (token VARCHAR(255) NOT NULL, user_id INTEGER NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(token));
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_post_user" ("post_id" integer not null, "user_id" integer not null, foreign key("post_id") references "db_prefix_posts"("id") on delete cascade, foreign key("user_id") references "db_prefix_users"("id") on delete cascade, primary key ("post_id", "user_id"));
|
||||
CREATE TABLE db_prefix_users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(255) NOT NULL COLLATE "BINARY", email VARCHAR(255) NOT NULL COLLATE "BINARY", is_email_confirmed BOOLEAN DEFAULT 0 NOT NULL, password VARCHAR(255) NOT NULL COLLATE "BINARY", avatar_url VARCHAR(255) DEFAULT NULL, preferences BLOB DEFAULT NULL, joined_at DATETIME DEFAULT NULL, last_seen_at DATETIME DEFAULT NULL, marked_all_as_read_at DATETIME DEFAULT NULL, read_notifications_at DATETIME DEFAULT NULL, discussion_count INTEGER DEFAULT 0 NOT NULL, comment_count INTEGER DEFAULT 0 NOT NULL);
|
||||
CREATE UNIQUE INDEX db_prefix_users_email_unique ON db_prefix_users (email);
|
||||
CREATE UNIQUE INDEX db_prefix_users_username_unique ON db_prefix_users (username);
|
||||
CREATE INDEX "db_prefix_users_joined_at_index" on "db_prefix_users" ("joined_at");
|
||||
CREATE INDEX "db_prefix_users_last_seen_at_index" on "db_prefix_users" ("last_seen_at");
|
||||
CREATE INDEX "db_prefix_users_discussion_count_index" on "db_prefix_users" ("discussion_count");
|
||||
CREATE INDEX "db_prefix_users_comment_count_index" on "db_prefix_users" ("comment_count");
|
||||
CREATE INDEX "db_prefix_notifications_user_id_index" on "db_prefix_notifications" ("user_id");
|
||||
CREATE TABLE db_prefix_registration_tokens (token VARCHAR(255) NOT NULL, payload CLOB DEFAULT NULL, created_at DATETIME NOT NULL, "provider" varchar not null, "identifier" varchar not null, "user_attributes" text, PRIMARY KEY(token));
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_login_providers" ("id" integer primary key autoincrement not null, "user_id" integer not null, "provider" varchar not null, "identifier" varchar not null, "created_at" datetime, "last_login_at" datetime, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
|
||||
CREATE UNIQUE INDEX "db_prefix_login_providers_provider_identifier_unique" on "db_prefix_login_providers" ("provider", "identifier");
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_access_tokens" ("id" integer primary key autoincrement not null, "token" varchar not null, "user_id" integer not null, "last_activity_at" datetime, "created_at" datetime not null, "type" varchar, "title" varchar, "last_ip_address" varchar, "last_user_agent" varchar, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
|
||||
CREATE INDEX "db_prefix_access_tokens_type_index" on "db_prefix_access_tokens" ("type");
|
||||
CREATE UNIQUE INDEX "db_prefix_access_tokens_token_unique" on "db_prefix_access_tokens" ("token");
|
||||
CREATE TABLE db_prefix_posts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, discussion_id INTEGER NOT NULL, number INTEGER DEFAULT NULL, created_at DATETIME NOT NULL, user_id INTEGER DEFAULT NULL, type VARCHAR(255) DEFAULT NULL, content CLOB DEFAULT NULL --
|
||||
, edited_at DATETIME DEFAULT NULL, edited_user_id INTEGER DEFAULT NULL, hidden_at DATETIME DEFAULT NULL, hidden_user_id INTEGER DEFAULT NULL, ip_address VARCHAR(255) DEFAULT NULL, is_private BOOLEAN DEFAULT 0 NOT NULL, foreign key("discussion_id") references "db_prefix_discussions"("id") on delete cascade,
|
||||
foreign key("user_id") references "db_prefix_users"("id") on delete set null,
|
||||
foreign key("edited_user_id") references "db_prefix_users"("id") on delete set null,
|
||||
foreign key("hidden_user_id") references "db_prefix_users"("id") on delete set null);
|
||||
CREATE INDEX db_prefix_posts_user_id_created_at_index ON db_prefix_posts (user_id, created_at);
|
||||
CREATE INDEX db_prefix_posts_discussion_id_created_at_index ON db_prefix_posts (discussion_id, created_at);
|
||||
CREATE INDEX db_prefix_posts_discussion_id_number_index ON db_prefix_posts (discussion_id, number);
|
||||
CREATE UNIQUE INDEX db_prefix_posts_discussion_id_number_unique ON db_prefix_posts (discussion_id, number);
|
||||
CREATE INDEX "db_prefix_posts_type_index" on "db_prefix_posts" ("type");
|
||||
CREATE INDEX "db_prefix_posts_type_created_at_index" on "db_prefix_posts" ("type", "created_at");
|
||||
CREATE TABLE IF NOT EXISTS "db_prefix_unsubscribe_tokens" ("id" integer primary key autoincrement not null, "user_id" integer not null, "email_type" varchar not null, "token" varchar not null, "unsubscribed_at" datetime, "created_at" datetime, "updated_at" datetime, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
|
||||
CREATE INDEX "db_prefix_unsubscribe_tokens_user_id_index" on "db_prefix_unsubscribe_tokens" ("user_id");
|
||||
CREATE INDEX "db_prefix_unsubscribe_tokens_email_type_index" on "db_prefix_unsubscribe_tokens" ("email_type");
|
||||
CREATE INDEX "db_prefix_unsubscribe_tokens_token_index" on "db_prefix_unsubscribe_tokens" ("token");
|
||||
CREATE INDEX "db_prefix_unsubscribe_tokens_user_id_email_type_index" on "db_prefix_unsubscribe_tokens" ("user_id", "email_type");
|
||||
CREATE UNIQUE INDEX "db_prefix_unsubscribe_tokens_token_unique" on "db_prefix_unsubscribe_tokens" ("token");
|
||||
CREATE TABLE db_prefix_discussions (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL COLLATE "BINARY", comment_count INTEGER DEFAULT 1 NOT NULL, participant_count INTEGER DEFAULT 0 NOT NULL, created_at DATETIME NOT NULL, user_id INTEGER DEFAULT NULL, first_post_id INTEGER DEFAULT NULL, last_posted_at DATETIME DEFAULT NULL, last_posted_user_id INTEGER DEFAULT NULL, last_post_id INTEGER DEFAULT NULL, last_post_number INTEGER DEFAULT NULL, hidden_at DATETIME DEFAULT NULL, hidden_user_id INTEGER DEFAULT NULL, slug VARCHAR(255) NOT NULL COLLATE "BINARY", is_private BOOLEAN DEFAULT 0 NOT NULL, foreign key("user_id") references "db_prefix_users"("id") on delete set null,
|
||||
foreign key("last_posted_user_id") references "db_prefix_users"("id") on delete set null,
|
||||
foreign key("hidden_user_id") references "db_prefix_users"("id") on delete set null,
|
||||
foreign key("first_post_id") references "db_prefix_posts"("id") on delete set null,
|
||||
foreign key("last_post_id") references "db_prefix_posts"("id") on delete set null);
|
||||
CREATE INDEX db_prefix_discussions_last_posted_at_index ON db_prefix_discussions (last_posted_at);
|
||||
CREATE INDEX db_prefix_discussions_last_posted_user_id_index ON db_prefix_discussions (last_posted_user_id);
|
||||
CREATE INDEX db_prefix_discussions_created_at_index ON db_prefix_discussions (created_at);
|
||||
CREATE INDEX db_prefix_discussions_user_id_index ON db_prefix_discussions (user_id);
|
||||
CREATE INDEX db_prefix_discussions_comment_count_index ON db_prefix_discussions (comment_count);
|
||||
CREATE INDEX db_prefix_discussions_participant_count_index ON db_prefix_discussions (participant_count);
|
||||
CREATE INDEX db_prefix_discussions_hidden_at_index ON db_prefix_discussions (hidden_at);
|
||||
INSERT INTO db_prefix_migrations VALUES(1,'2015_02_24_000000_create_access_tokens_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(2,'2015_02_24_000000_create_api_keys_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(3,'2015_02_24_000000_create_config_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(4,'2015_02_24_000000_create_discussions_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(5,'2015_02_24_000000_create_email_tokens_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(6,'2015_02_24_000000_create_groups_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(7,'2015_02_24_000000_create_notifications_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(8,'2015_02_24_000000_create_password_tokens_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(9,'2015_02_24_000000_create_permissions_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(10,'2015_02_24_000000_create_posts_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(11,'2015_02_24_000000_create_users_discussions_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(12,'2015_02_24_000000_create_users_groups_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(13,'2015_02_24_000000_create_users_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(14,'2015_09_15_000000_create_auth_tokens_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(15,'2015_09_20_224327_add_hide_to_discussions',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(16,'2015_09_22_030432_rename_notification_read_time',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(17,'2015_10_07_130531_rename_config_to_settings',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(18,'2015_10_24_194000_add_ip_address_to_posts',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(19,'2015_12_05_042721_change_access_tokens_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(20,'2015_12_17_194247_change_settings_value_column_to_text',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(21,'2016_02_04_095452_add_slug_to_discussions',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(22,'2017_04_07_114138_add_is_private_to_discussions',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(23,'2017_04_07_114138_add_is_private_to_posts',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(24,'2018_01_11_093900_change_access_tokens_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(25,'2018_01_11_094000_change_access_tokens_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(26,'2018_01_11_095000_change_api_keys_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(27,'2018_01_11_101800_rename_auth_tokens_to_registration_tokens',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(28,'2018_01_11_102000_change_registration_tokens_rename_id_to_token',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(29,'2018_01_11_102100_change_registration_tokens_created_at_to_datetime',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(30,'2018_01_11_120604_change_posts_table_to_innodb',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(31,'2018_01_11_155200_change_discussions_rename_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(32,'2018_01_11_155300_change_discussions_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(33,'2018_01_15_071700_rename_users_discussions_to_discussion_user',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(34,'2018_01_15_071800_change_discussion_user_rename_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(35,'2018_01_15_071900_change_discussion_user_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(36,'2018_01_15_072600_change_email_tokens_rename_id_to_token',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(37,'2018_01_15_072700_change_email_tokens_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(38,'2018_01_15_072800_change_email_tokens_created_at_to_datetime',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(39,'2018_01_18_130400_rename_permissions_to_group_permission',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(40,'2018_01_18_130500_change_group_permission_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(41,'2018_01_18_130600_rename_users_groups_to_group_user',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(42,'2018_01_18_130700_change_group_user_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(43,'2018_01_18_133000_change_notifications_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(44,'2018_01_18_133100_change_notifications_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(45,'2018_01_18_134400_change_password_tokens_rename_id_to_token',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(46,'2018_01_18_134500_change_password_tokens_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(47,'2018_01_18_134600_change_password_tokens_created_at_to_datetime',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(48,'2018_01_18_135000_change_posts_rename_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(49,'2018_01_18_135100_change_posts_add_foreign_keys',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(50,'2018_01_30_112238_add_fulltext_index_to_discussions_title',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(51,'2018_01_30_220100_create_post_user_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(52,'2018_01_30_222900_change_users_rename_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(55,'2018_09_15_041340_add_users_indicies',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(56,'2018_09_15_041828_add_discussions_indicies',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(57,'2018_09_15_043337_add_notifications_indices',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(58,'2018_09_15_043621_add_posts_indices',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(59,'2018_09_22_004100_change_registration_tokens_columns',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(60,'2018_09_22_004200_create_login_providers_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(61,'2018_10_08_144700_add_shim_prefix_to_group_icons',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(62,'2019_10_12_195349_change_posts_add_discussion_foreign_key',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(63,'2020_03_19_134512_change_discussions_default_comment_count',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(64,'2020_04_21_130500_change_permission_groups_add_is_hidden',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(65,'2021_03_02_040000_change_access_tokens_add_type',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(66,'2021_03_02_040500_change_access_tokens_add_id',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(67,'2021_03_02_041000_change_access_tokens_add_title_ip_agent',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(68,'2021_04_18_040500_change_migrations_add_id_primary_key',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(69,'2021_04_18_145100_change_posts_content_column_to_mediumtext',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(70,'2021_05_10_000000_rename_permissions',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(71,'2022_05_20_000000_add_timestamps_to_groups_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(72,'2022_05_20_000001_add_created_at_to_group_user_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(73,'2022_05_20_000002_add_created_at_to_group_permission_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(74,'2022_07_14_000000_add_type_index_to_posts',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(75,'2022_07_14_000001_add_type_created_at_composite_index_to_posts',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(76,'2022_08_06_000000_change_access_tokens_last_activity_at_to_nullable',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(77,'2023_08_19_000000_create_unsubscribe_tokens_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(78,'2023_10_23_000000_drop_post_number_index_column_from_discussions_table',NULL);
|
||||
INSERT INTO db_prefix_migrations VALUES(79,'2024_05_05_000000_add_sqlite_keys',NULL);
|
@@ -60,7 +60,8 @@ class AdminPayload
|
||||
$document->payload['searchDrivers'] = $this->getSearchDrivers();
|
||||
|
||||
$document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion();
|
||||
$document->payload['mysqlVersion'] = $this->appInfo->identifyDatabaseVersion();
|
||||
$document->payload['dbDriver'] = $this->appInfo->identifyDatabaseDriver();
|
||||
$document->payload['dbVersion'] = $this->appInfo->identifyDatabaseVersion();
|
||||
$document->payload['debugEnabled'] = Arr::get($this->config, 'debug');
|
||||
|
||||
if ($this->appInfo->scheduledTasksRegistered()) {
|
||||
|
@@ -31,13 +31,14 @@ class Index
|
||||
$extensionsEnabled = json_decode($this->settings->get('extensions_enabled', '{}'), true);
|
||||
$csrfToken = $request->getAttribute('session')->token();
|
||||
|
||||
$mysqlVersion = $document->payload['mysqlVersion'];
|
||||
$dbDriver = $document->payload['dbDriver'];
|
||||
$dbVersion = $document->payload['dbVersion'];
|
||||
$phpVersion = $document->payload['phpVersion'];
|
||||
$flarumVersion = Application::VERSION;
|
||||
|
||||
$document->content = $this->view->make(
|
||||
'flarum.admin::frontend.content.admin',
|
||||
compact('extensions', 'extensionsEnabled', 'csrfToken', 'flarumVersion', 'phpVersion', 'mysqlVersion')
|
||||
compact('extensions', 'extensionsEnabled', 'csrfToken', 'flarumVersion', 'phpVersion', 'dbVersion', 'dbDriver')
|
||||
);
|
||||
|
||||
return $document;
|
||||
|
@@ -36,14 +36,18 @@ class WhenSavingSettings
|
||||
{
|
||||
if (array_key_exists('safe_mode_extensions', $event->settings)) {
|
||||
$safeModeExtensions = json_decode($event->settings['safe_mode_extensions'] ?? '[]', true);
|
||||
$sorted = [];
|
||||
|
||||
$extensions = $this->extensions->getExtensions()->filter(function ($extension) use ($safeModeExtensions) {
|
||||
return in_array($extension->getId(), $safeModeExtensions);
|
||||
});
|
||||
if ($safeModeExtensions) {
|
||||
$extensions = $this->extensions->getExtensions()->filter(function ($extension) use ($safeModeExtensions) {
|
||||
return in_array($extension->getId(), $safeModeExtensions);
|
||||
});
|
||||
|
||||
$sorted = array_map(fn (Extension $e) => $e->getId(), $this->extensions->sortDependencies($extensions->all()));
|
||||
$sorted = array_map(fn (Extension $e) => $e->getId(), $this->extensions->sortDependencies($extensions->all()));
|
||||
$sorted = array_values($sorted);
|
||||
}
|
||||
|
||||
$event->settings['safe_mode_extensions'] = json_encode(array_values($sorted));
|
||||
$event->settings['safe_mode_extensions'] = json_encode($sorted);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace Flarum\Api;
|
||||
|
||||
use Flarum\Api\Endpoint\EndpointInterface;
|
||||
use Flarum\Api\Endpoint\Endpoint;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
@@ -22,7 +22,6 @@ use Flarum\Http\UrlGenerator;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Laminas\Stratigility\MiddlewarePipe;
|
||||
use ReflectionClass;
|
||||
use Tobyz\JsonApiServer\Endpoint\Endpoint;
|
||||
|
||||
class ApiServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -53,7 +52,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
$api->container($container);
|
||||
|
||||
foreach ($resources as $resourceClass) {
|
||||
/** @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource */
|
||||
/** @var \Flarum\Api\Resource\AbstractResource $resource */
|
||||
$resource = $container->make($resourceClass);
|
||||
$api->resource($resource->boot($api));
|
||||
}
|
||||
@@ -189,7 +188,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
*
|
||||
* We avoid dependency injection here to avoid early resolution.
|
||||
*
|
||||
* @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource
|
||||
* @var \Flarum\Api\Resource\AbstractResource $resource
|
||||
*/
|
||||
$resource = (new ReflectionClass($resourceClass))->newInstanceWithoutConstructor();
|
||||
|
||||
@@ -199,7 +198,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
* None of the injected dependencies should be directly used within
|
||||
* the `endpoints` method. Encourage using callbacks.
|
||||
*
|
||||
* @var array<Endpoint&EndpointInterface> $endpoints
|
||||
* @var array<Endpoint> $endpoints
|
||||
*/
|
||||
$endpoints = $resource->resolveEndpoints(true);
|
||||
|
||||
|
@@ -12,10 +12,18 @@ namespace Flarum\Api;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Search\SearchResults;
|
||||
use Flarum\User\User;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobyz\JsonApiServer\Context as BaseContext;
|
||||
use Tobyz\JsonApiServer\Resource\Resource;
|
||||
use Tobyz\JsonApiServer\Schema\Field\Field;
|
||||
use WeakMap;
|
||||
|
||||
class Context extends BaseContext
|
||||
{
|
||||
private WeakMap $fields;
|
||||
|
||||
public int|string|null $modelId = null;
|
||||
public ?array $requestIncludes = null;
|
||||
protected ?SearchResults $search = null;
|
||||
|
||||
/**
|
||||
@@ -29,6 +37,34 @@ class Context extends BaseContext
|
||||
*/
|
||||
protected array $parameters = [];
|
||||
|
||||
public function __construct(\Tobyz\JsonApiServer\JsonApi $api, ServerRequestInterface $request)
|
||||
{
|
||||
$this->fields = new WeakMap();
|
||||
|
||||
parent::__construct($api, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields for the given resource, keyed by name.
|
||||
*
|
||||
* @return array<string, Field>
|
||||
*/
|
||||
public function fields(Resource $resource): array
|
||||
{
|
||||
if (isset($this->fields[$resource])) {
|
||||
return $this->fields[$resource];
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
foreach ($resource->resolveFields() as $field) {
|
||||
$fields[$field->name] = $field;
|
||||
}
|
||||
|
||||
return $this->fields[$resource] = $fields;
|
||||
}
|
||||
|
||||
public function withSearchResults(SearchResults $search): static
|
||||
{
|
||||
$new = clone $this;
|
||||
@@ -96,4 +132,47 @@ class Context extends BaseContext
|
||||
{
|
||||
return $this->endpoint instanceof Endpoint\Index && (! $resource || is_a($this->collection, $resource));
|
||||
}
|
||||
|
||||
public function withRequest(ServerRequestInterface $request): static
|
||||
{
|
||||
$new = parent::withRequest($request);
|
||||
$new->requestIncludes = null;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withModelId(int|string|null $id): static
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->modelId = $id;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withRequestIncludes(array $requestIncludes): static
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->requestIncludes = $requestIncludes;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function extractIdFromPath(BaseContext $context): ?string
|
||||
{
|
||||
/** @var Endpoint\Endpoint $endpoint */
|
||||
$endpoint = $context->endpoint;
|
||||
|
||||
$currentPath = trim($context->path(), '/');
|
||||
$path = trim($context->collection->name().$endpoint->path, '/');
|
||||
|
||||
if (! str_contains($path, '{id}')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$segments = explode('/', $path);
|
||||
$idSegmentIndex = array_search('{id}', $segments);
|
||||
$currentPathSegments = explode('/', $currentPath);
|
||||
|
||||
return $currentPathSegments[$idSegmentIndex] ?? null;
|
||||
}
|
||||
}
|
||||
|
@@ -10,9 +10,9 @@
|
||||
namespace Flarum\Api\Endpoint\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Resource\AbstractResource;
|
||||
use Tobyz\JsonApiServer\Schema\Sort;
|
||||
|
||||
trait ExtractsListingParams
|
||||
@@ -110,11 +110,13 @@ trait ExtractsListingParams
|
||||
|
||||
public function getAvailableSorts(Context $context): array
|
||||
{
|
||||
if (! $context->collection instanceof AbstractResource) {
|
||||
$collection = $context->collection;
|
||||
|
||||
if (! $collection instanceof AbstractResource) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$asc = collect($context->collection->resolveSorts())
|
||||
$asc = collect($collection->resolveSorts())
|
||||
->filter(fn (Sort $field) => $field->isVisible($context))
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
@@ -14,9 +14,14 @@ use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Exception\NotAuthenticatedException;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility;
|
||||
|
||||
trait HasAuthorization
|
||||
{
|
||||
use HasVisibility {
|
||||
isVisible as parentIsVisible;
|
||||
}
|
||||
|
||||
protected bool|Closure $authenticated = false;
|
||||
|
||||
protected null|string|Closure $ability = null;
|
||||
@@ -86,6 +91,6 @@ trait HasAuthorization
|
||||
$actor->assertCan($ability, $context->model);
|
||||
}
|
||||
|
||||
return parent::isVisible($context);
|
||||
return $this->parentIsVisible($context);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,10 @@ use Tobyz\JsonApiServer\Context;
|
||||
|
||||
trait HasCustomHooks
|
||||
{
|
||||
use HasHooks {
|
||||
resolveCallable as protected resolveHookCallable;
|
||||
}
|
||||
|
||||
protected function resolveCallable(callable|string $callable, Context $context): callable
|
||||
{
|
||||
if (is_string($callable)) {
|
||||
|
213
framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php
Normal file
213
framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Str;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
/**
|
||||
* This is directed at eager loading relationships apart from the request includes.
|
||||
*/
|
||||
trait HasEagerLoading
|
||||
{
|
||||
/**
|
||||
* @var array<string|callable>
|
||||
*/
|
||||
protected array $loadRelations = [];
|
||||
|
||||
/**
|
||||
* @var array<string, callable>
|
||||
*/
|
||||
protected array $loadRelationWhere = [];
|
||||
|
||||
/**
|
||||
* Eager loads relationships needed for serializer logic.
|
||||
*
|
||||
* @param string|string[] $relations
|
||||
*/
|
||||
public function eagerLoad(array|string|Closure $relations): static
|
||||
{
|
||||
if (! is_callable($relations)) {
|
||||
$this->loadRelations = array_merge($this->loadRelations, array_map('strval', (array) $relations));
|
||||
} else {
|
||||
$this->loadRelations[] = $relations;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eager load relations when a relation is included in the serialized response.
|
||||
*
|
||||
* @param array<string, array<string>> $includedToRelations An array of included relation to relations to load 'includedRelation' => ['relation1', 'relation2']
|
||||
*/
|
||||
public function eagerLoadWhenIncluded(array $includedToRelations): static
|
||||
{
|
||||
return $this->eagerLoad(function (array $included) use ($includedToRelations) {
|
||||
$relations = [];
|
||||
|
||||
foreach ($includedToRelations as $includedRelation => $includedRelations) {
|
||||
if (in_array($includedRelation, $included)) {
|
||||
$relations = array_merge($relations, $includedRelations);
|
||||
}
|
||||
}
|
||||
|
||||
return $relations;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows loading a relationship with additional query modification.
|
||||
*
|
||||
* @param string $relation: Relationship name, see load method description.
|
||||
* @param callable $callback
|
||||
*
|
||||
* The callback to modify the query, should accept:
|
||||
* - \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query: A query object.
|
||||
* - Context $context: An instance of the API context.
|
||||
* - array $relations: An array of relations that are to be loaded.
|
||||
*/
|
||||
public function eagerLoadWhere(string $relation, callable $callback): static
|
||||
{
|
||||
$this->loadRelationWhere = array_merge($this->loadRelationWhere, [$relation => $callback]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eager loads relationships before serialization.
|
||||
*/
|
||||
protected function loadRelations(Collection $models, Context $context, array $included = []): void
|
||||
{
|
||||
if (! $context->collection instanceof AbstractDatabaseResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
$included = $this->stringInclude($included);
|
||||
$models = $models->filter(fn ($model) => $model instanceof Model);
|
||||
|
||||
$relations = $this->compileSimpleEagerLoads($context, $included);
|
||||
$addedRelationWhere = $this->compileWhereEagerLoads($context);
|
||||
|
||||
foreach ($addedRelationWhere as $name => $callable) {
|
||||
$relations[] = $name;
|
||||
}
|
||||
|
||||
if (! empty($relations)) {
|
||||
$relations = array_unique($relations);
|
||||
}
|
||||
|
||||
$whereRelations = [];
|
||||
$simpleRelations = [];
|
||||
|
||||
foreach ($relations as $relation) {
|
||||
if (isset($addedRelationWhere[$relation])) {
|
||||
$whereRelations[$relation] = $addedRelationWhere[$relation];
|
||||
} else {
|
||||
$simpleRelations[] = $relation;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($whereRelations)) {
|
||||
$models->loadMissing($whereRelations);
|
||||
}
|
||||
|
||||
if (! empty($simpleRelations)) {
|
||||
$models->loadMissing($simpleRelations);
|
||||
}
|
||||
}
|
||||
|
||||
protected function compileSimpleEagerLoads(Context $context, array $included): array
|
||||
{
|
||||
$relations = [];
|
||||
|
||||
foreach ($this->loadRelations as $relation) {
|
||||
if (is_callable($relation)) {
|
||||
$returnedRelations = $relation($included, $context);
|
||||
$relations = array_merge($relations, array_map('strval', (array) $returnedRelations));
|
||||
} else {
|
||||
$relations[] = $relation;
|
||||
}
|
||||
}
|
||||
|
||||
return $relations;
|
||||
}
|
||||
|
||||
protected function compileWhereEagerLoads(Context $context): array
|
||||
{
|
||||
$relations = [];
|
||||
|
||||
foreach ($this->loadRelationWhere as $name => $callable) {
|
||||
$relations[$name] = function ($query) use ($callable, $context) {
|
||||
$callable($query, $context);
|
||||
};
|
||||
}
|
||||
|
||||
return $relations;
|
||||
}
|
||||
|
||||
public function getEagerLoadsFor(string $included, Context $context): array
|
||||
{
|
||||
$subRelations = [];
|
||||
|
||||
$includes = $this->stringInclude($this->getInclude($context));
|
||||
|
||||
foreach ($this->compileSimpleEagerLoads($context, $includes) as $relation) {
|
||||
if (! is_callable($relation)) {
|
||||
if (Str::startsWith($relation, "$included.")) {
|
||||
$subRelations[] = Str::after($relation, "$included.");
|
||||
}
|
||||
} else {
|
||||
$returnedRelations = $relation($includes, $context);
|
||||
$subRelations = array_merge($subRelations, array_map('strval', (array) $returnedRelations));
|
||||
}
|
||||
}
|
||||
|
||||
return $subRelations;
|
||||
}
|
||||
|
||||
public function getWhereEagerLoadsFor(string $included, Context $context): array
|
||||
{
|
||||
$subRelations = [];
|
||||
|
||||
foreach ($this->loadRelationWhere as $relation => $callable) {
|
||||
if (Str::startsWith($relation, "$included.")) {
|
||||
$subRelations[$relation] = Str::after($relation, "$included.");
|
||||
}
|
||||
}
|
||||
|
||||
return $subRelations;
|
||||
}
|
||||
|
||||
/**
|
||||
* From format of: 'relation' => [ ...nested ] to ['relation', 'relation.nested'].
|
||||
*/
|
||||
private function stringInclude(array $include): array
|
||||
{
|
||||
$relations = [];
|
||||
|
||||
foreach ($include as $relation => $nested) {
|
||||
$relations[] = $relation;
|
||||
|
||||
if (is_array($nested)) {
|
||||
foreach ($this->stringInclude($nested) as $nestedRelation) {
|
||||
$relations[] = $relation.'.'.$nestedRelation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $relations;
|
||||
}
|
||||
}
|
64
framework/core/src/Api/Endpoint/Concerns/HasHooks.php
Normal file
64
framework/core/src/Api/Endpoint/Concerns/HasHooks.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
trait HasHooks
|
||||
{
|
||||
protected array $before = [];
|
||||
protected array $after = [];
|
||||
|
||||
public function before(callable|string $callback): static
|
||||
{
|
||||
$this->before[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function after(callable|string $callback): static
|
||||
{
|
||||
$this->after[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function resolveCallable(callable|string $callable, Context $context): callable
|
||||
{
|
||||
if (is_string($callable)) {
|
||||
return new $callable();
|
||||
}
|
||||
|
||||
return $callable;
|
||||
}
|
||||
|
||||
protected function callBeforeHook(Context $context): void
|
||||
{
|
||||
foreach ($this->before as $before) {
|
||||
$before = $this->resolveCallable($before, $context);
|
||||
$before($context);
|
||||
}
|
||||
}
|
||||
|
||||
protected function callAfterHook(Context $context, mixed $data): mixed
|
||||
{
|
||||
foreach ($this->after as $after) {
|
||||
$after = $this->resolveCallable($after, $context);
|
||||
$data = $after($context, $data);
|
||||
|
||||
if (empty($data)) {
|
||||
throw new RuntimeException('The after hook must return the data back.');
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
29
framework/core/src/Api/Endpoint/Concerns/IncludesData.php
Normal file
29
framework/core/src/Api/Endpoint/Concerns/IncludesData.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
trait IncludesData
|
||||
{
|
||||
use \Tobyz\JsonApiServer\Endpoint\Concerns\IncludesData;
|
||||
|
||||
public function addDefaultInclude(array $include): static
|
||||
{
|
||||
$this->defaultInclude = array_merge($this->defaultInclude ?? [], $include);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDefaultInclude(array $include): static
|
||||
{
|
||||
$this->defaultInclude = array_diff($this->defaultInclude ?? [], $include);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -0,0 +1,175 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use Flarum\Api\Schema\Concerns\HasValidationRules;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Translation\ArrayLoader;
|
||||
use Illuminate\Translation\Translator;
|
||||
use Illuminate\Validation\Factory;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Endpoint\Concerns\SavesData;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\ConflictException;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
||||
use Tobyz\JsonApiServer\Schema\Field\Attribute;
|
||||
|
||||
trait SavesAndValidatesData
|
||||
{
|
||||
use SavesData {
|
||||
parseData as protected parentParseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the field values within a data object pass validation.
|
||||
*
|
||||
* @param \Flarum\Api\Context $context
|
||||
* @throws UnprocessableEntityException
|
||||
*/
|
||||
protected function assertDataValid(Context $context, array $data): void
|
||||
{
|
||||
$this->mutateDataBeforeValidation($context, $data);
|
||||
|
||||
$collection = $context->collection;
|
||||
|
||||
$rules = [
|
||||
'attributes' => [],
|
||||
'relationships' => [],
|
||||
];
|
||||
|
||||
$messages = [];
|
||||
$attributes = [];
|
||||
|
||||
foreach ($context->fields($context->resource) as $field) {
|
||||
$writable = $field->isWritable($context->withField($field));
|
||||
|
||||
if (! $writable || ! in_array(HasValidationRules::class, class_uses_recursive($field))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $field instanceof Attribute ? 'attributes' : 'relationships';
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$rules[$type] = array_merge($rules[$type], $field->getValidationRules($context));
|
||||
// @phpstan-ignore-next-line
|
||||
$messages = array_merge($messages, $field->getValidationMessages($context));
|
||||
// @phpstan-ignore-next-line
|
||||
$attributes = array_merge($attributes, $field->getValidationAttributes($context));
|
||||
}
|
||||
|
||||
if (method_exists($collection, 'validationFactory')) {
|
||||
$factory = $collection->validationFactory();
|
||||
} else {
|
||||
$loader = new ArrayLoader();
|
||||
$translator = new Translator($loader, 'en');
|
||||
$factory = new Factory($translator);
|
||||
}
|
||||
|
||||
$attributeValidator = $factory->make($data['attributes'], $rules['attributes'], $messages, $attributes);
|
||||
$relationshipValidator = $factory->make($data['relationships'], $rules['relationships'], $messages, $attributes);
|
||||
|
||||
$this->validate('attributes', $attributeValidator);
|
||||
$this->validate('relationships', $relationshipValidator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnprocessableEntityException if any fields do not pass validation.
|
||||
*/
|
||||
protected function validate(string $type, Validator $validator): void
|
||||
{
|
||||
if ($validator->fails()) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($validator->errors()->messages() as $field => $messages) {
|
||||
$errors[] = [
|
||||
'source' => ['pointer' => "/data/$type/$field"],
|
||||
'detail' => implode(' ', $messages),
|
||||
];
|
||||
}
|
||||
|
||||
throw new UnprocessableEntityException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
protected function mutateDataBeforeValidation(Context $context, array $data): array
|
||||
{
|
||||
if (method_exists($context->resource, 'mutateDataBeforeValidation')) {
|
||||
return $context->resource->mutateDataBeforeValidation($context, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate a JSON:API document's `data` member.
|
||||
*
|
||||
* @throws BadRequestException if the `data` member is invalid.
|
||||
*/
|
||||
final protected function parseData(Context $context): array
|
||||
{
|
||||
$body = (array) $context->body();
|
||||
|
||||
if (! isset($body['data']) || ! is_array($body['data'])) {
|
||||
throw (new BadRequestException('data must be an object'))->setSource([
|
||||
'pointer' => '/data',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! isset($body['data']['type'])) {
|
||||
if (isset($context->collection->resources()[0])) {
|
||||
$body['data']['type'] = $context->collection->resources()[0];
|
||||
} else {
|
||||
throw (new BadRequestException('data.type must be present'))->setSource([
|
||||
'pointer' => '/data/type',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($context->model)) {
|
||||
// commented out to reduce strictness.
|
||||
// if (!isset($body['data']['id'])) {
|
||||
// throw (new BadRequestException('data.id must be present'))->setSource([
|
||||
// 'pointer' => '/data/id',
|
||||
// ]);
|
||||
// }
|
||||
|
||||
if (isset($body['data']['id']) && $body['data']['id'] !== $context->resource->getId($context->model, $context)) {
|
||||
throw (new ConflictException('data.id does not match the resource ID'))->setSource([
|
||||
'pointer' => '/data/id',
|
||||
]);
|
||||
}
|
||||
} elseif (isset($body['data']['id'])) {
|
||||
throw (new ForbiddenException('Client-generated IDs are not supported'))->setSource([
|
||||
'pointer' => '/data/id',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! in_array($body['data']['type'], $context->collection->resources())) {
|
||||
throw (new ConflictException(
|
||||
'collection does not support this resource type',
|
||||
))->setSource(['pointer' => '/data/type']);
|
||||
}
|
||||
|
||||
if (array_key_exists('attributes', $body['data']) && ! is_array($body['data']['attributes'])) {
|
||||
throw (new BadRequestException('data.attributes must be an object'))->setSource([
|
||||
'pointer' => '/data/attributes',
|
||||
]);
|
||||
}
|
||||
|
||||
if (array_key_exists('relationships', $body['data']) && ! is_array($body['data']['relationships'])) {
|
||||
throw (new BadRequestException('data.relationships must be an object'))->setSource([
|
||||
'pointer' => '/data/relationships',
|
||||
]);
|
||||
}
|
||||
|
||||
return array_merge(['attributes' => [], 'relationships' => []], $body['data']);
|
||||
}
|
||||
}
|
46
framework/core/src/Api/Endpoint/Concerns/ShowsResources.php
Normal file
46
framework/core/src/Api/Endpoint/Concerns/ShowsResources.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use Flarum\Api\Serializer;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Endpoint\Concerns\IncludesData;
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
||||
|
||||
trait ShowsResources
|
||||
{
|
||||
use HasMeta;
|
||||
use IncludesData;
|
||||
|
||||
protected function showResource(Context $context, mixed $model): array
|
||||
{
|
||||
$serializer = new Serializer($context);
|
||||
|
||||
$serializer->addPrimary(
|
||||
$context->resource($context->collection->resource($model, $context)),
|
||||
$model,
|
||||
$this->getInclude($context),
|
||||
);
|
||||
|
||||
[$primary, $included] = $serializer->serialize();
|
||||
|
||||
$document = ['data' => $primary[0]];
|
||||
|
||||
if (count($included)) {
|
||||
$document['included'] = $included;
|
||||
}
|
||||
|
||||
if ($meta = $this->serializeMeta($context)) {
|
||||
$document['meta'] = $meta;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
@@ -9,17 +9,89 @@
|
||||
|
||||
namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Create as BaseCreate;
|
||||
use Flarum\Api\Endpoint\Concerns\IncludesData;
|
||||
use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData;
|
||||
use Flarum\Api\Endpoint\Concerns\ShowsResources;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Flarum\Database\Eloquent\Collection;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Resource\Creatable;
|
||||
|
||||
class Create extends BaseCreate implements EndpointInterface
|
||||
use function Tobyz\JsonApiServer\has_value;
|
||||
use function Tobyz\JsonApiServer\json_api_response;
|
||||
use function Tobyz\JsonApiServer\set_value;
|
||||
|
||||
class Create extends Endpoint
|
||||
{
|
||||
use SavesAndValidatesData;
|
||||
use ShowsResources;
|
||||
use IncludesData;
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
|
||||
public static function make(?string $name = null): static
|
||||
{
|
||||
return parent::make($name ?? 'create');
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->route('POST', '/')
|
||||
->action(function (Context $context): ?object {
|
||||
if (str_contains($context->path(), '/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $context->collection;
|
||||
|
||||
if (! $collection instanceof Creatable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($collection), Creatable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$this->callBeforeHook($context);
|
||||
|
||||
$data = $this->parseData($context);
|
||||
|
||||
/** @var AbstractResource $resource */
|
||||
$resource = $context->resource($data['type']);
|
||||
|
||||
$context = $context
|
||||
->withResource($resource)
|
||||
->withModel($model = $collection->newModel($context));
|
||||
|
||||
$this->assertFieldsValid($context, $data);
|
||||
$this->fillDefaultValues($context, $data);
|
||||
$this->deserializeValues($context, $data);
|
||||
$this->assertDataValid($context, $data);
|
||||
$this->setValues($context, $data);
|
||||
|
||||
$context = $context->withModel($model = $resource->createAction($model, $context));
|
||||
|
||||
$this->saveFields($context, $data);
|
||||
|
||||
return $this->callAfterHook($context, $model);
|
||||
})
|
||||
->beforeSerialization(function (Context $context, object $model) {
|
||||
$this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context));
|
||||
})
|
||||
->response(function (Context $context, object $model) {
|
||||
return json_api_response($document = $this->showResource($context, $model))
|
||||
->withStatus(201)
|
||||
->withHeader('Location', $document['data']['links']['self']);
|
||||
});
|
||||
}
|
||||
|
||||
final protected function fillDefaultValues(Context $context, array &$data): void
|
||||
{
|
||||
foreach ($context->fields($context->resource) as $field) {
|
||||
if (! has_value($data, $field) && ($default = $field->default)) {
|
||||
set_value($data, $field, $default($context->withField($field)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,12 +9,59 @@
|
||||
|
||||
namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Nyholm\Psr7\Response;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Resource\Deletable;
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
||||
|
||||
class Delete extends BaseDelete implements EndpointInterface
|
||||
use function Tobyz\JsonApiServer\json_api_response;
|
||||
|
||||
class Delete extends Endpoint
|
||||
{
|
||||
use HasMeta;
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
|
||||
public static function make(?string $name = null): static
|
||||
{
|
||||
return parent::make($name ?? 'delete');
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->route('DELETE', '/{id}')
|
||||
->action(function (Context $context) {
|
||||
$model = $context->model;
|
||||
|
||||
/** @var AbstractResource $resource */
|
||||
$resource = $context->resource($context->collection->resource($model, $context));
|
||||
|
||||
$context = $context->withResource($resource);
|
||||
|
||||
if (! $resource instanceof Deletable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($resource), Deletable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$this->callBeforeHook($context);
|
||||
|
||||
$resource->deleteAction($model, $context);
|
||||
|
||||
$this->callAfterHook($context, $model);
|
||||
|
||||
return null;
|
||||
})
|
||||
->response(function (Context $context) {
|
||||
if ($meta = $this->serializeMeta($context)) {
|
||||
return json_api_response(['meta' => $meta]);
|
||||
}
|
||||
|
||||
return new Response(204);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -9,14 +9,145 @@
|
||||
|
||||
namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Closure;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Endpoint as BaseEndpoint;
|
||||
use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
|
||||
use Flarum\Api\Endpoint\Concerns\ShowsResources;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
||||
|
||||
class Endpoint extends BaseEndpoint implements EndpointInterface
|
||||
use function Tobyz\JsonApiServer\json_api_response;
|
||||
|
||||
class Endpoint implements \Tobyz\JsonApiServer\Endpoint\Endpoint
|
||||
{
|
||||
use ShowsResources;
|
||||
use FindsResources;
|
||||
use HasEagerLoading;
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
use ExtractsListingParams;
|
||||
|
||||
public string $method;
|
||||
public string $path;
|
||||
|
||||
protected ?Closure $action = null;
|
||||
protected ?Closure $response = null;
|
||||
protected array $beforeSerialization = [];
|
||||
|
||||
public function __construct(
|
||||
public string $name
|
||||
) {
|
||||
}
|
||||
|
||||
public static function make(?string $name): static
|
||||
{
|
||||
$endpoint = new static($name);
|
||||
|
||||
$endpoint->setUp();
|
||||
|
||||
return $endpoint;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function name(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function action(Closure $action): static
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function response(Closure $response): static
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function route(string $method, string $path): static
|
||||
{
|
||||
$this->method = $method;
|
||||
$this->path = '/'.ltrim(rtrim($path, '/'), '/');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function beforeSerialization(Closure $callback): static
|
||||
{
|
||||
$this->beforeSerialization[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function process(Context $context): mixed
|
||||
{
|
||||
if (! $this->action) {
|
||||
throw new RuntimeException('No action defined for endpoint ['.static::class.']');
|
||||
}
|
||||
|
||||
return ($this->action)($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
*/
|
||||
public function handle(\Tobyz\JsonApiServer\Context $context): ?Response
|
||||
{
|
||||
if (! isset($this->method, $this->path)) {
|
||||
throw new RuntimeException('No route defined for endpoint ['.static::class.']');
|
||||
}
|
||||
|
||||
if (strtolower($context->method()) !== strtolower($this->method)) {
|
||||
throw new MethodNotAllowedException();
|
||||
}
|
||||
|
||||
/** @var AbstractResource $collection */
|
||||
$collection = $context->collection;
|
||||
|
||||
$context = $context->withModelId(
|
||||
$collection->id($context)
|
||||
);
|
||||
|
||||
if ($context->modelId) {
|
||||
$context = $context->withModel(
|
||||
$this->findResource($context, $context->modelId)
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->isVisible($context)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$data = $this->process($context);
|
||||
|
||||
foreach ($this->beforeSerialization as $callback) {
|
||||
$callback($context, $data);
|
||||
}
|
||||
|
||||
if ($this->response) {
|
||||
return ($this->response)($context, $data);
|
||||
}
|
||||
|
||||
if ($context->model && $data instanceof $context->model) {
|
||||
return json_api_response($this->showResource($context, $data));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -9,28 +9,66 @@
|
||||
|
||||
namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Closure;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Flarum\Api\Endpoint\Concerns\IncludesData;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Flarum\Api\Resource\Contracts\Countable;
|
||||
use Flarum\Api\Resource\Contracts\Listable;
|
||||
use Flarum\Api\Serializer;
|
||||
use Flarum\Database\Eloquent\Collection;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Tobyz\JsonApiServer\Endpoint\Index as BaseIndex;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\Sourceable;
|
||||
use Tobyz\JsonApiServer\Pagination\OffsetPagination;
|
||||
use Tobyz\JsonApiServer\Pagination\Pagination;
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
||||
|
||||
class Index extends BaseIndex implements EndpointInterface
|
||||
use function Tobyz\JsonApiServer\apply_filters;
|
||||
use function Tobyz\JsonApiServer\json_api_response;
|
||||
use function Tobyz\JsonApiServer\parse_sort_string;
|
||||
|
||||
class Index extends Endpoint
|
||||
{
|
||||
use HasMeta;
|
||||
use IncludesData;
|
||||
use HasAuthorization;
|
||||
use ExtractsListingParams;
|
||||
use HasCustomHooks;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
public Closure $paginationResolver;
|
||||
public ?string $defaultSort = null;
|
||||
protected ?Closure $query = null;
|
||||
|
||||
$this
|
||||
public function __construct(string $name)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->paginationResolver = fn () => null;
|
||||
}
|
||||
|
||||
public static function make(?string $name = null): static
|
||||
{
|
||||
return parent::make($name ?? 'index');
|
||||
}
|
||||
|
||||
public function query(?Closure $query): static
|
||||
{
|
||||
$this->query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->route('GET', '/')
|
||||
->query(function ($query, ?Pagination $pagination, Context $context): Context {
|
||||
// This model has a searcher API, so we'll use that instead of the default.
|
||||
// The searcher API allows swapping the default search engine for a custom one.
|
||||
@@ -63,13 +101,163 @@ class Index extends BaseIndex implements EndpointInterface
|
||||
$this->applySorts($query, $context);
|
||||
$this->applyFilters($query, $context);
|
||||
|
||||
$pagination?->apply($query);
|
||||
if ($pagination && method_exists($pagination, 'apply')) {
|
||||
$pagination->apply($query);
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
})
|
||||
->action(function (\Tobyz\JsonApiServer\Context $context) {
|
||||
if (str_contains($context->path(), '/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $context->collection;
|
||||
|
||||
if (! $collection instanceof Listable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($collection), Listable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$this->callBeforeHook($context);
|
||||
|
||||
$query = $collection->query($context);
|
||||
|
||||
$pagination = ($this->paginationResolver)($context);
|
||||
|
||||
if ($this->query) {
|
||||
$context = ($this->query)($query, $pagination, $context);
|
||||
|
||||
if (! $context instanceof Context) {
|
||||
throw new RuntimeException('The Index endpoint query closure must return a Context instance.');
|
||||
}
|
||||
} else {
|
||||
/** @var Context $context */
|
||||
$context = $context->withQuery($query);
|
||||
|
||||
$this->applySorts($query, $context);
|
||||
$this->applyFilters($query, $context);
|
||||
|
||||
if ($pagination) {
|
||||
$pagination->apply($query);
|
||||
}
|
||||
}
|
||||
|
||||
$meta = $this->serializeMeta($context);
|
||||
|
||||
if (
|
||||
$collection instanceof Countable &&
|
||||
! is_null($total = $collection->count($query, $context))
|
||||
) {
|
||||
$meta['page']['total'] = $total;
|
||||
}
|
||||
|
||||
$models = $collection->results($query, $context);
|
||||
|
||||
$models = $this->callAfterHook($context, $models);
|
||||
|
||||
$total ??= null;
|
||||
|
||||
return compact('models', 'meta', 'pagination', 'total');
|
||||
})
|
||||
->beforeSerialization(function (Context $context, array $results) {
|
||||
// @phpstan-ignore-next-line
|
||||
$this->loadRelations(Collection::make($results['models']), $context, $this->getInclude($context));
|
||||
})
|
||||
->response(function (Context $context, array $results): Response {
|
||||
$collection = $context->collection;
|
||||
|
||||
['models' => $models, 'meta' => $meta, 'pagination' => $pagination, 'total' => $total] = $results;
|
||||
|
||||
$serializer = new Serializer($context);
|
||||
|
||||
$include = $this->getInclude($context);
|
||||
|
||||
foreach ($models as $model) {
|
||||
$serializer->addPrimary(
|
||||
$context->resource($collection->resource($model, $context)),
|
||||
$model,
|
||||
$include,
|
||||
);
|
||||
}
|
||||
|
||||
[$data, $included] = $serializer->serialize();
|
||||
|
||||
$links = [];
|
||||
|
||||
if ($pagination) {
|
||||
$meta['page'] = array_merge($meta['page'] ?? [], $pagination->meta());
|
||||
$links = array_merge($links, $pagination->links(count($data), $total));
|
||||
}
|
||||
|
||||
return json_api_response(compact('data', 'included', 'meta', 'links'));
|
||||
});
|
||||
}
|
||||
|
||||
public function defaultSort(?string $defaultSort): static
|
||||
{
|
||||
$this->defaultSort = $defaultSort;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
final protected function applySorts($query, Context $context): void
|
||||
{
|
||||
if (! ($sortString = $context->queryParam('sort', $this->defaultSort))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collection = $context->collection;
|
||||
|
||||
if (! $collection instanceof AbstractResource) {
|
||||
throw new RuntimeException('The collection '.$collection::class.' must extend '.AbstractResource::class);
|
||||
}
|
||||
|
||||
$sorts = $collection->resolveSorts();
|
||||
|
||||
foreach (parse_sort_string($sortString) as [$name, $direction]) {
|
||||
foreach ($sorts as $field) {
|
||||
if ($field->name === $name && $field->isVisible($context)) {
|
||||
$field->apply($query, $direction, $context);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
throw (new BadRequestException("Invalid sort: $name"))->setSource([
|
||||
'parameter' => 'sort',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
final protected function applyFilters($query, Context $context): void
|
||||
{
|
||||
if (! ($filters = $context->queryParam('filter'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_array($filters)) {
|
||||
throw (new BadRequestException('filter must be an array'))->setSource([
|
||||
'parameter' => 'filter',
|
||||
]);
|
||||
}
|
||||
|
||||
$collection = $context->collection;
|
||||
|
||||
if (! $collection instanceof \Tobyz\JsonApiServer\Resource\Listable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', $collection::class, \Tobyz\JsonApiServer\Resource\Listable::class),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
apply_filters($query, $filters, $collection, $context);
|
||||
} catch (Sourceable $e) {
|
||||
throw $e->prependSource(['parameter' => 'filter']);
|
||||
}
|
||||
}
|
||||
|
||||
public function paginate(int $defaultLimit = 20, int $maxLimit = 50): static
|
||||
{
|
||||
$this->limit = $defaultLimit;
|
||||
|
@@ -9,19 +9,37 @@
|
||||
|
||||
namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Show as BaseShow;
|
||||
use Flarum\Api\Endpoint\Concerns\IncludesData;
|
||||
use Flarum\Api\Endpoint\Concerns\ShowsResources;
|
||||
use Flarum\Database\Eloquent\Collection;
|
||||
|
||||
class Show extends BaseShow implements EndpointInterface
|
||||
class Show extends Endpoint
|
||||
{
|
||||
use ShowsResources;
|
||||
use IncludesData;
|
||||
use HasAuthorization;
|
||||
use ExtractsListingParams;
|
||||
use HasCustomHooks;
|
||||
|
||||
public static function make(?string $name = null): static
|
||||
{
|
||||
return parent::make($name ?? 'show');
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->route('GET', '/{id}')
|
||||
->action(function (Context $context): ?object {
|
||||
$this->callBeforeHook($context);
|
||||
|
||||
return $this->callAfterHook($context, $context->model);
|
||||
})
|
||||
->beforeSerialization(function (Context $context, object $model) {
|
||||
$this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -9,17 +9,64 @@
|
||||
|
||||
namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Update as BaseUpdate;
|
||||
use Flarum\Api\Endpoint\Concerns\IncludesData;
|
||||
use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData;
|
||||
use Flarum\Api\Endpoint\Concerns\ShowsResources;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Flarum\Database\Eloquent\Collection;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Resource\Updatable;
|
||||
|
||||
class Update extends BaseUpdate implements EndpointInterface
|
||||
class Update extends Endpoint
|
||||
{
|
||||
use SavesAndValidatesData;
|
||||
use ShowsResources;
|
||||
use IncludesData;
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
|
||||
public static function make(?string $name = null): static
|
||||
{
|
||||
return parent::make($name ?? 'update');
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->route('PATCH', '/{id}')
|
||||
->action(function (Context $context): object {
|
||||
$model = $context->model;
|
||||
|
||||
/** @var AbstractResource $resource */
|
||||
$resource = $context->resource($context->collection->resource($model, $context));
|
||||
|
||||
$context = $context->withResource($resource);
|
||||
|
||||
if (! $resource instanceof Updatable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($resource), Updatable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$this->callBeforeHook($context);
|
||||
|
||||
$data = $this->parseData($context);
|
||||
|
||||
$this->assertFieldsValid($context, $data);
|
||||
$this->deserializeValues($context, $data);
|
||||
$this->assertDataValid($context, $data);
|
||||
$this->setValues($context, $data);
|
||||
|
||||
$context = $context->withModel($model = $resource->updateAction($model, $context));
|
||||
|
||||
$this->saveFields($context, $data);
|
||||
|
||||
return $this->callAfterHook($context, $model);
|
||||
})
|
||||
->beforeSerialization(function (Context $context, object $model) {
|
||||
$this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -9,24 +9,26 @@
|
||||
|
||||
namespace Flarum\Api;
|
||||
|
||||
use Flarum\Api\Endpoint\EndpointInterface;
|
||||
use Flarum\Api\Endpoint\Endpoint;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Resource\AbstractResource;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use Laminas\Diactoros\Uri;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Tobyz\JsonApiServer\Endpoint\Endpoint;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
||||
use Tobyz\JsonApiServer\JsonApi as BaseJsonApi;
|
||||
use Tobyz\JsonApiServer\Resource\Collection;
|
||||
use Tobyz\JsonApiServer\Resource\Resource;
|
||||
|
||||
class JsonApi extends BaseJsonApi
|
||||
{
|
||||
protected ?string $resourceClass = null;
|
||||
protected ?string $endpointName = null;
|
||||
protected string $resourceClass;
|
||||
protected string $endpointName;
|
||||
protected ?Request $baseRequest = null;
|
||||
protected ?Container $container = null;
|
||||
|
||||
@@ -57,9 +59,13 @@ class JsonApi extends BaseJsonApi
|
||||
->withEndpoint($this->findEndpoint($collection));
|
||||
}
|
||||
|
||||
protected function findEndpoint(?Collection $collection): Endpoint&EndpointInterface
|
||||
protected function findEndpoint(?Collection $collection): Endpoint
|
||||
{
|
||||
/** @var Endpoint&EndpointInterface $endpoint */
|
||||
if (! $collection instanceof AbstractResource) {
|
||||
throw new RuntimeException('Resource '.$collection::class.' must extend '.AbstractResource::class);
|
||||
}
|
||||
|
||||
/** @var Endpoint $endpoint */
|
||||
foreach ($collection->resolveEndpoints() as $endpoint) {
|
||||
if ($endpoint->name === $this->endpointName) {
|
||||
return $endpoint;
|
||||
@@ -69,6 +75,46 @@ class JsonApi extends BaseJsonApi
|
||||
throw new BadRequestException('Invalid endpoint specified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection by name or class.
|
||||
*
|
||||
* @throws ResourceNotFoundException if the collection has not been defined.
|
||||
*/
|
||||
public function getCollection(string $type): Collection
|
||||
{
|
||||
if (isset($this->collections[$type])) {
|
||||
return $this->collections[$type];
|
||||
}
|
||||
|
||||
foreach ($this->collections as $instance) {
|
||||
if ($instance instanceof $type) {
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a resource by type or class.
|
||||
*
|
||||
* @throws ResourceNotFoundException if the resource has not been defined.
|
||||
*/
|
||||
public function getResource(string $type): Resource
|
||||
{
|
||||
if (isset($this->resources[$type])) {
|
||||
return $this->resources[$type];
|
||||
}
|
||||
|
||||
foreach ($this->resources as $instance) {
|
||||
if ($instance instanceof $type) {
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException($type);
|
||||
}
|
||||
|
||||
public function withRequest(Request $request): self
|
||||
{
|
||||
$this->baseRequest = $request;
|
||||
@@ -112,13 +158,19 @@ class JsonApi extends BaseJsonApi
|
||||
$context = $context->withInternal($key, $value);
|
||||
}
|
||||
|
||||
$endpoint = $context->endpoint;
|
||||
|
||||
if (! $endpoint instanceof Endpoint) {
|
||||
throw new RuntimeException('The endpoint '.$endpoint::class.' must extend '.Endpoint::class);
|
||||
}
|
||||
|
||||
$context = $context->withRequest(
|
||||
$request
|
||||
->withMethod($context->endpoint->method)
|
||||
->withUri(new Uri($context->endpoint->path))
|
||||
->withMethod($endpoint->method)
|
||||
->withUri(new Uri($endpoint->path))
|
||||
);
|
||||
|
||||
return $context->endpoint->process($context);
|
||||
return $endpoint->process($context);
|
||||
}
|
||||
|
||||
public function validateQueryParameters(Request $request): void
|
||||
|
@@ -10,37 +10,43 @@
|
||||
namespace Flarum\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context as FlarumContext;
|
||||
use Flarum\Api\Resource\Concerns\Bootable;
|
||||
use Flarum\Api\Resource\Concerns\Extendable;
|
||||
use Flarum\Api\Resource\Concerns\HasSortMap;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\User;
|
||||
use Flarum\Api\Schema\Contracts\RelationAggregator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Laravel\EloquentResource as BaseResource;
|
||||
use Tobyz\JsonApiServer\Pagination\OffsetPagination;
|
||||
use Tobyz\JsonApiServer\Schema\Field\Attribute;
|
||||
use Tobyz\JsonApiServer\Schema\Field\Field;
|
||||
use Tobyz\JsonApiServer\Schema\Field\Relationship;
|
||||
use Tobyz\JsonApiServer\Schema\Field\ToMany;
|
||||
use Tobyz\JsonApiServer\Schema\Type\DateTime;
|
||||
|
||||
/**
|
||||
* @template M of Model
|
||||
* @extends BaseResource<M, FlarumContext>
|
||||
* @extends AbstractResource<M>
|
||||
*/
|
||||
abstract class AbstractDatabaseResource extends BaseResource
|
||||
abstract class AbstractDatabaseResource extends AbstractResource implements
|
||||
Contracts\Findable,
|
||||
Contracts\Listable,
|
||||
Contracts\Countable,
|
||||
Contracts\Paginatable,
|
||||
Contracts\Creatable,
|
||||
Contracts\Updatable,
|
||||
Contracts\Deletable
|
||||
{
|
||||
use Bootable;
|
||||
use Extendable;
|
||||
use HasSortMap;
|
||||
use DispatchEventsTrait {
|
||||
dispatchEventsFor as traitDispatchEventsFor;
|
||||
}
|
||||
|
||||
abstract public function model(): string;
|
||||
|
||||
/** @inheritDoc */
|
||||
public function newModel(Context $context): object
|
||||
{
|
||||
return new ($this->model());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function resource(object $model, Context $context): ?string
|
||||
{
|
||||
$baseModel = $this->model();
|
||||
@@ -52,134 +58,128 @@ abstract class AbstractDatabaseResource extends BaseResource
|
||||
return null;
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
|
||||
}
|
||||
|
||||
public function createAction(object $model, Context $context): object
|
||||
{
|
||||
$model = parent::createAction($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function updateAction(object $model, Context $context): object
|
||||
{
|
||||
$model = parent::updateAction($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function deleteAction(object $model, Context $context): void
|
||||
{
|
||||
$this->deleting($model, $context);
|
||||
|
||||
$this->delete($model, $context);
|
||||
|
||||
$this->deleted($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function creating(object $model, Context $context): ?object
|
||||
public function getId(object $model, Context $context): string
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function updating(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function saving(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function saved(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function created(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function updated(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
return $model->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleting(object $model, Context $context): void
|
||||
public function getValue(object $model, Field $field, Context $context): mixed
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleted(object $model, Context $context): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function dispatchEventsFor(mixed $entity, User $actor = null): void
|
||||
{
|
||||
if (method_exists($entity, 'releaseEvents')) {
|
||||
$this->traitDispatchEventsFor($entity, $actor);
|
||||
if ($field instanceof Relationship) {
|
||||
return $this->getRelationshipValue($model, $field, $context);
|
||||
} else {
|
||||
return $this->getAttributeValue($model, $field, $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function mutateDataBeforeValidation(Context $context, array $data): array
|
||||
protected function getAttributeValue(Model $model, Field $field, Context $context): mixed
|
||||
{
|
||||
return $data;
|
||||
if ($field instanceof RelationAggregator && ($aggregate = $field->getRelationAggregate())) {
|
||||
$relationName = $aggregate['relation'];
|
||||
|
||||
if (! $model->isRelation($relationName)) {
|
||||
return $model->getAttribute($this->property($field));
|
||||
}
|
||||
|
||||
/** @var Relationship|null $relationship */
|
||||
$relationship = collect($context->fields($this))->first(fn ($f) => $f->name === $relationName);
|
||||
|
||||
if (! $relationship) {
|
||||
throw new InvalidArgumentException("To use relation aggregates, the relationship field must be part of the resource. Missing field: $relationName for attribute $field->name.");
|
||||
}
|
||||
|
||||
EloquentBuffer::add($model, $relationName, $aggregate);
|
||||
|
||||
return function () use ($model, $relationName, $relationship, $field, $context, $aggregate) {
|
||||
EloquentBuffer::load($model, $relationName, $relationship, $context, $aggregate);
|
||||
|
||||
return $model->getAttribute($this->property($field));
|
||||
};
|
||||
}
|
||||
|
||||
return $model->getAttribute($this->property($field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
protected function getRelationshipValue(Model $model, Relationship $field, Context $context): mixed
|
||||
{
|
||||
$method = $this->method($field);
|
||||
|
||||
if ($model->isRelation($method)) {
|
||||
$relation = $model->$method();
|
||||
|
||||
// If this is a belongs-to relationship, and we only need to get the ID
|
||||
// for linkage, then we don't have to actually load the relation because
|
||||
// the ID is stored in a column directly on the model. We will mock up a
|
||||
// related model with the value of the ID filled.
|
||||
if ($relation instanceof BelongsTo && $context->include === null) {
|
||||
if ($key = $model->getAttribute($relation->getForeignKeyName())) {
|
||||
if ($relation instanceof MorphTo) {
|
||||
$morphType = $model->{$relation->getMorphType()};
|
||||
$related = $relation->createModelByType($morphType);
|
||||
} else {
|
||||
$related = $relation->getRelated();
|
||||
}
|
||||
|
||||
return $related->newInstance()->forceFill([$related->getKeyName() => $key]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
EloquentBuffer::add($model, $method);
|
||||
|
||||
return function () use ($model, $method, $field, $context) {
|
||||
EloquentBuffer::load($model, $method, $field, $context);
|
||||
|
||||
$data = $model->getRelation($method);
|
||||
|
||||
return $data instanceof Collection ? $data->all() : $data;
|
||||
};
|
||||
}
|
||||
|
||||
return $this->getAttributeValue($model, $field, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function query(Context $context): object
|
||||
{
|
||||
$query = $this->newModel($context)->query();
|
||||
|
||||
$this->scope($query, $context);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to scope a query for this resource.
|
||||
*
|
||||
* @param Builder<M> $query
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function scope(Builder $query, Context $context): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<M> $query
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function results(object $query, Context $context): iterable
|
||||
@@ -192,6 +192,15 @@ abstract class AbstractDatabaseResource extends BaseResource
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<M> $query
|
||||
*/
|
||||
public function paginate(object $query, OffsetPagination $pagination): void
|
||||
{
|
||||
$query->take($pagination->limit)->skip($pagination->offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<M> $query
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function count(object $query, Context $context): ?int
|
||||
@@ -200,6 +209,142 @@ abstract class AbstractDatabaseResource extends BaseResource
|
||||
return $results->getTotalResults();
|
||||
}
|
||||
|
||||
return parent::count($query, $context);
|
||||
return $query->toBase()->getCountForPagination();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function find(string $id, Context $context): ?object
|
||||
{
|
||||
return $this->query($context)->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setValue(object $model, Field $field, mixed $value, Context $context): void
|
||||
{
|
||||
if ($field instanceof Relationship) {
|
||||
$method = $this->method($field);
|
||||
$relation = $model->$method();
|
||||
|
||||
// If this is a belongs-to relationship, then the ID is stored on the
|
||||
// model itself, so we can set it here.
|
||||
if ($relation instanceof BelongsTo) {
|
||||
$relation->associate($value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Mind-blowingly, Laravel discards timezone information when storing
|
||||
// dates in the database. Since the API can receive dates in any
|
||||
// timezone, we will need to convert it to the app's configured
|
||||
// timezone ourselves before storage.
|
||||
if (
|
||||
$field instanceof Attribute &&
|
||||
$field->type instanceof DateTime &&
|
||||
$value instanceof \DateTimeInterface
|
||||
) {
|
||||
$value = \DateTime::createFromInterface($value)->setTimezone(
|
||||
new \DateTimeZone(config('app.timezone')),
|
||||
);
|
||||
}
|
||||
|
||||
$model->setAttribute($this->property($field), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function saveValue(object $model, Field $field, mixed $value, Context $context): void
|
||||
{
|
||||
if ($field instanceof ToMany) {
|
||||
$method = $this->method($field);
|
||||
$relation = $model->$method();
|
||||
|
||||
if ($relation instanceof BelongsToMany) {
|
||||
$relation->sync(new Collection($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function create(object $model, Context $context): object
|
||||
{
|
||||
$this->saveModel($model, $context);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function update(object $model, Context $context): object
|
||||
{
|
||||
$this->saveModel($model, $context);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
protected function saveModel(Model $model, Context $context): void
|
||||
{
|
||||
$model->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function delete(object $model, Context $context): void
|
||||
{
|
||||
$model->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model property that a field represents.
|
||||
*/
|
||||
protected function property(Field $field): string
|
||||
{
|
||||
return $field->property ?: Str::snake($field->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model method that a field represents.
|
||||
*/
|
||||
protected function method(Field $field): string
|
||||
{
|
||||
return $field->property ?: $field->name;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function newModel(Context $context): object
|
||||
{
|
||||
return new ($this->model());
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function mutateDataBeforeValidation(Context $context, array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@@ -12,16 +12,22 @@ namespace Flarum\Api\Resource;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource\Concerns\Bootable;
|
||||
use Flarum\Api\Resource\Concerns\Extendable;
|
||||
use Flarum\Api\Resource\Concerns\HasHooks;
|
||||
use Flarum\Api\Resource\Concerns\HasSortMap;
|
||||
use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource;
|
||||
|
||||
/**
|
||||
* @template M of object
|
||||
* @extends BaseResource<M, Context>
|
||||
*/
|
||||
abstract class AbstractResource extends BaseResource
|
||||
{
|
||||
use Bootable;
|
||||
use Extendable;
|
||||
use HasSortMap;
|
||||
use HasHooks;
|
||||
|
||||
public function id(Context $context): ?string
|
||||
{
|
||||
return $context->extractIdFromPath($context);
|
||||
}
|
||||
}
|
||||
|
189
framework/core/src/Api/Resource/Concerns/HasHooks.php
Normal file
189
framework/core/src/Api/Resource/Concerns/HasHooks.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?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\Api\Resource\Concerns;
|
||||
|
||||
use Flarum\Api\Context as FlarumContext;
|
||||
use Flarum\Api\Resource\Contracts\Creatable;
|
||||
use Flarum\Api\Resource\Contracts\Deletable;
|
||||
use Flarum\Api\Resource\Contracts\Updatable;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\User;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
/**
|
||||
* @template M of object
|
||||
*/
|
||||
trait HasHooks
|
||||
{
|
||||
use DispatchEventsTrait {
|
||||
dispatchEventsFor as traitDispatchEventsFor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function createAction(object $model, Context $context): object
|
||||
{
|
||||
if (! $this instanceof Creatable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($this), Creatable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$model = $this->creating($model, $context) ?: $model;
|
||||
|
||||
$model = $this->saving($model, $context) ?: $model;
|
||||
|
||||
$model = $this->create($model, $context);
|
||||
|
||||
$model = $this->saved($model, $context) ?: $model;
|
||||
|
||||
$model = $this->created($model, $context) ?: $model;
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function updateAction(object $model, Context $context): object
|
||||
{
|
||||
if (! $this instanceof Updatable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($this), Updatable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$model = $this->updating($model, $context) ?: $model;
|
||||
|
||||
$model = $this->saving($model, $context) ?: $model;
|
||||
|
||||
$this->update($model, $context);
|
||||
|
||||
$model = $this->saved($model, $context) ?: $model;
|
||||
|
||||
$model = $this->updated($model, $context) ?: $model;
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleteAction(object $model, Context $context): void
|
||||
{
|
||||
if (! $this instanceof Deletable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('%s must implement %s', get_class($this), Deletable::class),
|
||||
);
|
||||
}
|
||||
|
||||
$this->deleting($model, $context);
|
||||
|
||||
$this->delete($model, $context);
|
||||
|
||||
$this->deleted($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function creating(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function updating(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function saving(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function saved(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function created(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function updated(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleting(object $model, Context $context): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleted(object $model, Context $context): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function dispatchEventsFor(mixed $entity, User $actor = null): void
|
||||
{
|
||||
if (method_exists($entity, 'releaseEvents')) {
|
||||
$this->traitDispatchEventsFor($entity, $actor);
|
||||
}
|
||||
}
|
||||
}
|
17
framework/core/src/Api/Resource/Contracts/Attachable.php
Normal file
17
framework/core/src/Api/Resource/Contracts/Attachable.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Attachable as AttachableContract;
|
||||
|
||||
interface Attachable extends AttachableContract
|
||||
{
|
||||
//
|
||||
}
|
20
framework/core/src/Api/Resource/Contracts/Countable.php
Normal file
20
framework/core/src/Api/Resource/Contracts/Countable.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
interface Countable extends Listable
|
||||
{
|
||||
/**
|
||||
* Count the models for the given query.
|
||||
*/
|
||||
public function count(object $query, Context $context): ?int;
|
||||
}
|
17
framework/core/src/Api/Resource/Contracts/Creatable.php
Normal file
17
framework/core/src/Api/Resource/Contracts/Creatable.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Creatable as CreatableContract;
|
||||
|
||||
interface Creatable extends CreatableContract
|
||||
{
|
||||
//
|
||||
}
|
17
framework/core/src/Api/Resource/Contracts/Deletable.php
Normal file
17
framework/core/src/Api/Resource/Contracts/Deletable.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Deletable as DeletableContract;
|
||||
|
||||
interface Deletable extends DeletableContract
|
||||
{
|
||||
//
|
||||
}
|
17
framework/core/src/Api/Resource/Contracts/Findable.php
Normal file
17
framework/core/src/Api/Resource/Contracts/Findable.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Findable as FindableContract;
|
||||
|
||||
interface Findable extends FindableContract
|
||||
{
|
||||
//
|
||||
}
|
54
framework/core/src/Api/Resource/Contracts/Listable.php
Normal file
54
framework/core/src/Api/Resource/Contracts/Listable.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Schema\Filter;
|
||||
use Tobyz\JsonApiServer\Schema\Sort;
|
||||
|
||||
/**
|
||||
* @template M of object
|
||||
* @template C of Context
|
||||
*/
|
||||
interface Listable
|
||||
{
|
||||
/**
|
||||
* Create a query object for the current request.
|
||||
*
|
||||
* @param Context $context
|
||||
*/
|
||||
public function query(Context $context): object;
|
||||
|
||||
/**
|
||||
* Get results from the given query.
|
||||
*
|
||||
* @param Context $context
|
||||
*/
|
||||
public function results(object $query, Context $context): iterable;
|
||||
|
||||
/**
|
||||
* Filters that can be applied to the resource list.
|
||||
*
|
||||
* @return Filter[]
|
||||
*/
|
||||
public function filters(): array;
|
||||
|
||||
/**
|
||||
* Sorts that can be applied to the resource list.
|
||||
*
|
||||
* @return Sort[]
|
||||
*/
|
||||
public function sorts(): array;
|
||||
|
||||
/**
|
||||
* Resolve the sorts for this resource.
|
||||
*/
|
||||
public function resolveSorts(): array;
|
||||
}
|
17
framework/core/src/Api/Resource/Contracts/Paginatable.php
Normal file
17
framework/core/src/Api/Resource/Contracts/Paginatable.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Paginatable as PaginatableContract;
|
||||
|
||||
interface Paginatable extends PaginatableContract
|
||||
{
|
||||
//
|
||||
}
|
17
framework/core/src/Api/Resource/Contracts/Updatable.php
Normal file
17
framework/core/src/Api/Resource/Contracts/Updatable.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Updatable as UpdatableContract;
|
||||
|
||||
interface Updatable extends UpdatableContract
|
||||
{
|
||||
//
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user