Skip to content

Commit e9df9be

Browse files
committed
feat: Implement quota rules
Signed-off-by: Lukas Schaefer <[email protected]>
1 parent 454482e commit e9df9be

22 files changed

+1331
-35
lines changed

.eslintrc.cjs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55

66
module.exports = {
77
globals: {
8-
appVersion: true
8+
appVersion: true,
99
},
1010
parserOptions: {
11-
requireConfigFile: false
11+
requireConfigFile: false,
1212
},
1313
extends: [
14-
'@nextcloud'
14+
'@nextcloud',
1515
],
1616
rules: {
1717
'jsdoc/require-jsdoc': 'off',
1818
'jsdoc/tag-lines': 'off',
1919
'vue/first-attribute-linebreak': 'off',
20-
'import/extensions': 'off'
21-
}
20+
'import/extensions': 'off',
21+
'vue/no-v-model-argument': 'off',
22+
},
2223
}

appinfo/routes.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@
1616
['name' => 'openAiAPI#getModels', 'url' => '/models', 'verb' => 'GET'],
1717
['name' => 'openAiAPI#getUserQuotaInfo', 'url' => '/quota-info', 'verb' => 'GET'],
1818
['name' => 'openAiAPI#getAdminQuotaInfo', 'url' => '/admin-quota-info', 'verb' => 'GET'],
19+
20+
['name' => 'quotaRule#addRule', 'url' => '/quota/rule', 'verb' => 'POST'],
21+
['name' => 'quotaRule#updateRule', 'url' => '/quota/rule', 'verb' => 'PUT'],
22+
['name' => 'quotaRule#deleteRule', 'url' => '/quota/rule', 'verb' => 'DELETE'],
1923
],
2024
];

lib/AppInfo/Application.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class Application extends App implements IBootstrap {
7676
];
7777

7878
public const MODELS_CACHE_KEY = 'models';
79+
public const QUOTA_RULES_CACHE_PREFIX = 'quota_rules';
7980
public const MODELS_CACHE_TTL = 60 * 30;
8081

8182
private IAppConfig $appConfig;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\OpenAi\Controller;
9+
10+
use Exception;
11+
use OCA\OpenAi\Service\QuotaRuleService;
12+
use OCP\AppFramework\Controller;
13+
use OCP\AppFramework\Http;
14+
use OCP\AppFramework\Http\DataResponse;
15+
use OCP\IRequest;
16+
17+
class QuotaRuleController extends Controller {
18+
public function __construct(
19+
string $appName,
20+
IRequest $request,
21+
private QuotaRuleService $quotaRuleService,
22+
) {
23+
parent::__construct($appName, $request);
24+
}
25+
26+
/**
27+
* POST /rule Creates a new empty rule returning the value of the rule
28+
* @return DataResponse
29+
*/
30+
public function addRule(): DataResponse {
31+
try {
32+
$result = $this->quotaRuleService->addRule();
33+
return new DataResponse($result);
34+
} catch (Exception $e) {
35+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
36+
}
37+
}
38+
39+
/**
40+
* PUT /rule
41+
* @param int $id
42+
* @param array $rule expects: type, amount, priority, pool, entities[]
43+
* @return DataResponse
44+
*/
45+
public function updateRule(int $id, array $rule): DataResponse {
46+
if (!isset($rule['type']) || !is_int($rule['type'])) {
47+
return new DataResponse(['error' => 'Missing or invalid type'], Http::STATUS_BAD_REQUEST);
48+
}
49+
if (!isset($rule['amount']) || !is_int($rule['amount'])) {
50+
return new DataResponse(['error' => 'Missing or invalid amount'], Http::STATUS_BAD_REQUEST);
51+
}
52+
if (!isset($rule['priority']) || !is_int($rule['priority'])) {
53+
return new DataResponse(['error' => 'Missing or invalid priority'], Http::STATUS_BAD_REQUEST);
54+
}
55+
if (!isset($rule['pool']) || !is_bool($rule['pool'])) {
56+
return new DataResponse(['error' => 'Missing or invalid pool value'], Http::STATUS_BAD_REQUEST);
57+
}
58+
if (!isset($rule['entities']) || !is_array($rule['entities'])) {
59+
return new DataResponse(['error' => 'Missing or invalid entities'], Http::STATUS_BAD_REQUEST);
60+
}
61+
try {
62+
$result = $this->quotaRuleService->updateRule($id, $rule);
63+
return new DataResponse($result);
64+
} catch (Exception $e) {
65+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
66+
}
67+
}
68+
69+
/**
70+
* DELETE /rule
71+
* @param int $id
72+
* @return DataResponse
73+
*/
74+
public function deleteRule(int $id): DataResponse {
75+
try {
76+
$this->quotaRuleService->deleteRule($id);
77+
return new DataResponse('');
78+
} catch (Exception $e) {
79+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
80+
}
81+
}
82+
}

lib/Db/QuotaRule.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\OpenAi\Db;
11+
12+
use JsonSerializable;
13+
use OCP\AppFramework\Db\Entity;
14+
use ReturnTypeWillChange;
15+
16+
/**
17+
* @method int getType()
18+
* @method void setType(int $type)
19+
* @method int getAmount()
20+
* @method void setAmount(int $amount)
21+
* @method int getPriority()
22+
* @method void setPriority(int $priority)
23+
* @method bool getPool()
24+
* @method void setPool(bool $pool)
25+
*/
26+
class QuotaRule extends Entity implements JsonSerializable {
27+
/** @var int */
28+
protected $type;
29+
/** @var int */
30+
protected $amount;
31+
/** @var int */
32+
protected $priority;
33+
/** @var bool */
34+
protected $pool;
35+
36+
public function __construct() {
37+
$this->addType('type', 'integer');
38+
$this->addType('amount', 'integer');
39+
$this->addType('priority', 'integer');
40+
$this->addType('pool', 'boolean');
41+
}
42+
43+
#[ReturnTypeWillChange]
44+
public function jsonSerialize() {
45+
return [
46+
'id' => $this->id,
47+
'type' => $this->type,
48+
'amount' => $this->amount,
49+
'priority' => $this->priority,
50+
'pool' => $this->pool
51+
];
52+
}
53+
}

lib/Db/QuotaRuleMapper.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\OpenAi\Db;
9+
10+
use OCP\AppFramework\Db\DoesNotExistException;
11+
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
12+
use OCP\AppFramework\Db\QBMapper;
13+
use OCP\DB\Exception;
14+
use OCP\DB\QueryBuilder\IQueryBuilder;
15+
use OCP\IDBConnection;
16+
17+
/**
18+
* @extends QBMapper<QuotaRule>
19+
*/
20+
class QuotaRuleMapper extends QBMapper {
21+
public function __construct(
22+
IDBConnection $db,
23+
private QuotaUserMapper $quotaUserMapper,
24+
) {
25+
parent::__construct($db, 'openai_quota_rule', QuotaRule::class);
26+
}
27+
28+
/**
29+
* @return array
30+
* @throws Exception
31+
*/
32+
public function getRules(): array {
33+
$qb = $this->db->getQueryBuilder();
34+
35+
$qb->select('*')
36+
->from($this->getTableName());
37+
38+
return $this->findEntities($qb);
39+
}
40+
41+
/**
42+
* @param int $quotaType
43+
* @param string $userId
44+
* @param array $groups
45+
* @return QuotaRule
46+
* @throws DoesNotExistException
47+
* @throws Exception
48+
* @throws MultipleObjectsReturnedException
49+
*/
50+
public function getRule(int $quotaType, string $userId, array $groups): QuotaRule {
51+
$qb = $this->db->getQueryBuilder();
52+
53+
$qb->select('r.*')
54+
->from($this->getTableName(), 'r')
55+
->leftJoin('r', 'openai_quota_user', 'u', 'r.id = u.rule_id')
56+
->where(
57+
$qb->expr()->eq('type', $qb->createNamedParameter($quotaType, IQueryBuilder::PARAM_INT))
58+
)->andWhere(
59+
$qb->expr()->orX(
60+
$qb->expr()->andX(
61+
$qb->expr()->eq('u.entity_type', $qb->createNamedParameter('user', IQueryBuilder::PARAM_STR)),
62+
$qb->expr()->eq('u.entity_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
63+
),
64+
$qb->expr()->andX(
65+
$qb->expr()->eq('u.entity_type', $qb->createNamedParameter('group', IQueryBuilder::PARAM_STR)),
66+
$qb->expr()->in('u.entity_id', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))
67+
),
68+
69+
)
70+
)->orderBy('r.priority', 'ASC')
71+
->setMaxResults(1);
72+
/** @var QuotaRule $entity */
73+
$entity = $this->findEntity($qb);
74+
return $entity;
75+
}
76+
/**
77+
* @param int $quotaType
78+
* @param int $amount
79+
* @param int $priority
80+
* @param bool $pool
81+
* @return int
82+
* @throws Exception
83+
*/
84+
public function addRule(int $quotaType, int $amount, int $priority, bool $pool): int {
85+
$qb = $this->db->getQueryBuilder();
86+
87+
$qb->insert($this->getTableName())
88+
->values(
89+
[
90+
'type' => $qb->createNamedParameter($quotaType, IQueryBuilder::PARAM_INT),
91+
'amount' => $qb->createNamedParameter($amount, IQueryBuilder::PARAM_INT),
92+
'priority' => $qb->createNamedParameter($priority, IQueryBuilder::PARAM_INT),
93+
'pool' => $qb->createNamedParameter($pool, IQueryBuilder::PARAM_BOOL)
94+
]
95+
);
96+
$qb->executeStatement();
97+
return $qb->getLastInsertId();
98+
}
99+
/**
100+
* @param int $id
101+
* @param int $quotaType
102+
* @param int $amount
103+
* @param int $priority
104+
* @param bool $pool
105+
* @return void
106+
* @throws Exception
107+
*/
108+
public function updateRule(int $id, int $quotaType, int $amount, int $priority, bool $pool): void {
109+
$qb = $this->db->getQueryBuilder();
110+
111+
$qb->update($this->getTableName())
112+
->set('type', $qb->createNamedParameter($quotaType, IQueryBuilder::PARAM_INT))
113+
->set('amount', $qb->createNamedParameter($amount, IQueryBuilder::PARAM_INT))
114+
->set('priority', $qb->createNamedParameter($priority, IQueryBuilder::PARAM_INT))
115+
->set('pool', $qb->createNamedParameter($pool, IQueryBuilder::PARAM_BOOL))
116+
->where(
117+
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
118+
);
119+
$qb->executeStatement();
120+
}
121+
/**
122+
* @param int $id
123+
* @throws Exception
124+
*/
125+
public function deleteRule(int $id): void {
126+
$qb = $this->db->getQueryBuilder();
127+
128+
$qb->delete($this->getTableName())
129+
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
130+
$qb->executeStatement();
131+
}
132+
}

lib/Db/QuotaUsage.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
namespace OCA\OpenAi\Db;
1111

12+
use JsonSerializable;
1213
use OCP\AppFramework\Db\Entity;
14+
use ReturnTypeWillChange;
1315

1416
/**
1517
* @method string getUserId()
@@ -20,8 +22,10 @@
2022
* @method void setUnits(int $units)
2123
* @method int getTimestamp()
2224
* @method void setTimestamp(int $timestamp)
25+
* @method int getPool()
26+
* @method void setPool(int $pool)
2327
*/
24-
class QuotaUsage extends Entity implements \JsonSerializable {
28+
class QuotaUsage extends Entity implements JsonSerializable {
2529
/** @var string */
2630
protected $userId;
2731
/** @var int */
@@ -30,22 +34,26 @@ class QuotaUsage extends Entity implements \JsonSerializable {
3034
protected $units;
3135
/** @var int */
3236
protected $timestamp;
37+
/** @var int */
38+
protected $pool;
3339

3440
public function __construct() {
3541
$this->addType('user_id', 'string');
3642
$this->addType('type', 'integer');
3743
$this->addType('units', 'integer');
3844
$this->addType('timestamp', 'integer');
45+
$this->addType('pool', 'integer');
3946
}
4047

41-
#[\ReturnTypeWillChange]
48+
#[ReturnTypeWillChange]
4249
public function jsonSerialize() {
4350
return [
4451
'id' => $this->id,
4552
'user_id' => $this->userId,
4653
'type' => $this->type,
4754
'units' => $this->units,
4855
'timestamp' => $this->timestamp,
56+
'pool' => $this->pool
4957
];
5058
}
5159
}

0 commit comments

Comments
 (0)