mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.8.x' into feat-apps-module-dl
This commit is contained in:
@@ -273,6 +273,9 @@ class Oidc extends OAuth2
|
||||
{
|
||||
if (empty($this->wellKnownConfiguration)) {
|
||||
$response = $this->request('GET', $this->getWellKnownEndpoint());
|
||||
if (empty($response)) {
|
||||
throw new Exception('Invalid well-known configuration');
|
||||
}
|
||||
$this->wellKnownConfiguration = \json_decode($response, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Databases;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception;
|
||||
use Utopia\Database\Exception\Timeout;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
||||
/**
|
||||
* Service for managing transaction state and providing transaction-aware document operations
|
||||
*
|
||||
* This class provides methods to:
|
||||
* - Query documents with transaction awareness (getDocument, listDocuments, countDocuments)
|
||||
* - Apply bulk operations to transaction state for cross-operation visibility
|
||||
* - Replay transaction operations to build current state
|
||||
*/
|
||||
class TransactionState
|
||||
{
|
||||
private Database $dbForProject;
|
||||
|
||||
public function __construct(Database $dbForProject)
|
||||
{
|
||||
$this->dbForProject = $dbForProject;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a document with transaction-aware logic
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string $documentId Document ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @param array $queries Optional query filters
|
||||
* @return Document
|
||||
* @throws Exception
|
||||
* @throws Exception\Query
|
||||
* @throws Timeout
|
||||
*/
|
||||
public function getDocument(
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
?string $transactionId = null,
|
||||
array $queries = []
|
||||
): Document {
|
||||
if ($transactionId === null) {
|
||||
return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
|
||||
}
|
||||
|
||||
$state = $this->getTransactionState($transactionId);
|
||||
|
||||
if (isset($state[$collectionId][$documentId])) {
|
||||
$docState = $state[$collectionId][$documentId];
|
||||
|
||||
if (!$docState['exists']) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
if ($docState['action'] === 'create') {
|
||||
return $this->applyProjection($docState['document'], $queries);
|
||||
}
|
||||
|
||||
if ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
|
||||
// Merge with committed version
|
||||
$committedDoc = $this->dbForProject->getDocument($collectionId, $documentId, $queries);
|
||||
if (!$committedDoc->isEmpty()) {
|
||||
foreach ($docState['document']->getAttributes() as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$committedDoc->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
// Reapply projection in case transaction added new fields
|
||||
return $this->applyProjection($committedDoc, $queries);
|
||||
} elseif ($docState['action'] === 'upsert') {
|
||||
return $this->applyProjection($docState['document'], $queries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* List documents with transaction-aware logic
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @param array $queries Optional query filters
|
||||
* @return array Array of Document objects
|
||||
* @throws Exception
|
||||
* @throws Exception\Query
|
||||
* @throws Timeout
|
||||
*/
|
||||
public function listDocuments(
|
||||
string $collectionId,
|
||||
?string $transactionId = null,
|
||||
array $queries = []
|
||||
): array {
|
||||
// If no transaction, use normal database retrieval
|
||||
if ($transactionId === null) {
|
||||
return $this->dbForProject->find($collectionId, $queries);
|
||||
}
|
||||
|
||||
$state = $this->getTransactionState($transactionId);
|
||||
$committedDocs = $this->dbForProject->find($collectionId, $queries);
|
||||
$documentMap = [];
|
||||
|
||||
// Build map of committed documents
|
||||
foreach ($committedDocs as $doc) {
|
||||
$documentMap[$doc->getId()] = $doc;
|
||||
}
|
||||
|
||||
// Apply transaction state changes
|
||||
if (isset($state[$collectionId])) {
|
||||
foreach ($state[$collectionId] as $docId => $docState) {
|
||||
if (!$docState['exists']) {
|
||||
// Document was deleted, remove from results
|
||||
unset($documentMap[$docId]);
|
||||
} elseif ($docState['action'] === 'create') {
|
||||
// Document was created, add to results with projection
|
||||
$documentMap[$docId] = $this->applyProjection($docState['document'], $queries);
|
||||
} elseif ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
|
||||
if (isset($documentMap[$docId])) {
|
||||
// Update existing document
|
||||
foreach ($docState['document']->getAttributes() as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$documentMap[$docId]->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
// Reapply projection in case transaction added new fields
|
||||
$documentMap[$docId] = $this->applyProjection($documentMap[$docId], $queries);
|
||||
} elseif ($docState['action'] === 'upsert') {
|
||||
// Upsert created a new document, apply projection
|
||||
$documentMap[$docId] = $this->applyProjection($docState['document'], $queries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($documentMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count documents with transaction-aware logic
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @param array $queries Optional query filters
|
||||
* @return int Document count
|
||||
* @throws Exception
|
||||
* @throws Exception\Query
|
||||
* @throws Timeout
|
||||
*/
|
||||
public function countDocuments(
|
||||
string $collectionId,
|
||||
?string $transactionId = null,
|
||||
array $queries = []
|
||||
): int {
|
||||
if ($transactionId === null) {
|
||||
return $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||
}
|
||||
|
||||
$state = $this->getTransactionState($transactionId);
|
||||
|
||||
$baseCount = $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||
|
||||
if (!isset($state[$collectionId])) {
|
||||
return $baseCount;
|
||||
}
|
||||
|
||||
$committedDocs = $this->dbForProject->find($collectionId, $queries);
|
||||
$committedDocIds = [];
|
||||
foreach ($committedDocs as $doc) {
|
||||
$committedDocIds[$doc->getId()] = true;
|
||||
}
|
||||
|
||||
$adjustedCount = $baseCount;
|
||||
|
||||
$filters = $this->extractFilters($queries);
|
||||
|
||||
foreach ($state[$collectionId] as $docId => $docState) {
|
||||
if (!$docState['exists']) {
|
||||
if (isset($committedDocIds[$docId])) {
|
||||
$adjustedCount--;
|
||||
}
|
||||
} elseif ($docState['action'] === 'create') {
|
||||
if ($this->documentMatchesFilters($docState['document'], $filters)) {
|
||||
$adjustedCount++;
|
||||
}
|
||||
} elseif ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
|
||||
$wasInResults = isset($committedDocIds[$docId]);
|
||||
$nowMatches = $this->documentMatchesFilters($docState['document'], $filters);
|
||||
|
||||
if (!$wasInResults && $nowMatches && $docState['action'] === 'upsert') {
|
||||
$adjustedCount++;
|
||||
} elseif ($wasInResults && !$nowMatches) {
|
||||
$adjustedCount--;
|
||||
} elseif (!$wasInResults && $nowMatches) {
|
||||
// Update shouldn't add a new doc, but upsert might have
|
||||
if ($docState['action'] === 'upsert') {
|
||||
$adjustedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max(0, $adjustedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a document exists with transaction-aware logic
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string $documentId Document ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @return bool True if document exists
|
||||
*/
|
||||
public function documentExists(
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
?string $transactionId = null
|
||||
): bool {
|
||||
$doc = $this->getDocument($collectionId, $documentId, $transactionId);
|
||||
return !$doc->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply bulk update to documents in transaction state that match queries
|
||||
*
|
||||
* This allows bulk operations within a transaction to see each other's changes.
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param Document $updateData Document with update values
|
||||
* @param array $queries Query filters to match documents
|
||||
* @param array &$state Transaction state (passed by reference)
|
||||
* @return void
|
||||
*/
|
||||
public function applyBulkUpdateToState(
|
||||
string $collectionId,
|
||||
Document $updateData,
|
||||
array $queries,
|
||||
array &$state
|
||||
): void {
|
||||
if (!isset($state[$collectionId])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $this->extractFilters($queries);
|
||||
|
||||
foreach ($state[$collectionId] as $docId => $doc) {
|
||||
if ($this->documentMatchesFilters($doc, $filters)) {
|
||||
foreach ($updateData->getArrayCopy() as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$doc->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply bulk delete to documents in transaction state that match queries
|
||||
*
|
||||
* This allows bulk operations within a transaction to see each other's changes.
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param array $queries Query filters to match documents
|
||||
* @param array &$state Transaction state (passed by reference)
|
||||
* @return void
|
||||
*/
|
||||
public function applyBulkDeleteToState(
|
||||
string $collectionId,
|
||||
array $queries,
|
||||
array &$state
|
||||
): void {
|
||||
if (!isset($state[$collectionId])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $this->extractFilters($queries);
|
||||
|
||||
foreach ($state[$collectionId] as $docId => $doc) {
|
||||
if ($this->documentMatchesFilters($doc, $filters)) {
|
||||
unset($state[$collectionId][$docId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply bulk upsert to documents in transaction state
|
||||
*
|
||||
* This merges partial upsert data with full documents from transaction state,
|
||||
* preventing validation errors when upserting documents created in the same transaction.
|
||||
*
|
||||
* @param string $collectionId Collection ID
|
||||
* @param array $documents Array of Document objects to upsert (can be partial)
|
||||
* @param array &$state Transaction state (passed by reference)
|
||||
* @return array Merged documents ready for database upsert
|
||||
*/
|
||||
public function applyBulkUpsertToState(
|
||||
string $collectionId,
|
||||
array $documents,
|
||||
array &$state
|
||||
): array {
|
||||
$mergedDocuments = [];
|
||||
|
||||
foreach ($documents as $doc) {
|
||||
if (!($doc instanceof Document)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$docId = $doc->getId();
|
||||
if (!$docId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($state[$collectionId][$docId])) {
|
||||
foreach ($doc->getArrayCopy() as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$state[$collectionId][$docId]->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
$mergedDocuments[] = $state[$collectionId][$docId];
|
||||
} else {
|
||||
$mergedDocuments[] = $doc;
|
||||
}
|
||||
}
|
||||
|
||||
return $mergedDocuments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state of a transaction by replaying its operations
|
||||
*
|
||||
* @param string $transactionId Transaction ID
|
||||
* @return array State array with structure: [collectionId => [docId => ['action' => ..., 'document' => ..., 'exists' => ...]]]
|
||||
* @throws Exception
|
||||
* @throws Exception\Query
|
||||
* @throws Timeout
|
||||
*/
|
||||
private function getTransactionState(string $transactionId): array
|
||||
{
|
||||
$transaction = Authorization::skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId));
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX)
|
||||
]));
|
||||
|
||||
$state = [];
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
$collectionInternalId = $operation['collectionInternalId'];
|
||||
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
|
||||
$documentId = $operation['documentId'];
|
||||
$action = $operation['action'];
|
||||
$data = $operation['data'];
|
||||
|
||||
if ($data instanceof Document) {
|
||||
$data = $data->getArrayCopy();
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'create':
|
||||
$docId = $documentId ?? ($data['$id'] ?? null);
|
||||
if ($docId) {
|
||||
if (!isset($data['$id'])) {
|
||||
$data['$id'] = $docId;
|
||||
}
|
||||
$state[$collectionId][$docId] = [
|
||||
'action' => 'create',
|
||||
'document' => new Document($data),
|
||||
'exists' => true
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
if (isset($state[$collectionId][$documentId])) {
|
||||
$existingDocument = $state[$collectionId][$documentId]['document'];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$existingDocument->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
// Only set action to 'update' if it's not already 'create' or 'upsert'
|
||||
$currentAction = $state[$collectionId][$documentId]['action'];
|
||||
if ($currentAction !== 'create' && $currentAction !== 'upsert') {
|
||||
$state[$collectionId][$documentId]['action'] = 'update';
|
||||
}
|
||||
} else {
|
||||
$state[$collectionId][$documentId] = [
|
||||
'action' => 'update',
|
||||
'document' => new Document($data),
|
||||
'exists' => true
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'upsert':
|
||||
$docId = $documentId ?? ($data['$id'] ?? null);
|
||||
if (!$docId) {
|
||||
break;
|
||||
}
|
||||
$state[$collectionId][$docId] = [
|
||||
'action' => 'upsert',
|
||||
'document' => new Document($data),
|
||||
'exists' => true
|
||||
];
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$state[$collectionId][$documentId] = [
|
||||
'action' => 'delete',
|
||||
'exists' => false
|
||||
];
|
||||
break;
|
||||
|
||||
case 'increment':
|
||||
case 'decrement':
|
||||
$attribute = $data['attribute'] ?? null;
|
||||
$value = $data['value'] ?? 1;
|
||||
|
||||
if ($attribute) {
|
||||
if (isset($state[$collectionId][$documentId])) {
|
||||
$existingDocument = $state[$collectionId][$documentId]['document'];
|
||||
$currentValue = $existingDocument->getAttribute($attribute, 0);
|
||||
$newValue = $action === 'increment' ? $currentValue + $value : $currentValue - $value;
|
||||
$existingDocument->setAttribute($attribute, $newValue);
|
||||
|
||||
$currentAction = $state[$collectionId][$documentId]['action'];
|
||||
if ($currentAction !== 'create' && $currentAction !== 'upsert') {
|
||||
$state[$collectionId][$documentId]['action'] = 'update';
|
||||
}
|
||||
} else {
|
||||
$newValue = $action === 'increment' ? $value : -$value;
|
||||
$state[$collectionId][$documentId] = [
|
||||
'action' => 'update',
|
||||
'document' => new Document([$attribute => $newValue]),
|
||||
'exists' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulkCreate':
|
||||
if (\is_array($data)) {
|
||||
foreach ($data as $doc) {
|
||||
if ($doc instanceof Document) {
|
||||
$doc = $doc->getArrayCopy();
|
||||
}
|
||||
$state[$collectionId][$doc['$id']] = [
|
||||
'action' => 'create',
|
||||
'document' => new Document($doc),
|
||||
'exists' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulkUpdate':
|
||||
if (isset($data['queries']) && isset($data['data'])) {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
$updateData = $data['data'];
|
||||
|
||||
foreach ($state[$collectionId] ?? [] as $docId => $entry) {
|
||||
if (!$entry['exists']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$document = $entry['document'];
|
||||
$filters = $this->extractFilters($queries);
|
||||
|
||||
if ($this->documentMatchesFilters($document, $filters)) {
|
||||
foreach ($updateData as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$document->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$currentAction = $state[$collectionId][$docId]['action'];
|
||||
if ($currentAction !== 'create' && $currentAction !== 'upsert') {
|
||||
$state[$collectionId][$docId]['action'] = 'update';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulkUpsert':
|
||||
if (\is_array($data)) {
|
||||
foreach ($data as $doc) {
|
||||
if ($doc instanceof Document) {
|
||||
$doc = $doc->getArrayCopy();
|
||||
}
|
||||
|
||||
$docId = $doc['$id'] ?? null;
|
||||
if (!$docId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($state[$collectionId][$docId])) {
|
||||
$existingDocument = $state[$collectionId][$docId]['document'];
|
||||
foreach ($doc as $key => $value) {
|
||||
$existingDocument->setAttribute($key, $value);
|
||||
}
|
||||
} else {
|
||||
$state[$collectionId][$docId] = [
|
||||
'action' => 'upsert',
|
||||
'document' => new Document($doc),
|
||||
'exists' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulkDelete':
|
||||
if (isset($data['queries'])) {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
$filters = $this->extractFilters($queries);
|
||||
|
||||
foreach ($state[$collectionId] ?? [] as $docId => $entry) {
|
||||
if (!$entry['exists']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$document = $entry['document'];
|
||||
if ($this->documentMatchesFilters($document, $filters)) {
|
||||
$state[$collectionId][$docId] = [
|
||||
'action' => 'delete',
|
||||
'exists' => false
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply projection (select) semantics from queries to a document
|
||||
*
|
||||
* @param Document $doc Document to apply projection to
|
||||
* @param array $queries Query array that may contain select queries
|
||||
* @return Document Projected document
|
||||
*/
|
||||
private function applyProjection(Document $doc, array $queries): Document
|
||||
{
|
||||
if (empty($queries)) {
|
||||
return $doc;
|
||||
}
|
||||
|
||||
$selections = [];
|
||||
foreach ($queries as $query) {
|
||||
if ($query->getMethod() === Query::TYPE_SELECT) {
|
||||
$values = $query->getValues();
|
||||
foreach ($values as $value) {
|
||||
// Skip relationship selections (containing '.')
|
||||
if (!\str_contains($value, '.')) {
|
||||
$selections[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($selections) || \in_array('*', $selections)) {
|
||||
return $doc;
|
||||
}
|
||||
|
||||
// Create a new document with only selected attributes
|
||||
$projected = new Document();
|
||||
|
||||
// Always preserve internal attributes
|
||||
$projected->setAttribute('$id', $doc->getId());
|
||||
$projected->setAttribute('$collection', $doc->getCollection());
|
||||
$projected->setAttribute('$createdAt', $doc->getCreatedAt());
|
||||
$projected->setAttribute('$updatedAt', $doc->getUpdatedAt());
|
||||
if ($doc->offsetExists('$permissions')) {
|
||||
$projected->setAttribute('$permissions', $doc->getPermissions());
|
||||
}
|
||||
|
||||
// Add selected attributes
|
||||
foreach ($selections as $attribute) {
|
||||
if ($doc->offsetExists($attribute)) {
|
||||
$projected->setAttribute($attribute, $doc->getAttribute($attribute));
|
||||
}
|
||||
}
|
||||
|
||||
return $projected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract only filter queries from a query array
|
||||
*
|
||||
* @param array $queries Query array
|
||||
* @return array Filtered queries
|
||||
*/
|
||||
private function extractFilters(array $queries): array
|
||||
{
|
||||
$filters = [];
|
||||
foreach ($queries as $query) {
|
||||
$method = $query->getMethod();
|
||||
if (!\in_array($method, [
|
||||
Query::TYPE_LIMIT,
|
||||
Query::TYPE_OFFSET,
|
||||
Query::TYPE_CURSOR_AFTER,
|
||||
Query::TYPE_CURSOR_BEFORE,
|
||||
Query::TYPE_SELECT,
|
||||
Query::TYPE_ORDER_ASC,
|
||||
Query::TYPE_ORDER_DESC
|
||||
])) {
|
||||
$filters[] = $query;
|
||||
}
|
||||
}
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a document matches filter queries
|
||||
*
|
||||
* @param Document $doc Document to check
|
||||
* @param array $filters Pre-filtered Query filters (use extractFilters first)
|
||||
* @return bool True if document matches all filters
|
||||
*/
|
||||
private function documentMatchesFilters(Document $doc, array $filters): bool
|
||||
{
|
||||
if (empty($filters)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
$attribute = $filter->getAttribute();
|
||||
$values = $filter->getValues();
|
||||
$docValue = $doc->getAttribute($attribute);
|
||||
|
||||
switch ($filter->getMethod()) {
|
||||
case Query::TYPE_EQUAL:
|
||||
if (!\in_array($docValue, $values)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_NOT_EQUAL:
|
||||
if (\in_array($docValue, $values)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_CONTAINS:
|
||||
$matches = false;
|
||||
foreach ($values as $value) {
|
||||
if (\is_array($docValue) && \in_array($value, $docValue)) {
|
||||
$matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matches) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_STARTS_WITH:
|
||||
$matches = false;
|
||||
foreach ($values as $value) {
|
||||
if (\is_string($docValue) && \str_starts_with($docValue, $value)) {
|
||||
$matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matches) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_ENDS_WITH:
|
||||
$matches = false;
|
||||
foreach ($values as $value) {
|
||||
if (\is_string($docValue) && \str_ends_with($docValue, $value)) {
|
||||
$matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matches) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_GREATER:
|
||||
if (!($docValue > $values[0])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_GREATER_EQUAL:
|
||||
if (!($docValue >= $values[0])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_LESSER:
|
||||
if (!($docValue < $values[0])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_LESSER_EQUAL:
|
||||
if (!($docValue <= $values[0])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_IS_NULL:
|
||||
if (!\is_null($docValue)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_IS_NOT_NULL:
|
||||
if (\is_null($docValue)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Query::TYPE_BETWEEN:
|
||||
if (!($docValue >= $values[0] && $docValue <= $values[1])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -592,6 +592,7 @@ class Event
|
||||
$this->project = $event->getProject();
|
||||
$this->user = $event->getUser();
|
||||
$this->payload = $event->getPayload();
|
||||
$this->sensitive = $event->sensitive;
|
||||
$this->event = $event->getEvent();
|
||||
$this->params = $event->getParams();
|
||||
$this->context = $event->context;
|
||||
|
||||
+257
-241
@@ -36,334 +36,345 @@ class Exception extends \Exception
|
||||
*/
|
||||
|
||||
/** General */
|
||||
public const GENERAL_UNKNOWN = 'general_unknown';
|
||||
public const GENERAL_MOCK = 'general_mock';
|
||||
public const GENERAL_ACCESS_FORBIDDEN = 'general_access_forbidden';
|
||||
public const GENERAL_RESOURCE_BLOCKED = 'general_resource_blocked';
|
||||
public const GENERAL_UNKNOWN_ORIGIN = 'general_unknown_origin';
|
||||
public const GENERAL_API_DISABLED = 'general_api_disabled';
|
||||
public const GENERAL_SERVICE_DISABLED = 'general_service_disabled';
|
||||
public const GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope';
|
||||
public const GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded';
|
||||
public const GENERAL_SMTP_DISABLED = 'general_smtp_disabled';
|
||||
public const GENERAL_PHONE_DISABLED = 'general_phone_disabled';
|
||||
public const GENERAL_ARGUMENT_INVALID = 'general_argument_invalid';
|
||||
public const GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED = 'general_column_query_limit_exceeded';
|
||||
public const GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED = 'general_attribute_query_limit_exceeded';
|
||||
public const GENERAL_QUERY_INVALID = 'general_query_invalid';
|
||||
public const GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found';
|
||||
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
|
||||
public const GENERAL_SERVER_ERROR = 'general_server_error';
|
||||
public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported';
|
||||
public const GENERAL_CODES_DISABLED = 'general_codes_disabled';
|
||||
public const GENERAL_USAGE_DISABLED = 'general_usage_disabled';
|
||||
public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented';
|
||||
public const GENERAL_INVALID_EMAIL = 'general_invalid_email';
|
||||
public const GENERAL_INVALID_PHONE = 'general_invalid_phone';
|
||||
public const GENERAL_REGION_ACCESS_DENIED = 'general_region_access_denied';
|
||||
public const GENERAL_BAD_REQUEST = 'general_bad_request';
|
||||
public const string GENERAL_UNKNOWN = 'general_unknown';
|
||||
public const string GENERAL_MOCK = 'general_mock';
|
||||
public const string GENERAL_ACCESS_FORBIDDEN = 'general_access_forbidden';
|
||||
public const string GENERAL_RESOURCE_BLOCKED = 'general_resource_blocked';
|
||||
public const string GENERAL_UNKNOWN_ORIGIN = 'general_unknown_origin';
|
||||
public const string GENERAL_API_DISABLED = 'general_api_disabled';
|
||||
public const string GENERAL_SERVICE_DISABLED = 'general_service_disabled';
|
||||
public const string GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope';
|
||||
public const string GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded';
|
||||
public const string GENERAL_SMTP_DISABLED = 'general_smtp_disabled';
|
||||
public const string GENERAL_PHONE_DISABLED = 'general_phone_disabled';
|
||||
public const string GENERAL_ARGUMENT_INVALID = 'general_argument_invalid';
|
||||
public const string GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED = 'general_column_query_limit_exceeded';
|
||||
public const string GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED = 'general_attribute_query_limit_exceeded';
|
||||
public const string GENERAL_QUERY_INVALID = 'general_query_invalid';
|
||||
public const string GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found';
|
||||
public const string GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
|
||||
public const string GENERAL_SERVER_ERROR = 'general_server_error';
|
||||
public const string GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported';
|
||||
public const string GENERAL_CODES_DISABLED = 'general_codes_disabled';
|
||||
public const string GENERAL_USAGE_DISABLED = 'general_usage_disabled';
|
||||
public const string GENERAL_NOT_IMPLEMENTED = 'general_not_implemented';
|
||||
public const string GENERAL_INVALID_EMAIL = 'general_invalid_email';
|
||||
public const string GENERAL_INVALID_PHONE = 'general_invalid_phone';
|
||||
public const string GENERAL_REGION_ACCESS_DENIED = 'general_region_access_denied';
|
||||
public const string GENERAL_BAD_REQUEST = 'general_bad_request';
|
||||
|
||||
/** Users */
|
||||
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
|
||||
public const USER_CONSOLE_COUNT_EXCEEDED = 'user_console_count_exceeded';
|
||||
public const USER_JWT_INVALID = 'user_jwt_invalid';
|
||||
public const USER_ALREADY_EXISTS = 'user_already_exists';
|
||||
public const USER_BLOCKED = 'user_blocked';
|
||||
public const USER_INVALID_TOKEN = 'user_invalid_token';
|
||||
public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
|
||||
public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
|
||||
public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
|
||||
public const USER_INVALID_CODE = 'user_invalid_code';
|
||||
public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials';
|
||||
public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
|
||||
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
|
||||
public const USER_NOT_FOUND = 'user_not_found';
|
||||
public const USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
|
||||
public const USER_PASSWORD_PERSONAL_DATA = 'password_personal_data';
|
||||
public const USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
|
||||
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
||||
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||
public const USER_IDENTITY_NOT_FOUND = 'user_identity_not_found';
|
||||
public const USER_UNAUTHORIZED = 'user_unauthorized';
|
||||
public const USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported';
|
||||
public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists';
|
||||
public const USER_PHONE_NOT_FOUND = 'user_phone_not_found';
|
||||
public const USER_PHONE_NOT_VERIFIED = 'user_phone_not_verified';
|
||||
public const USER_EMAIL_NOT_FOUND = 'user_email_not_found';
|
||||
public const USER_EMAIL_NOT_VERIFIED = 'user_email_not_verified';
|
||||
public const USER_MISSING_ID = 'user_missing_id';
|
||||
public const USER_MORE_FACTORS_REQUIRED = 'user_more_factors_required';
|
||||
public const USER_INVALID_CHALLENGE = 'user_invalid_challenge';
|
||||
public const USER_AUTHENTICATOR_NOT_FOUND = 'user_authenticator_not_found';
|
||||
public const USER_AUTHENTICATOR_ALREADY_VERIFIED = 'user_authenticator_already_verified';
|
||||
public const USER_RECOVERY_CODES_ALREADY_EXISTS = 'user_recovery_codes_already_exists';
|
||||
public const USER_RECOVERY_CODES_NOT_FOUND = 'user_recovery_codes_not_found';
|
||||
public const USER_CHALLENGE_REQUIRED = 'user_challenge_required';
|
||||
public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request';
|
||||
public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized';
|
||||
public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
|
||||
public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_already_verified';
|
||||
public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified';
|
||||
public const USER_DELETION_PROHIBITED = 'user_deletion_prohibited';
|
||||
public const USER_TARGET_NOT_FOUND = 'user_target_not_found';
|
||||
public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
|
||||
public const USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set';
|
||||
public const string USER_COUNT_EXCEEDED = 'user_count_exceeded';
|
||||
public const string USER_CONSOLE_COUNT_EXCEEDED = 'user_console_count_exceeded';
|
||||
public const string USER_JWT_INVALID = 'user_jwt_invalid';
|
||||
public const string USER_ALREADY_EXISTS = 'user_already_exists';
|
||||
public const string USER_BLOCKED = 'user_blocked';
|
||||
public const string USER_INVALID_TOKEN = 'user_invalid_token';
|
||||
public const string USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
|
||||
public const string USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
|
||||
public const string USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
|
||||
public const string USER_INVALID_CODE = 'user_invalid_code';
|
||||
public const string USER_INVALID_CREDENTIALS = 'user_invalid_credentials';
|
||||
public const string USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
|
||||
public const string USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
|
||||
public const string USER_NOT_FOUND = 'user_not_found';
|
||||
public const string USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
|
||||
public const string USER_PASSWORD_PERSONAL_DATA = 'password_personal_data';
|
||||
public const string USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
|
||||
public const string USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
||||
public const string USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||
public const string USER_IDENTITY_NOT_FOUND = 'user_identity_not_found';
|
||||
public const string USER_UNAUTHORIZED = 'user_unauthorized';
|
||||
public const string USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported';
|
||||
public const string USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists';
|
||||
public const string USER_PHONE_NOT_FOUND = 'user_phone_not_found';
|
||||
public const string USER_PHONE_NOT_VERIFIED = 'user_phone_not_verified';
|
||||
public const string USER_EMAIL_NOT_FOUND = 'user_email_not_found';
|
||||
public const string USER_EMAIL_NOT_VERIFIED = 'user_email_not_verified';
|
||||
public const string USER_MISSING_ID = 'user_missing_id';
|
||||
public const string USER_MORE_FACTORS_REQUIRED = 'user_more_factors_required';
|
||||
public const string USER_INVALID_CHALLENGE = 'user_invalid_challenge';
|
||||
public const string USER_AUTHENTICATOR_NOT_FOUND = 'user_authenticator_not_found';
|
||||
public const string USER_AUTHENTICATOR_ALREADY_VERIFIED = 'user_authenticator_already_verified';
|
||||
public const string USER_RECOVERY_CODES_ALREADY_EXISTS = 'user_recovery_codes_already_exists';
|
||||
public const string USER_RECOVERY_CODES_NOT_FOUND = 'user_recovery_codes_not_found';
|
||||
public const string USER_CHALLENGE_REQUIRED = 'user_challenge_required';
|
||||
public const string USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request';
|
||||
public const string USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized';
|
||||
public const string USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
|
||||
public const string USER_EMAIL_ALREADY_VERIFIED = 'user_email_already_verified';
|
||||
public const string USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified';
|
||||
public const string USER_DELETION_PROHIBITED = 'user_deletion_prohibited';
|
||||
public const string USER_TARGET_NOT_FOUND = 'user_target_not_found';
|
||||
public const string USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
|
||||
public const string USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set';
|
||||
|
||||
public const API_KEY_EXPIRED = 'api_key_expired';
|
||||
public const string API_KEY_EXPIRED = 'api_key_expired';
|
||||
|
||||
/** Teams */
|
||||
public const TEAM_NOT_FOUND = 'team_not_found';
|
||||
public const TEAM_INVITE_NOT_FOUND = 'team_invite_not_found';
|
||||
public const TEAM_INVALID_SECRET = 'team_invalid_secret';
|
||||
public const TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch';
|
||||
public const TEAM_INVITE_MISMATCH = 'team_invite_mismatch';
|
||||
public const TEAM_ALREADY_EXISTS = 'team_already_exists';
|
||||
public const string TEAM_NOT_FOUND = 'team_not_found';
|
||||
public const string TEAM_INVITE_NOT_FOUND = 'team_invite_not_found';
|
||||
public const string TEAM_INVALID_SECRET = 'team_invalid_secret';
|
||||
public const string TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch';
|
||||
public const string TEAM_INVITE_MISMATCH = 'team_invite_mismatch';
|
||||
public const string TEAM_ALREADY_EXISTS = 'team_already_exists';
|
||||
|
||||
/** Console */
|
||||
public const RESOURCE_ALREADY_EXISTS = 'resource_already_exists';
|
||||
public const string RESOURCE_ALREADY_EXISTS = 'resource_already_exists';
|
||||
|
||||
/** Membership */
|
||||
public const MEMBERSHIP_NOT_FOUND = 'membership_not_found';
|
||||
public const MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed';
|
||||
public const MEMBERSHIP_DELETION_PROHIBITED = 'membership_deletion_prohibited';
|
||||
public const MEMBERSHIP_DOWNGRADE_PROHIBITED = 'membership_downgrade_prohibited';
|
||||
public const string MEMBERSHIP_NOT_FOUND = 'membership_not_found';
|
||||
public const string MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed';
|
||||
public const string MEMBERSHIP_DELETION_PROHIBITED = 'membership_deletion_prohibited';
|
||||
public const string MEMBERSHIP_DOWNGRADE_PROHIBITED = 'membership_downgrade_prohibited';
|
||||
|
||||
/** Avatars */
|
||||
public const AVATAR_SET_NOT_FOUND = 'avatar_set_not_found';
|
||||
public const AVATAR_NOT_FOUND = 'avatar_not_found';
|
||||
public const AVATAR_IMAGE_NOT_FOUND = 'avatar_image_not_found';
|
||||
public const AVATAR_REMOTE_URL_FAILED = 'avatar_remote_url_failed';
|
||||
public const AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found';
|
||||
public const AVATAR_SVG_SANITIZATION_FAILED = 'avatar_svg_sanitization_failed';
|
||||
public const string AVATAR_SET_NOT_FOUND = 'avatar_set_not_found';
|
||||
public const string AVATAR_NOT_FOUND = 'avatar_not_found';
|
||||
public const string AVATAR_IMAGE_NOT_FOUND = 'avatar_image_not_found';
|
||||
public const string AVATAR_REMOTE_URL_FAILED = 'avatar_remote_url_failed';
|
||||
public const string AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found';
|
||||
public const string AVATAR_SVG_SANITIZATION_FAILED = 'avatar_svg_sanitization_failed';
|
||||
|
||||
/** Storage */
|
||||
public const STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists';
|
||||
public const STORAGE_FILE_NOT_FOUND = 'storage_file_not_found';
|
||||
public const STORAGE_DEVICE_NOT_FOUND = 'storage_device_not_found';
|
||||
public const STORAGE_FILE_EMPTY = 'storage_file_empty';
|
||||
public const STORAGE_FILE_TYPE_UNSUPPORTED = 'storage_file_type_unsupported';
|
||||
public const STORAGE_INVALID_FILE_SIZE = 'storage_invalid_file_size';
|
||||
public const STORAGE_INVALID_FILE = 'storage_invalid_file';
|
||||
public const STORAGE_BUCKET_ALREADY_EXISTS = 'storage_bucket_already_exists';
|
||||
public const STORAGE_BUCKET_NOT_FOUND = 'storage_bucket_not_found';
|
||||
public const STORAGE_INVALID_CONTENT_RANGE = 'storage_invalid_content_range';
|
||||
public const STORAGE_INVALID_RANGE = 'storage_invalid_range';
|
||||
public const STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id';
|
||||
public const STORAGE_FILE_NOT_PUBLIC = 'storage_file_not_public';
|
||||
public const string STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists';
|
||||
public const string STORAGE_FILE_NOT_FOUND = 'storage_file_not_found';
|
||||
public const string STORAGE_DEVICE_NOT_FOUND = 'storage_device_not_found';
|
||||
public const string STORAGE_FILE_EMPTY = 'storage_file_empty';
|
||||
public const string STORAGE_FILE_TYPE_UNSUPPORTED = 'storage_file_type_unsupported';
|
||||
public const string STORAGE_INVALID_FILE_SIZE = 'storage_invalid_file_size';
|
||||
public const string STORAGE_INVALID_FILE = 'storage_invalid_file';
|
||||
public const string STORAGE_BUCKET_ALREADY_EXISTS = 'storage_bucket_already_exists';
|
||||
public const string STORAGE_BUCKET_NOT_FOUND = 'storage_bucket_not_found';
|
||||
public const string STORAGE_INVALID_CONTENT_RANGE = 'storage_invalid_content_range';
|
||||
public const string STORAGE_INVALID_RANGE = 'storage_invalid_range';
|
||||
public const string STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id';
|
||||
public const string STORAGE_FILE_NOT_PUBLIC = 'storage_file_not_public';
|
||||
|
||||
/** VCS */
|
||||
public const INSTALLATION_NOT_FOUND = 'installation_not_found';
|
||||
public const PROVIDER_REPOSITORY_NOT_FOUND = 'provider_repository_not_found';
|
||||
public const REPOSITORY_NOT_FOUND = 'repository_not_found';
|
||||
public const PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict';
|
||||
public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure';
|
||||
public const string INSTALLATION_NOT_FOUND = 'installation_not_found';
|
||||
public const string PROVIDER_REPOSITORY_NOT_FOUND = 'provider_repository_not_found';
|
||||
public const string REPOSITORY_NOT_FOUND = 'repository_not_found';
|
||||
public const string PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict';
|
||||
public const string GENERAL_PROVIDER_FAILURE = 'general_provider_failure';
|
||||
|
||||
/** Sites */
|
||||
public const SITE_NOT_FOUND = 'site_not_found';
|
||||
public const SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
|
||||
public const string SITE_NOT_FOUND = 'site_not_found';
|
||||
public const string SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
|
||||
|
||||
/** Functions */
|
||||
public const FUNCTION_NOT_FOUND = 'function_not_found';
|
||||
public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
|
||||
public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
|
||||
public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
|
||||
public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
|
||||
public const FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected';
|
||||
public const FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing';
|
||||
public const string FUNCTION_NOT_FOUND = 'function_not_found';
|
||||
public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
|
||||
public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
|
||||
public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
|
||||
public const string FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
|
||||
public const string FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected';
|
||||
public const string FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing';
|
||||
|
||||
/** Deployments */
|
||||
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
|
||||
public const string DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
|
||||
|
||||
/** Builds */
|
||||
public const BUILD_NOT_FOUND = 'build_not_found';
|
||||
public const BUILD_NOT_READY = 'build_not_ready';
|
||||
public const BUILD_IN_PROGRESS = 'build_in_progress';
|
||||
public const BUILD_ALREADY_COMPLETED = 'build_already_completed';
|
||||
public const BUILD_CANCELED = 'build_canceled';
|
||||
public const BUILD_FAILED = 'build_failed';
|
||||
public const string BUILD_NOT_FOUND = 'build_not_found';
|
||||
public const string BUILD_NOT_READY = 'build_not_ready';
|
||||
public const string BUILD_IN_PROGRESS = 'build_in_progress';
|
||||
public const string BUILD_ALREADY_COMPLETED = 'build_already_completed';
|
||||
public const string BUILD_CANCELED = 'build_canceled';
|
||||
public const string BUILD_FAILED = 'build_failed';
|
||||
|
||||
/** Execution */
|
||||
public const EXECUTION_NOT_FOUND = 'execution_not_found';
|
||||
public const EXECUTION_IN_PROGRESS = 'execution_in_progress';
|
||||
public const string EXECUTION_NOT_FOUND = 'execution_not_found';
|
||||
public const string EXECUTION_IN_PROGRESS = 'execution_in_progress';
|
||||
|
||||
/** Log */
|
||||
public const LOG_NOT_FOUND = 'log_not_found';
|
||||
public const string LOG_NOT_FOUND = 'log_not_found';
|
||||
|
||||
/** Databases */
|
||||
public const DATABASE_NOT_FOUND = 'database_not_found';
|
||||
public const DATABASE_ALREADY_EXISTS = 'database_already_exists';
|
||||
public const DATABASE_TIMEOUT = 'database_timeout';
|
||||
public const DATABASE_QUERY_ORDER_NULL = 'database_query_order_null';
|
||||
public const string DATABASE_NOT_FOUND = 'database_not_found';
|
||||
public const string DATABASE_ALREADY_EXISTS = 'database_already_exists';
|
||||
public const string DATABASE_TIMEOUT = 'database_timeout';
|
||||
public const string DATABASE_QUERY_ORDER_NULL = 'database_query_order_null';
|
||||
|
||||
/** Collections */
|
||||
public const COLLECTION_NOT_FOUND = 'collection_not_found';
|
||||
public const COLLECTION_ALREADY_EXISTS = 'collection_already_exists';
|
||||
public const COLLECTION_LIMIT_EXCEEDED = 'collection_limit_exceeded';
|
||||
public const string COLLECTION_NOT_FOUND = 'collection_not_found';
|
||||
public const string COLLECTION_ALREADY_EXISTS = 'collection_already_exists';
|
||||
public const string COLLECTION_LIMIT_EXCEEDED = 'collection_limit_exceeded';
|
||||
|
||||
/** Tables */
|
||||
public const TABLE_NOT_FOUND = 'table_not_found';
|
||||
public const TABLE_ALREADY_EXISTS = 'table_already_exists';
|
||||
public const TABLE_LIMIT_EXCEEDED = 'table_limit_exceeded';
|
||||
public const string TABLE_NOT_FOUND = 'table_not_found';
|
||||
public const string TABLE_ALREADY_EXISTS = 'table_already_exists';
|
||||
public const string TABLE_LIMIT_EXCEEDED = 'table_limit_exceeded';
|
||||
|
||||
/** Documents */
|
||||
public const DOCUMENT_NOT_FOUND = 'document_not_found';
|
||||
public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
|
||||
public const DOCUMENT_MISSING_DATA = 'document_missing_data';
|
||||
public const DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload';
|
||||
public const DOCUMENT_ALREADY_EXISTS = 'document_already_exists';
|
||||
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
|
||||
public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
|
||||
public const string DOCUMENT_NOT_FOUND = 'document_not_found';
|
||||
public const string DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
|
||||
public const string DOCUMENT_MISSING_DATA = 'document_missing_data';
|
||||
public const string DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload';
|
||||
public const string DOCUMENT_ALREADY_EXISTS = 'document_already_exists';
|
||||
public const string DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
|
||||
public const string DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
|
||||
|
||||
/** Rows */
|
||||
public const ROW_NOT_FOUND = 'row_not_found';
|
||||
public const ROW_INVALID_STRUCTURE = 'row_invalid_structure';
|
||||
public const ROW_MISSING_DATA = 'row_missing_data';
|
||||
public const ROW_MISSING_PAYLOAD = 'row_missing_payload';
|
||||
public const ROW_ALREADY_EXISTS = 'row_already_exists';
|
||||
public const ROW_UPDATE_CONFLICT = 'row_update_conflict';
|
||||
public const ROW_DELETE_RESTRICTED = 'row_delete_restricted';
|
||||
public const string ROW_NOT_FOUND = 'row_not_found';
|
||||
public const string ROW_INVALID_STRUCTURE = 'row_invalid_structure';
|
||||
public const string ROW_MISSING_DATA = 'row_missing_data';
|
||||
public const string ROW_MISSING_PAYLOAD = 'row_missing_payload';
|
||||
public const string ROW_ALREADY_EXISTS = 'row_already_exists';
|
||||
public const string ROW_UPDATE_CONFLICT = 'row_update_conflict';
|
||||
public const string ROW_DELETE_RESTRICTED = 'row_delete_restricted';
|
||||
|
||||
/** Attributes */
|
||||
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';
|
||||
public const ATTRIBUTE_UNKNOWN = 'attribute_unknown';
|
||||
public const ATTRIBUTE_NOT_AVAILABLE = 'attribute_not_available';
|
||||
public const ATTRIBUTE_FORMAT_UNSUPPORTED = 'attribute_format_unsupported';
|
||||
public const ATTRIBUTE_DEFAULT_UNSUPPORTED = 'attribute_default_unsupported';
|
||||
public const ATTRIBUTE_ALREADY_EXISTS = 'attribute_already_exists';
|
||||
public const ATTRIBUTE_LIMIT_EXCEEDED = 'attribute_limit_exceeded';
|
||||
public const ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid';
|
||||
public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
|
||||
public const ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize';
|
||||
public const string ATTRIBUTE_NOT_FOUND = 'attribute_not_found';
|
||||
public const string ATTRIBUTE_UNKNOWN = 'attribute_unknown';
|
||||
public const string ATTRIBUTE_NOT_AVAILABLE = 'attribute_not_available';
|
||||
public const string ATTRIBUTE_FORMAT_UNSUPPORTED = 'attribute_format_unsupported';
|
||||
public const string ATTRIBUTE_DEFAULT_UNSUPPORTED = 'attribute_default_unsupported';
|
||||
public const string ATTRIBUTE_ALREADY_EXISTS = 'attribute_already_exists';
|
||||
public const string ATTRIBUTE_LIMIT_EXCEEDED = 'attribute_limit_exceeded';
|
||||
public const string ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid';
|
||||
public const string ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
|
||||
public const string ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize';
|
||||
|
||||
public const ATTRIBUTE_TYPE_NOT_SUPPORTED = 'ATTRIBUTE_TYPE_NOT_SUPPORTED';
|
||||
|
||||
/** Columns */
|
||||
public const COLUMN_NOT_FOUND = 'column_not_found';
|
||||
public const COLUMN_UNKNOWN = 'column_unknown';
|
||||
public const COLUMN_NOT_AVAILABLE = 'column_not_available';
|
||||
public const COLUMN_FORMAT_UNSUPPORTED = 'column_format_unsupported';
|
||||
public const COLUMN_DEFAULT_UNSUPPORTED = 'column_default_unsupported';
|
||||
public const COLUMN_ALREADY_EXISTS = 'column_already_exists';
|
||||
public const COLUMN_LIMIT_EXCEEDED = 'column_limit_exceeded';
|
||||
public const COLUMN_VALUE_INVALID = 'column_value_invalid';
|
||||
public const COLUMN_TYPE_INVALID = 'column_type_invalid';
|
||||
public const COLUMN_INVALID_RESIZE = 'column_invalid_resize';
|
||||
public const string COLUMN_NOT_FOUND = 'column_not_found';
|
||||
public const string COLUMN_UNKNOWN = 'column_unknown';
|
||||
public const string COLUMN_NOT_AVAILABLE = 'column_not_available';
|
||||
public const string COLUMN_FORMAT_UNSUPPORTED = 'column_format_unsupported';
|
||||
public const string COLUMN_DEFAULT_UNSUPPORTED = 'column_default_unsupported';
|
||||
public const string COLUMN_ALREADY_EXISTS = 'column_already_exists';
|
||||
public const string COLUMN_LIMIT_EXCEEDED = 'column_limit_exceeded';
|
||||
public const string COLUMN_VALUE_INVALID = 'column_value_invalid';
|
||||
public const string COLUMN_TYPE_INVALID = 'column_type_invalid';
|
||||
public const string COLUMN_INVALID_RESIZE = 'column_invalid_resize';
|
||||
|
||||
public const COLUMN_TYPE_NOT_SUPPORTED = 'COLUMN_TYPE_NOT_SUPPORTED';
|
||||
|
||||
/** Relationship */
|
||||
public const RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid';
|
||||
public const string RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid';
|
||||
|
||||
/** Indexes */
|
||||
public const INDEX_NOT_FOUND = 'index_not_found';
|
||||
public const INDEX_LIMIT_EXCEEDED = 'index_limit_exceeded';
|
||||
public const INDEX_ALREADY_EXISTS = 'index_already_exists';
|
||||
public const INDEX_INVALID = 'index_invalid';
|
||||
public const INDEX_DEPENDENCY = 'index_dependency';
|
||||
public const string INDEX_NOT_FOUND = 'index_not_found';
|
||||
public const string INDEX_LIMIT_EXCEEDED = 'index_limit_exceeded';
|
||||
public const string INDEX_ALREADY_EXISTS = 'index_already_exists';
|
||||
public const string INDEX_INVALID = 'index_invalid';
|
||||
public const string INDEX_DEPENDENCY = 'index_dependency';
|
||||
|
||||
/** Column Indexes */
|
||||
public const COLUMN_INDEX_NOT_FOUND = 'column_index_not_found';
|
||||
public const COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded';
|
||||
public const COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists';
|
||||
public const COLUMN_INDEX_INVALID = 'column_index_invalid';
|
||||
public const COLUMN_INDEX_DEPENDENCY = 'column_index_dependency';
|
||||
public const string COLUMN_INDEX_NOT_FOUND = 'column_index_not_found';
|
||||
public const string COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded';
|
||||
public const string COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists';
|
||||
public const string COLUMN_INDEX_INVALID = 'column_index_invalid';
|
||||
public const string COLUMN_INDEX_DEPENDENCY = 'column_index_dependency';
|
||||
|
||||
/** Transactions */
|
||||
public const string TRANSACTION_NOT_FOUND = 'transaction_not_found';
|
||||
public const string TRANSACTION_ALREADY_EXISTS = 'transaction_already_exists';
|
||||
public const string TRANSACTION_INVALID = 'transaction_invalid';
|
||||
public const string TRANSACTION_FAILED = 'transaction_failed';
|
||||
public const string TRANSACTION_EXPIRED = 'transaction_expired';
|
||||
public const string TRANSACTION_CONFLICT = 'transaction_conflict';
|
||||
public const string TRANSACTION_LIMIT_EXCEEDED = 'transaction_limit_exceeded';
|
||||
public const string TRANSACTION_NOT_READY = 'transaction_not_ready';
|
||||
|
||||
|
||||
/** Projects */
|
||||
public const PROJECT_NOT_FOUND = 'project_not_found';
|
||||
public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
|
||||
public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
||||
public const PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
||||
public const PROJECT_INVALID_SUCCESS_URL = 'project_invalid_success_url';
|
||||
public const PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
|
||||
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
|
||||
public const PROJECT_KEY_EXPIRED = 'project_key_expired';
|
||||
public const string PROJECT_NOT_FOUND = 'project_not_found';
|
||||
public const string PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
|
||||
public const string PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
||||
public const string PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
||||
public const string PROJECT_INVALID_SUCCESS_URL = 'project_invalid_success_url';
|
||||
public const string PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
|
||||
public const string PROJECT_RESERVED_PROJECT = 'project_reserved_project';
|
||||
public const string PROJECT_KEY_EXPIRED = 'project_key_expired';
|
||||
|
||||
public const PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
|
||||
public const string PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
|
||||
|
||||
public const PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion';
|
||||
public const string PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion';
|
||||
|
||||
public const PROJECT_REGION_UNSUPPORTED = 'project_region_unsupported';
|
||||
public const string PROJECT_REGION_UNSUPPORTED = 'project_region_unsupported';
|
||||
|
||||
/** Webhooks */
|
||||
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
|
||||
public const string WEBHOOK_NOT_FOUND = 'webhook_not_found';
|
||||
|
||||
/** Router */
|
||||
public const ROUTER_HOST_NOT_FOUND = 'router_host_not_found';
|
||||
public const ROUTER_DOMAIN_NOT_CONFIGURED = 'router_domain_not_configured';
|
||||
public const string ROUTER_HOST_NOT_FOUND = 'router_host_not_found';
|
||||
public const string ROUTER_DOMAIN_NOT_CONFIGURED = 'router_domain_not_configured';
|
||||
|
||||
/** Proxy */
|
||||
public const RULE_RESOURCE_NOT_FOUND = 'rule_resource_not_found';
|
||||
public const RULE_NOT_FOUND = 'rule_not_found';
|
||||
public const RULE_ALREADY_EXISTS = 'rule_already_exists';
|
||||
public const RULE_VERIFICATION_FAILED = 'rule_verification_failed';
|
||||
public const string RULE_RESOURCE_NOT_FOUND = 'rule_resource_not_found';
|
||||
public const string RULE_NOT_FOUND = 'rule_not_found';
|
||||
public const string RULE_ALREADY_EXISTS = 'rule_already_exists';
|
||||
public const string RULE_VERIFICATION_FAILED = 'rule_verification_failed';
|
||||
|
||||
/** Keys */
|
||||
public const KEY_NOT_FOUND = 'key_not_found';
|
||||
public const string KEY_NOT_FOUND = 'key_not_found';
|
||||
|
||||
/** Variables */
|
||||
public const VARIABLE_NOT_FOUND = 'variable_not_found';
|
||||
public const VARIABLE_ALREADY_EXISTS = 'variable_already_exists';
|
||||
public const VARIABLE_CANNOT_UNSET_SECRET = 'variable_cannot_unset_secret';
|
||||
public const string VARIABLE_NOT_FOUND = 'variable_not_found';
|
||||
public const string VARIABLE_ALREADY_EXISTS = 'variable_already_exists';
|
||||
public const string VARIABLE_CANNOT_UNSET_SECRET = 'variable_cannot_unset_secret';
|
||||
|
||||
/** Platform */
|
||||
public const PLATFORM_NOT_FOUND = 'platform_not_found';
|
||||
public const string PLATFORM_NOT_FOUND = 'platform_not_found';
|
||||
|
||||
/** GraphqQL */
|
||||
public const GRAPHQL_NO_QUERY = 'graphql_no_query';
|
||||
public const GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries';
|
||||
public const string GRAPHQL_NO_QUERY = 'graphql_no_query';
|
||||
public const string GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries';
|
||||
|
||||
/** Migrations */
|
||||
public const MIGRATION_NOT_FOUND = 'migration_not_found';
|
||||
public const MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
|
||||
public const MIGRATION_IN_PROGRESS = 'migration_in_progress';
|
||||
public const MIGRATION_PROVIDER_ERROR = 'migration_provider_error';
|
||||
public const string MIGRATION_NOT_FOUND = 'migration_not_found';
|
||||
public const string MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
|
||||
public const string MIGRATION_IN_PROGRESS = 'migration_in_progress';
|
||||
public const string MIGRATION_PROVIDER_ERROR = 'migration_provider_error';
|
||||
|
||||
/** Realtime */
|
||||
public const REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
|
||||
public const REALTIME_TOO_MANY_MESSAGES = 'realtime_too_many_messages';
|
||||
public const REALTIME_POLICY_VIOLATION = 'realtime_policy_violation';
|
||||
public const string REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
|
||||
public const string REALTIME_TOO_MANY_MESSAGES = 'realtime_too_many_messages';
|
||||
public const string REALTIME_POLICY_VIOLATION = 'realtime_policy_violation';
|
||||
|
||||
/** Health */
|
||||
public const HEALTH_QUEUE_SIZE_EXCEEDED = 'health_queue_size_exceeded';
|
||||
public const HEALTH_CERTIFICATE_EXPIRED = 'health_certificate_expired';
|
||||
public const HEALTH_INVALID_HOST = 'health_invalid_host';
|
||||
public const string HEALTH_QUEUE_SIZE_EXCEEDED = 'health_queue_size_exceeded';
|
||||
public const string HEALTH_CERTIFICATE_EXPIRED = 'health_certificate_expired';
|
||||
public const string HEALTH_INVALID_HOST = 'health_invalid_host';
|
||||
|
||||
/** Provider */
|
||||
public const PROVIDER_NOT_FOUND = 'provider_not_found';
|
||||
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
|
||||
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
|
||||
public const PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials';
|
||||
public const string PROVIDER_NOT_FOUND = 'provider_not_found';
|
||||
public const string PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
|
||||
public const string PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
|
||||
public const string PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials';
|
||||
|
||||
/** Topic */
|
||||
public const TOPIC_NOT_FOUND = 'topic_not_found';
|
||||
public const TOPIC_ALREADY_EXISTS = 'topic_already_exists';
|
||||
public const string TOPIC_NOT_FOUND = 'topic_not_found';
|
||||
public const string TOPIC_ALREADY_EXISTS = 'topic_already_exists';
|
||||
|
||||
/** Subscriber */
|
||||
public const SUBSCRIBER_NOT_FOUND = 'subscriber_not_found';
|
||||
public const SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists';
|
||||
public const string SUBSCRIBER_NOT_FOUND = 'subscriber_not_found';
|
||||
public const string SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists';
|
||||
|
||||
/** Message */
|
||||
public const MESSAGE_NOT_FOUND = 'message_not_found';
|
||||
public const MESSAGE_MISSING_TARGET = 'message_missing_target';
|
||||
public const MESSAGE_ALREADY_SENT = 'message_already_sent';
|
||||
public const MESSAGE_ALREADY_PROCESSING = 'message_already_processing';
|
||||
public const MESSAGE_ALREADY_FAILED = 'message_already_failed';
|
||||
public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled';
|
||||
public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email';
|
||||
public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms';
|
||||
public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
|
||||
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
|
||||
public const string MESSAGE_NOT_FOUND = 'message_not_found';
|
||||
public const string MESSAGE_MISSING_TARGET = 'message_missing_target';
|
||||
public const string MESSAGE_ALREADY_SENT = 'message_already_sent';
|
||||
public const string MESSAGE_ALREADY_PROCESSING = 'message_already_processing';
|
||||
public const string MESSAGE_ALREADY_FAILED = 'message_already_failed';
|
||||
public const string MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled';
|
||||
public const string MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email';
|
||||
public const string MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms';
|
||||
public const string MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
|
||||
public const string MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
|
||||
|
||||
/** Targets */
|
||||
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
|
||||
public const string TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
|
||||
|
||||
/** Schedules */
|
||||
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
|
||||
public const string SCHEDULE_NOT_FOUND = 'schedule_not_found';
|
||||
|
||||
/** Tokens */
|
||||
public const TOKEN_NOT_FOUND = 'token_not_found';
|
||||
public const TOKEN_EXPIRED = 'token_expired';
|
||||
public const TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
|
||||
public const string TOKEN_NOT_FOUND = 'token_not_found';
|
||||
public const string TOKEN_EXPIRED = 'token_expired';
|
||||
public const string TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
|
||||
|
||||
protected string $type = '';
|
||||
protected array $errors = [];
|
||||
@@ -371,8 +382,13 @@ class Exception extends \Exception
|
||||
private array $ctas = [];
|
||||
private ?string $view = null;
|
||||
|
||||
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null, ?string $view = null)
|
||||
{
|
||||
public function __construct(
|
||||
string $type = Exception::GENERAL_UNKNOWN,
|
||||
string $message = null,
|
||||
int|string $code = null,
|
||||
\Throwable $previous = null,
|
||||
?string $view = null
|
||||
) {
|
||||
$this->errors = Config::getParam('errors');
|
||||
$this->type = $type;
|
||||
$this->view = $view;
|
||||
@@ -381,7 +397,7 @@ class Exception extends \Exception
|
||||
// Mark string errors like HY001 from PDO as 500 errors
|
||||
if (\is_string($this->code)) {
|
||||
if (\is_numeric($this->code)) {
|
||||
$this->code = (int) $this->code;
|
||||
$this->code = (int)$this->code;
|
||||
} else {
|
||||
$this->code = 500;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ class Mapper
|
||||
'json' => Types::json(),
|
||||
'none' => Types::json(),
|
||||
'any' => Types::json(),
|
||||
'array' => Types::json()
|
||||
'array' => Types::json(),
|
||||
'enum' => Type::string()
|
||||
];
|
||||
|
||||
foreach ($defaults as $type => $default) {
|
||||
@@ -452,6 +453,7 @@ class Mapper
|
||||
'ip' => static::model("{$prefix}Ip"),
|
||||
default => static::model("{$prefix}String"),
|
||||
},
|
||||
'enum' => static::model("{$prefix}String"), // TODO: Add enum type (breaking change if added)
|
||||
'integer' => static::model("{$prefix}Integer"),
|
||||
'double' => static::model("{$prefix}Float"),
|
||||
'boolean' => static::model("{$prefix}Boolean"),
|
||||
|
||||
@@ -312,13 +312,17 @@ class Realtime extends MessagingAdapter
|
||||
throw new \Exception('Collection or the Table needs to be passed to Realtime for Document/Row events in the Database.');
|
||||
}
|
||||
|
||||
$tableId = $payload->getAttribute('$tableId', '');
|
||||
$collectionId = $payload->getAttribute('$collectionId', '');
|
||||
$resourceId = $tableId ?: $collectionId;
|
||||
|
||||
$channels[] = 'rows';
|
||||
$channels[] = 'databases.' . $database->getId() . '.tables.' . $payload->getAttribute('$tableId') . '.rows';
|
||||
$channels[] = 'databases.' . $database->getId() . '.tables.' . $payload->getAttribute('$tableId') . '.rows.' . $payload->getId();
|
||||
$channels[] = 'databases.' . $database->getId() . '.tables.' . $resourceId . '.rows';
|
||||
$channels[] = 'databases.' . $database->getId() . '.tables.' . $resourceId . '.rows.' . $payload->getId();
|
||||
|
||||
$channels[] = 'documents';
|
||||
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getAttribute('$collectionId') . '.documents';
|
||||
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getAttribute('$collectionId') . '.documents.' . $payload->getId();
|
||||
$channels[] = 'databases.' . $database->getId() . '.collections.' . $resourceId . '.documents';
|
||||
$channels[] = 'databases.' . $database->getId() . '.collections.' . $resourceId . '.documents.' . $payload->getId();
|
||||
|
||||
$roles = $collection->getAttribute('documentSecurity', false)
|
||||
? \array_merge($collection->getRead(), $payload->getRead())
|
||||
|
||||
@@ -730,8 +730,8 @@ class V19 extends Migration
|
||||
|
||||
if (empty($document->getAttribute('scheduleId', null))) {
|
||||
$schedule = $this->dbForPlatform->createDocument('schedules', new Document([
|
||||
'region' => $project->getAttribute('region'),
|
||||
'resourceType' => 'function',
|
||||
'region' => $this->project->getAttribute('region'),
|
||||
'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION,
|
||||
'resourceId' => $document->getId(),
|
||||
'resourceInternalId' => $document->getSequence(),
|
||||
'resourceUpdatedAt' => DateTime::now(),
|
||||
|
||||
@@ -53,7 +53,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* Get the SDK group name for the current action.
|
||||
*/
|
||||
protected function getSdkGroup(): string
|
||||
protected function getSDKGroup(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'collections' : 'tables';
|
||||
}
|
||||
@@ -61,7 +61,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* Get the SDK namespace for the current action.
|
||||
*/
|
||||
protected function getSdkNamespace(): string
|
||||
protected function getSDKNamespace(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'databases' : 'tablesDB';
|
||||
}
|
||||
|
||||
+19
-5
@@ -66,7 +66,7 @@ abstract class Action extends UtopiaAction
|
||||
*
|
||||
* Can be used for XList operations as well!
|
||||
*/
|
||||
protected function getSdkGroup(): string
|
||||
protected function getSDKGroup(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'attributes' : 'columns';
|
||||
}
|
||||
@@ -74,7 +74,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* Get the SDK namespace for the current action.
|
||||
*/
|
||||
protected function getSdkNamespace(): string
|
||||
protected function getSDKNamespace(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'databases' : 'tablesDB';
|
||||
}
|
||||
@@ -122,7 +122,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* Get the correct invalid structure message.
|
||||
*/
|
||||
protected function getInvalidStructureException(): string
|
||||
protected function getStructureException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::DOCUMENT_INVALID_STRUCTURE
|
||||
@@ -366,13 +366,27 @@ abstract class Action extends UtopiaAction
|
||||
'filters' => $filters,
|
||||
'options' => $options,
|
||||
]);
|
||||
if (
|
||||
!$dbForProject->getAdapter()->getSupportForSpatialIndexNull() &&
|
||||
\in_array($attribute->getAttribute('type'), Database::SPATIAL_TYPES) &&
|
||||
$attribute->getAttribute('required')
|
||||
) {
|
||||
$hasData = !Authorization::skip(fn () => $dbForProject
|
||||
->findOne('database_' . $db->getSequence() . '_collection_' . $collection->getSequence()))
|
||||
->isEmpty();
|
||||
|
||||
if ($hasData) {
|
||||
throw new StructureException('Failed to add required spatial column: existing rows present. Make the column optional.');
|
||||
}
|
||||
}
|
||||
$dbForProject->checkAttribute($collection, $attribute);
|
||||
$attribute = $dbForProject->createDocument('attributes', $attribute);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
} catch (Throwable $e) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
|
||||
@@ -416,7 +430,7 @@ abstract class Action extends UtopiaAction
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
} catch (StructureException) {
|
||||
throw new Exception($this->getInvalidStructureException());
|
||||
throw new Exception($this->getStructureException());
|
||||
} catch (Throwable $e) {
|
||||
$dbForProject->deleteDocument('attributes', $attribute->getId());
|
||||
throw $e;
|
||||
@@ -580,7 +594,7 @@ abstract class Action extends UtopiaAction
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
}
|
||||
|
||||
if ($primaryDocumentOptions['twoWay']) {
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-boolean-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-boolean-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-datetime-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-datetime-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Delete extends Action
|
||||
->label('audits.event', 'attribute.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/delete-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-email-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-email-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-enum-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-enum-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-float-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-float-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -47,8 +47,8 @@ class Get extends Action
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/get-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-ip-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-ip-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-integer-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-integer-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-line-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-line-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-point-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-point-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-polygon-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-polygon-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-relationship-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-relationship-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+3
-3
@@ -47,8 +47,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-string-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -95,7 +95,7 @@ class Create extends Action
|
||||
array $plan
|
||||
): void {
|
||||
if (!App::isDevelopment() && $encrypt && !empty($plan) && !($plan['databasesAllowEncrypt'] ?? false)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSdkGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSdkGroup() . '.');
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSDKGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSDKGroup() . '.');
|
||||
}
|
||||
|
||||
if ($encrypt && $size < APP_DATABASE_ENCRYPT_SIZE_MIN) {
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-string-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Create extends Action
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-url-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ class Update extends Action
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-url-attribute.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+3
-3
@@ -41,8 +41,8 @@ class XList extends Action
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/list-attributes.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -141,7 +141,7 @@ class XList extends Action
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $total,
|
||||
$this->getSdkGroup() => $attributes,
|
||||
$this->getSDKGroup() => $attributes,
|
||||
]), $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Create extends Action
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: $this->getSdkGroup(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-collection.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -42,7 +42,7 @@ class Delete extends Action
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: $this->getSdkGroup(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/delete-collection.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+28
-3
@@ -76,7 +76,7 @@ abstract class Action extends AppwriteAction
|
||||
*
|
||||
* Can be used for XList operations as well!
|
||||
*/
|
||||
protected function getSdkGroup(): string
|
||||
protected function getSDKGroup(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'documents' : 'rows';
|
||||
}
|
||||
@@ -84,7 +84,7 @@ abstract class Action extends AppwriteAction
|
||||
/**
|
||||
* Get the SDK namespace for the current action.
|
||||
*/
|
||||
protected function getSdkNamespace(): string
|
||||
protected function getSDKNamespace(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'databases' : 'tablesDB';
|
||||
}
|
||||
@@ -160,7 +160,7 @@ abstract class Action extends AppwriteAction
|
||||
/**
|
||||
* Get the correct invalid structure message.
|
||||
*/
|
||||
protected function getInvalidStructureException(): string
|
||||
protected function getStructureException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::DOCUMENT_INVALID_STRUCTURE
|
||||
@@ -205,6 +205,31 @@ abstract class Action extends AppwriteAction
|
||||
return $this->isCollectionsAPI() ? 'collection' : 'table';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct attribute/column key for increment/decrement operations.
|
||||
*/
|
||||
protected function getAttributeKey(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'attribute' : 'column';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key used in ID parameters (e.g., 'collectionId' or 'tableId').
|
||||
*/
|
||||
protected function getGroupId(): string
|
||||
{
|
||||
return $this->getCollectionsEventsContext() . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource ID key for the current action.
|
||||
*/
|
||||
protected function getResourceId(): string
|
||||
{
|
||||
$resource = $this->isCollectionsAPI() ? 'document' : 'row';
|
||||
return $resource . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove configured removable attributes from a document.
|
||||
* Used for relationship path handling to remove API-specific attributes.
|
||||
|
||||
+77
-5
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -14,10 +15,12 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use InvalidArgumentException;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Limit as LimitException;
|
||||
use Utopia\Database\Exception\NotFound as NotFoundException;
|
||||
use Utopia\Database\Exception\Type as TypeException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -52,8 +55,8 @@ class Decrement extends Action
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/decrement-document-attribute.md',
|
||||
auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY],
|
||||
@@ -75,15 +78,20 @@ class Decrement extends Action
|
||||
->param('attribute', '', new Key(), 'Attribute key.')
|
||||
->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true)
|
||||
->param('min', null, new Numeric(), 'Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
@@ -94,6 +102,70 @@ class Decrement extends Action
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $documentId,
|
||||
'action' => 'decrement',
|
||||
'data' => [
|
||||
$this->getAttributeKey() => $attribute,
|
||||
'value' => $value,
|
||||
'min' => $min,
|
||||
],
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually decrementing
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
$attribute => $value,
|
||||
]);
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($mockDocument, $this->getResponseModel());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$document = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
@@ -107,9 +179,9 @@ class Decrement extends Action
|
||||
} catch (NotFoundException) {
|
||||
throw new Exception($this->getStructureNotFoundException());
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the minimum value of ' . $min);
|
||||
throw new Exception($this->getLimitException(), $this->getSDKNamespace() . ' "' . $attribute . '" has reached the minimum value of ' . $min);
|
||||
} catch (TypeException) {
|
||||
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number');
|
||||
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSDKNamespace() . ' "' . $attribute . '" is not a number');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
+77
-5
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -14,10 +15,12 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use InvalidArgumentException;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Limit as LimitException;
|
||||
use Utopia\Database\Exception\NotFound as NotFoundException;
|
||||
use Utopia\Database\Exception\Type as TypeException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -52,8 +55,8 @@ class Increment extends Action
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/increment-document-attribute.md',
|
||||
auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY],
|
||||
@@ -75,15 +78,20 @@ class Increment extends Action
|
||||
->param('attribute', '', new Key(), 'Attribute key.')
|
||||
->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true)
|
||||
->param('max', null, new Numeric(), 'Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
@@ -94,6 +102,70 @@ class Increment extends Action
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $documentId,
|
||||
'action' => 'increment',
|
||||
'data' => [
|
||||
$this->getAttributeKey() => $attribute,
|
||||
'value' => $value,
|
||||
'max' => $max,
|
||||
],
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually incrementing
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
$attribute => $value,
|
||||
]);
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($mockDocument, $this->getResponseModel());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$document = $dbForProject->increaseDocumentAttribute(
|
||||
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
@@ -107,9 +179,9 @@ class Increment extends Action
|
||||
} catch (NotFoundException) {
|
||||
throw new Exception($this->getStructureNotFoundException());
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the maximum value of ' . $max);
|
||||
throw new Exception($this->getLimitException(), $this->getSDKNamespace() . ' "' . $attribute . '" has reached the maximum value of ' . $max);
|
||||
} catch (TypeException) {
|
||||
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number');
|
||||
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSDKNamespace() . ' "' . $attribute . '" is not a number');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
+57
-5
@@ -17,6 +17,7 @@ use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Exception\Restricted as RestrictedException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
@@ -50,8 +51,8 @@ class Delete extends Action
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/delete-documents.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
@@ -70,6 +71,7 @@ class Delete extends Action
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
@@ -81,7 +83,7 @@ class Delete extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
{
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
@@ -99,15 +101,63 @@ class Delete extends Action
|
||||
);
|
||||
|
||||
if ($hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
|
||||
}
|
||||
|
||||
$originalQueries = $queries;
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'action' => 'bulkDelete',
|
||||
'data' => [
|
||||
'queries' => $originalQueries,
|
||||
],
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually deleting documents
|
||||
$response->dynamic(new Document([
|
||||
$this->getSDKGroup() => [],
|
||||
'total' => 0, // Can't predict how many would be deleted
|
||||
]), $this->getResponseModel());
|
||||
return;
|
||||
}
|
||||
|
||||
$documents = [];
|
||||
|
||||
try {
|
||||
@@ -124,6 +174,8 @@ class Delete extends Action
|
||||
throw new Exception($this->getConflictException());
|
||||
} catch (RestrictedException) {
|
||||
throw new Exception($this->getRestrictedException());
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($documents as $document) {
|
||||
@@ -137,7 +189,7 @@ class Delete extends Action
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $modified,
|
||||
$this->getSdkGroup() => $documents,
|
||||
$this->getSDKGroup() => $documents,
|
||||
]), $this->getResponseModel());
|
||||
|
||||
$this->triggerBulk(
|
||||
|
||||
+60
-7
@@ -18,6 +18,7 @@ use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Exception\Relationship as RelationshipException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -53,8 +54,8 @@ class Update extends Action
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-documents.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
@@ -74,6 +75,7 @@ class Update extends Action
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
@@ -85,7 +87,7 @@ class Update extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -111,16 +113,18 @@ class Update extends Action
|
||||
);
|
||||
|
||||
if ($hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
|
||||
}
|
||||
|
||||
$originalQueries = $queries;
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if ($data['$permissions']) {
|
||||
if (isset($data['$permissions'])) {
|
||||
$validator = new Permissions();
|
||||
if (!$validator->isValid($data['$permissions'])) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
|
||||
@@ -129,6 +133,53 @@ class Update extends Action
|
||||
|
||||
$data = $this->removeReadonlyAttributes($data, privileged: true);
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'action' => 'bulkUpdate',
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'queries' => $originalQueries,
|
||||
],
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually updating documents
|
||||
$response->dynamic(new Document([
|
||||
$this->getSDKGroup() => [],
|
||||
'total' => 0, // Can't predict how many would be updated
|
||||
]), $this->getResponseModel());
|
||||
return;
|
||||
}
|
||||
|
||||
$documents = [];
|
||||
|
||||
try {
|
||||
@@ -149,7 +200,9 @@ class Update extends Action
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($documents as $document) {
|
||||
@@ -163,7 +216,7 @@ class Update extends Action
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $modified,
|
||||
$this->getSdkGroup() => $documents
|
||||
$this->getSDKGroup() => $documents
|
||||
]), $this->getResponseModel());
|
||||
|
||||
$this->triggerBulk(
|
||||
|
||||
+55
-7
@@ -18,6 +18,7 @@ use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Exception\Relationship as RelationshipException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
@@ -51,8 +52,8 @@ class Upsert extends Action
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/upsert-documents.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
@@ -72,6 +73,7 @@ class Upsert extends Action
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of document data as JSON objects. May contain partial documents.', false, ['plan'])
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
@@ -83,7 +85,7 @@ class Upsert extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
{
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
@@ -101,7 +103,7 @@ class Upsert extends Action
|
||||
);
|
||||
|
||||
if ($hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
|
||||
}
|
||||
|
||||
foreach ($documents as $key => $document) {
|
||||
@@ -109,11 +111,57 @@ class Upsert extends Action
|
||||
$documents[$key] = new Document($document);
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operations in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'action' => 'bulkUpsert',
|
||||
'data' => $documents,
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually upserting documents
|
||||
$response->dynamic(new Document([
|
||||
$this->getSDKGroup() => [],
|
||||
'total' => \count($documents),
|
||||
]), $this->getResponseModel());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$upserted = [];
|
||||
|
||||
try {
|
||||
$modified = $dbForProject->withPreserveDates(function () use ($dbForProject, $database, $collection, $documents, $plan, &$upserted) {
|
||||
return $dbForProject->createOrUpdateDocuments(
|
||||
return $dbForProject->upsertDocuments(
|
||||
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
$documents,
|
||||
onNext: function (Document $document) use ($plan, &$upserted) {
|
||||
@@ -130,7 +178,7 @@ class Upsert extends Action
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($upserted as $document) {
|
||||
@@ -144,7 +192,7 @@ class Upsert extends Action
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $modified,
|
||||
$this->getSdkGroup() => $upserted
|
||||
$this->getSDKGroup() => $upserted
|
||||
]), $this->getResponseModel());
|
||||
|
||||
$this->triggerBulk(
|
||||
|
||||
+83
-11
@@ -63,8 +63,8 @@ class Create extends Action
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
desc: 'Create document',
|
||||
description: '/docs/references/databases/create-document.md',
|
||||
@@ -82,6 +82,7 @@ class Create extends Action
|
||||
new Parameter('documentId', optional: false),
|
||||
new Parameter('data', optional: false),
|
||||
new Parameter('permissions', optional: true),
|
||||
new Parameter('transactionId', optional: true),
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
@@ -89,8 +90,8 @@ class Create extends Action
|
||||
),
|
||||
),
|
||||
new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: $this->getBulkActionName(self::getName()),
|
||||
desc: 'Create documents',
|
||||
description: '/docs/references/databases/create-documents.md',
|
||||
@@ -106,6 +107,7 @@ class Create extends Action
|
||||
new Parameter('databaseId', optional: false),
|
||||
new Parameter('collectionId', optional: false),
|
||||
new Parameter('documents', optional: false),
|
||||
new Parameter('transactionId', optional: true),
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
@@ -119,6 +121,7 @@ class Create extends Action
|
||||
->param('data', [], new JSON(), 'Document data as JSON object.', true, example: '{"username":"walter.obrien","email":"walter.obrien@example.com","fullName":"Walter O\'Brien","age":30,"isAdmin":false}')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan'])
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
@@ -127,9 +130,10 @@ class Create extends Action
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -144,7 +148,7 @@ class Create extends Action
|
||||
}
|
||||
if (!empty($data) && !empty($documents)) {
|
||||
// Both single and bulk documents provided
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSdkGroup());
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
|
||||
}
|
||||
if (!empty($data) && empty($documentId)) {
|
||||
// Single document provided without document ID
|
||||
@@ -157,12 +161,12 @@ class Create extends Action
|
||||
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
|
||||
throw new Exception(
|
||||
Exception::GENERAL_BAD_REQUEST,
|
||||
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSdkGroup() . ', set "$id" on each instead.'
|
||||
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
|
||||
);
|
||||
}
|
||||
if (!empty($documents) && !empty($permissions)) {
|
||||
// Bulk documents provided with permissions
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSdkGroup() . ', set "$permissions" on each instead');
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
|
||||
}
|
||||
|
||||
$isBulk = true;
|
||||
@@ -196,7 +200,7 @@ class Create extends Action
|
||||
);
|
||||
|
||||
if ($isBulk && $hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSdkNamespace() .' with relationship ' . $this->getStructureContext());
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() .' with relationship ' . $this->getStructureContext());
|
||||
}
|
||||
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk) {
|
||||
@@ -361,6 +365,74 @@ class Create extends Action
|
||||
return $document;
|
||||
}, $documents);
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $isBulk ? null : $documentId,
|
||||
'action' => $isBulk ? 'bulkCreate' : 'create',
|
||||
'data' => $isBulk ? $documents : $documents[0],
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually creating documents
|
||||
if ($isBulk) {
|
||||
$response->dynamic(new Document([
|
||||
$this->getSDKGroup() => [],
|
||||
'total' => \count($documents),
|
||||
]), $this->getBulkResponseModel());
|
||||
} else {
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documents[0]['$id'] ?? $documentId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$documents[0]
|
||||
]);
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
|
||||
->dynamic($mockDocument, $this->getResponseModel());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$dbForProject->withPreserveDates(
|
||||
fn () => $dbForProject->createDocuments(
|
||||
@@ -375,7 +447,7 @@ class Create extends Action
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
@@ -405,7 +477,7 @@ class Create extends Action
|
||||
if ($isBulk) {
|
||||
$response->dynamic(new Document([
|
||||
'total' => count($documents),
|
||||
$this->getSdkGroup() => $documents
|
||||
$this->getSDKGroup() => $documents
|
||||
]), $this->getBulkResponseModel());
|
||||
|
||||
$this->triggerBulk(
|
||||
|
||||
+83
-5
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -13,8 +14,10 @@ use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Restricted as RestrictedException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
@@ -51,8 +54,8 @@ class Delete extends Action
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/delete-document.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
@@ -71,16 +74,30 @@ class Delete extends Action
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
|
||||
{
|
||||
public function action(
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
?string $transactionId,
|
||||
?\DateTime $requestTimestamp,
|
||||
UtopiaResponse $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
TransactionState $transactionState,
|
||||
array $plan
|
||||
): void {
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
@@ -97,12 +114,73 @@ class Delete extends Action
|
||||
}
|
||||
|
||||
// Read permission should not be required for delete
|
||||
$document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
if ($transactionId !== null) {
|
||||
// Use transaction-aware document retrieval to see changes from same transaction
|
||||
$document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
|
||||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $documentId,
|
||||
'action' => 'delete',
|
||||
'data' => [],
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually deleting document
|
||||
$response->noContent();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
|
||||
$dbForProject->deleteDocument(
|
||||
|
||||
+13
-6
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -42,8 +43,8 @@ class Get extends Action
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/get-document.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
@@ -63,13 +64,15 @@ class Get extends Action
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
@@ -93,13 +96,17 @@ class Get extends Action
|
||||
|
||||
try {
|
||||
$selects = Query::groupByType($queries)['selections'] ?? [];
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
if (! empty($selects)) {
|
||||
// Use transaction-aware document retrieval if transactionId is provided
|
||||
if ($transactionId !== null) {
|
||||
$document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId, $queries);
|
||||
} elseif (! empty($selects)) {
|
||||
// has selects, allow relationship on documents!
|
||||
$document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries);
|
||||
$document = $dbForProject->getDocument($collectionTableId, $documentId, $queries);
|
||||
} else {
|
||||
// has no selects, disable relationship looping on documents!
|
||||
$document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries));
|
||||
$document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument($collectionTableId, $documentId, $queries));
|
||||
}
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
|
||||
+2
-5
@@ -48,7 +48,7 @@ class XList extends Action
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: 'logs',
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/get-document-logs.md',
|
||||
@@ -108,10 +108,7 @@ class XList extends Action
|
||||
$audit = new Audit($dbForProject);
|
||||
$type = $this->getCollectionsEventsContext();
|
||||
$context = $this->getContext();
|
||||
$resource = match ($context) {
|
||||
ROWS => "database/$databaseId/grid/$type/$collectionId/$context/{$document->getId()}",
|
||||
default => "database/$databaseId/$type/$collectionId/$context/{$document->getId()}",
|
||||
};
|
||||
$resource = "database/$databaseId/$type/$collectionId/$context/{$document->getId()}";
|
||||
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
||||
|
||||
+82
-6
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -55,8 +56,8 @@ class Update extends Action
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-document.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
@@ -77,17 +78,19 @@ class Update extends Action
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan): void
|
||||
{
|
||||
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
if (empty($data) && \is_null($permissions)) {
|
||||
@@ -111,7 +114,14 @@ class Update extends Action
|
||||
|
||||
// Read permission should not be required for update
|
||||
/** @var Document $document */
|
||||
$document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
if ($transactionId !== null) {
|
||||
// Use transaction-aware document retrieval to see changes from same transaction
|
||||
$document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
|
||||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
@@ -231,6 +241,72 @@ class Update extends Action
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
|
||||
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $documentId,
|
||||
'action' => 'update',
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually updating document
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$document->getArrayCopy(),
|
||||
...$data
|
||||
]);
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($mockDocument, $this->getResponseModel());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$document = $dbForProject->withRequestTimestamp(
|
||||
$requestTimestamp,
|
||||
@@ -247,7 +323,7 @@ class Update extends Action
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
}
|
||||
|
||||
$collectionsCache = [];
|
||||
|
||||
+86
-7
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -57,8 +58,8 @@ class Upsert extends Action
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/upsert-document.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
@@ -80,16 +81,19 @@ class Upsert extends Action
|
||||
->param('documentId', '', new CustomId(), 'Document ID.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object. Include all required attributes of the document to be created or updated.')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -122,9 +126,16 @@ class Upsert extends Action
|
||||
|
||||
$permissions = Permission::aggregate($permissions, $allowedPermissions);
|
||||
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
// If no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario)
|
||||
if (\is_null($permissions)) {
|
||||
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
|
||||
if ($transactionId !== null) {
|
||||
// Use transaction-aware document retrieval to see changes from same transaction
|
||||
$oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
|
||||
}
|
||||
if ($oldDocument->isEmpty()) {
|
||||
if (!empty($user->getId())) {
|
||||
$defaultPermissions = [];
|
||||
@@ -240,10 +251,73 @@ class Upsert extends Action
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
// Enforce max operations per transaction
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
if (($existing + 1) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
// Stage the operation in transaction logs
|
||||
$staged = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $documentId,
|
||||
'action' => 'upsert',
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
|
||||
$dbForProject->createDocument('transactionLogs', $staged);
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// Return successful response without actually upserting document
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$data
|
||||
]);
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
|
||||
->dynamic($mockDocument, $this->getResponseModel());
|
||||
return;
|
||||
}
|
||||
|
||||
$upserted = [];
|
||||
try {
|
||||
$dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) {
|
||||
return $dbForProject->createOrUpdateDocuments(
|
||||
return $dbForProject->upsertDocuments(
|
||||
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
[$newDocument],
|
||||
onNext: function (Document $document) use (&$upserted) {
|
||||
@@ -258,13 +332,18 @@ class Upsert extends Action
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
}
|
||||
|
||||
$collectionsCache = [];
|
||||
|
||||
if (empty($upserted[0])) {
|
||||
$upserted[0] = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
|
||||
if ($transactionId !== null) {
|
||||
// For transactions, get the document with transaction changes applied
|
||||
$upserted[0] = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$upserted[0] = $dbForProject->getDocument($collectionTableId, $documentId);
|
||||
}
|
||||
}
|
||||
|
||||
$document = $upserted[0];
|
||||
|
||||
+17
-46
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -45,8 +46,8 @@ class XList extends Action
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/list-documents.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
@@ -65,13 +66,15 @@ class XList extends Action
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
@@ -121,17 +124,22 @@ class XList extends Action
|
||||
|
||||
try {
|
||||
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
if (! empty($selectQueries)) {
|
||||
// Use transaction-aware document retrieval if transactionId is provided
|
||||
if ($transactionId !== null) {
|
||||
$documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries);
|
||||
$total = $transactionState->countDocuments($collectionTableId, $transactionId, $queries);
|
||||
} elseif (! empty($selectQueries)) {
|
||||
// has selects, allow relationship on documents
|
||||
$documents = $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries);
|
||||
$documents = $dbForProject->find($collectionTableId, $queries);
|
||||
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
} else {
|
||||
// has no selects, disable relationship loading on documents
|
||||
/* @type Document[] $documents */
|
||||
$documents = $dbForProject->skipRelationships(fn () => $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries));
|
||||
$documents = $dbForProject->skipRelationships(fn () => $dbForProject->find($collectionTableId, $queries));
|
||||
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
}
|
||||
|
||||
$total = $dbForProject->count('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries, APP_LIMIT_COUNT);
|
||||
} catch (OrderException $e) {
|
||||
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
|
||||
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
|
||||
@@ -158,47 +166,10 @@ class XList extends Action
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
|
||||
|
||||
// Check if the SELECT query includes the removable attributes
|
||||
$hasWildcard = false;
|
||||
$hasSelectQueries = !empty($selectQueries);
|
||||
$requestedAttributes = [];
|
||||
|
||||
if ($hasSelectQueries) {
|
||||
foreach ($selectQueries as $query) {
|
||||
if ($query->getMethod() !== Query::TYPE_SELECT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values = $query->getValues();
|
||||
if (\in_array('*', $values, true)) {
|
||||
$hasWildcard = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check which removable attributes are explicitly requested
|
||||
foreach ($this->removableAttributes['*'] as $attribute) {
|
||||
if (\in_array($attribute, $values, true)) {
|
||||
$requestedAttributes[$attribute] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasWildcard) {
|
||||
foreach ($documents as $document) {
|
||||
// Remove attributes that are not explicitly requested
|
||||
foreach ($this->removableAttributes['*'] as $attribute) {
|
||||
if (!isset($requestedAttributes[$attribute])) {
|
||||
$document->removeAttribute($attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $total,
|
||||
// rows or documents
|
||||
$this->getSdkGroup() => $documents,
|
||||
$this->getSDKGroup() => $documents,
|
||||
]), $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class Get extends Action
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: $this->getSdkGroup(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/get-collection.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* Get the SDK group name for the current action.
|
||||
*/
|
||||
final protected function getSdkGroup(): string
|
||||
final protected function getSDKGroup(): string
|
||||
{
|
||||
return 'indexes';
|
||||
}
|
||||
@@ -60,7 +60,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* Get the SDK namespace for the current action.
|
||||
*/
|
||||
final protected function getSdkNamespace(): string
|
||||
final protected function getSDKNamespace(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'databases' : 'tablesDB';
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@ class Create extends Action
|
||||
->label('audits.event', 'index.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-index.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -46,8 +46,8 @@ class Delete extends Action
|
||||
->label('audits.event', 'index.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/delete-index.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -37,8 +37,8 @@ class Get extends Action
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/get-index.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ class XList extends Action
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/list-indexes.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -49,7 +49,7 @@ class XList extends Action
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: $this->getSdkGroup(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/get-collection-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
@@ -104,11 +104,7 @@ class XList extends Action
|
||||
|
||||
$audit = new Audit($dbForProject);
|
||||
$context = $this->getContext();
|
||||
$resource = match ($context) {
|
||||
TABLES => "database/$databaseId/grid/$context/$collectionId",
|
||||
default => "database/$databaseId/$context/$collectionId",
|
||||
};
|
||||
|
||||
$resource = "database/$databaseId/$context/$collectionId";
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
||||
$output = [];
|
||||
|
||||
@@ -45,7 +45,7 @@ class Update extends Action
|
||||
->label('audits.resource', 'database/{request.databaseId}/collections/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: $this->getSdkGroup(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-collection.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -44,7 +44,7 @@ class XList extends Action
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: $this->getSdkGroup(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/list-collections.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -121,7 +121,7 @@ class XList extends Action
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $total,
|
||||
$this->getSdkGroup() => $collections,
|
||||
$this->getSDKGroup() => $collections,
|
||||
]), $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
|
||||
abstract class Action extends UtopiaAction
|
||||
{
|
||||
/**
|
||||
* The current API context (either 'table' or 'collection').
|
||||
*/
|
||||
private ?string $context = COLLECTIONS;
|
||||
|
||||
public function setHttpPath(string $path): UtopiaAction
|
||||
{
|
||||
if (\str_contains($path, '/tablesdb')) {
|
||||
$this->context = TABLES;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current API context.
|
||||
*/
|
||||
protected function getContext(): string
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current action is for the Collections API.
|
||||
*/
|
||||
protected function isCollectionsAPI(): bool
|
||||
{
|
||||
return $this->getContext() === COLLECTIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key used in event parameters (e.g., 'collectionId' or 'tableId').
|
||||
*/
|
||||
protected function getGroupId(): string
|
||||
{
|
||||
return $this->getContext() . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource type for the current action (either 'document' or 'row').
|
||||
*/
|
||||
protected function getResource(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'document' : 'row';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource ID key for the current action.
|
||||
*/
|
||||
protected function getResourceId(): string
|
||||
{
|
||||
return $this->getResource() . 'Id';
|
||||
}
|
||||
|
||||
protected function getAttributeKey(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'attribute' : 'column';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'createDatabasesTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/databases/transactions')
|
||||
->desc('Create transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: 'transactions',
|
||||
name: 'createTransaction',
|
||||
description: '/docs/references/databases/create-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_CREATED,
|
||||
model: UtopiaResponse::MODEL_TRANSACTION,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user): void
|
||||
{
|
||||
$permissions = [];
|
||||
if (!empty($user->getId())) {
|
||||
$allowedPermissions = [
|
||||
Database::PERMISSION_READ,
|
||||
Database::PERMISSION_UPDATE,
|
||||
Database::PERMISSION_DELETE,
|
||||
];
|
||||
|
||||
foreach ($allowedPermissions as $permission) {
|
||||
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->createDocument('transactions', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => $permissions,
|
||||
'status' => 'pending',
|
||||
'operations' => 0,
|
||||
'expiresAt' => DateTime::addSeconds(new \DateTime(), $ttl),
|
||||
])));
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
|
||||
->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
|
||||
class Delete extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'deleteDatabasesTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_NONE;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/databases/transactions/:transactionId')
|
||||
->desc('Delete transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: 'transactions',
|
||||
name: 'deleteTransaction',
|
||||
description: '/docs/references/databases/delete-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_NOCONTENT,
|
||||
model: UtopiaResponse::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('transactionId', '', new UID(), 'Transaction ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDeletes')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeleteEvent $queueForDeletes): void
|
||||
{
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('transactions', $transactionId);
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getDatabasesTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/databases/transactions/:transactionId')
|
||||
->desc('Get transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: 'transactions',
|
||||
name: 'getTransaction',
|
||||
description: '/docs/references/databases/get-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: UtopiaResponse::MODEL_TRANSACTION,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('transactionId', '', new UID(), 'Transaction ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject): void
|
||||
{
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION);
|
||||
}
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Operation;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'createDatabasesTransactionOperations';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/databases/transactions/:transactionId/operations')
|
||||
->desc('Create operations')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: 'transactions',
|
||||
name: 'createOperations',
|
||||
description: '/docs/references/databases/create-operations.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_CREATED,
|
||||
model: UtopiaResponse::MODEL_TRANSACTION,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('transactionId', '', new UID(), 'Transaction ID.')
|
||||
->param('operations', [], new ArrayList(new Operation(type: 'legacy')), 'Array of staged operations.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan): void
|
||||
{
|
||||
if (empty($operations)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty');
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
// API keys and admins can read any transaction, regular users need permissions
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
|
||||
$existing = $transaction->getAttribute('operations', 0);
|
||||
|
||||
if (($existing + \count($operations)) > $maxBatch) {
|
||||
throw new Exception(
|
||||
Exception::TRANSACTION_LIMIT_EXCEEDED,
|
||||
'Transaction already has ' . $existing . ' operations, adding ' . \count($operations) . ' would exceed the maximum of ' . $maxBatch
|
||||
);
|
||||
}
|
||||
|
||||
$databases = $collections = $staged = $dependants = [];
|
||||
foreach ($operations as $operation) {
|
||||
if (!$isAPIKey && !$isPrivilegedUser && \in_array($operation['action'], [
|
||||
'bulkCreate',
|
||||
'bulkUpdate',
|
||||
'bulkUpsert',
|
||||
'bulkDelete'
|
||||
])) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$database = $databases[$operation['databaseId']] ??= Authorization::skip(fn () => $dbForProject->getDocument('databases', $operation['databaseId']));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collection = $collections[$operation[$this->getGroupId()]] ??=
|
||||
Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()]));
|
||||
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (\in_array($operation['action'], ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
$hasRelationships = \array_filter(
|
||||
$collection->getAttribute('attributes', []),
|
||||
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
|
||||
);
|
||||
if ($hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk operations are not supported for ' . $this->getGroupId() . ' with relationship attributes');
|
||||
}
|
||||
}
|
||||
|
||||
// For update, upsert, delete, increment, decrement, check document existence first
|
||||
$document = null;
|
||||
if (\in_array($operation['action'], ['update', 'delete', 'upsert', 'increment', 'decrement'])) {
|
||||
$documentId = $operation[$this->getResourceId()] ?? null;
|
||||
if (empty($documentId)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Document ID is required for ' . $operation['action'] . ' operations');
|
||||
}
|
||||
|
||||
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
$isDependant = isset($dependants[$collectionKey][$documentId]);
|
||||
|
||||
$document = $transactionState->getDocument($collectionKey, $documentId, $transactionId);
|
||||
if ($document->isEmpty() && !$isDependant && $operation['action'] !== 'upsert') {
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk operations skip permission validation entirely (API key/admin only, already checked above)
|
||||
if (!\in_array($operation['action'], ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
$permissionType = match ($operation['action']) {
|
||||
'create' => Database::PERMISSION_CREATE,
|
||||
'update', 'increment', 'decrement' => Database::PERMISSION_UPDATE,
|
||||
'delete' => Database::PERMISSION_DELETE,
|
||||
'upsert' => ($document && !$document->isEmpty()) ? Database::PERMISSION_UPDATE : Database::PERMISSION_CREATE,
|
||||
default => throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid action: ' . $operation['action'])
|
||||
};
|
||||
|
||||
// For individual operations, enforce permissions unless using API key/admin
|
||||
if (!$isAPIKey && !$isPrivilegedUser) {
|
||||
$documentSecurity = $collection->getAttribute('documentSecurity', false);
|
||||
$validator = new Authorization($permissionType);
|
||||
$collectionValid = $validator->isValid($collection->getPermissionsByType($permissionType));
|
||||
$documentValid = false;
|
||||
if ($document !== null && !$document->isEmpty() && $documentSecurity) {
|
||||
if ($permissionType === Database::PERMISSION_UPDATE) {
|
||||
$documentValid = $validator->isValid($document->getUpdate());
|
||||
} elseif ($permissionType === Database::PERMISSION_DELETE) {
|
||||
$documentValid = $validator->isValid($document->getDelete());
|
||||
}
|
||||
}
|
||||
|
||||
if ($permissionType === Database::PERMISSION_CREATE || !$documentSecurity) {
|
||||
if (!$collectionValid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
} else {
|
||||
if (!$collectionValid && !$documentValid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
// Users can only set permissions for roles they have
|
||||
if (isset($operation['data']['$permissions'])) {
|
||||
$permissions = $operation['data']['$permissions'];
|
||||
$roles = Authorization::getRoles();
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
if ($permission->getPermission() != $type) {
|
||||
continue;
|
||||
}
|
||||
$role = (new Role(
|
||||
$permission->getRole(),
|
||||
$permission->getIdentifier(),
|
||||
$permission->getDimension()
|
||||
))->toString();
|
||||
if (!Authorization::isRole($role)) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$staged[] = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'transactionInternalId' => $transaction->getSequence(),
|
||||
'documentId' => $operation[$this->getResourceId()] ?? null,
|
||||
'action' => $operation['action'],
|
||||
'data' => $operation['data'] ?? [],
|
||||
]);
|
||||
|
||||
// Track create operations for dependent update/increment/decrement/delete operations in same batch
|
||||
if ($operation['action'] === 'create') {
|
||||
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
$documentId = $operation[$this->getResourceId()] ?? null;
|
||||
if ($documentId) {
|
||||
$dependants[$collectionKey][$documentId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
|
||||
$dbForProject->createDocuments('transactionLogs', $staged);
|
||||
return $dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
'operations',
|
||||
\count($operations)
|
||||
);
|
||||
}));
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
|
||||
->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Exception\Limit as LimitException;
|
||||
use Utopia\Database\Exception\NotFound as NotFoundException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Exception\Transaction as TransactionException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'updateDatabasesTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/databases/transactions/:transactionId')
|
||||
->desc('Update transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: 'transactions',
|
||||
name: 'updateTransaction',
|
||||
description: '/docs/references/databases/update-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: UtopiaResponse::MODEL_TRANSACTION,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('transactionId', '', new UID(), 'Transaction ID.')
|
||||
->param('commit', false, new Boolean(), 'Commit transaction?', true)
|
||||
->param('rollback', false, new Boolean(), 'Rollback transaction?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('transactionState')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $transactionId
|
||||
* @param bool $commit
|
||||
* @param bool $rollback
|
||||
* @param UtopiaResponse $response
|
||||
* @param Database $dbForProject
|
||||
* @param Document $user
|
||||
* @param TransactionState $transactionState
|
||||
* @param Delete $queueForDeletes
|
||||
* @param Event $queueForEvents
|
||||
* @param StatsUsage $queueForStatsUsage
|
||||
* @param Event $queueForRealtime
|
||||
* @param Event $queueForFunctions
|
||||
* @param Event $queueForWebhooks
|
||||
* @return void
|
||||
* @throws ConflictException
|
||||
* @throws Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @throws \Utopia\Exception
|
||||
*/
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void
|
||||
{
|
||||
if (!$commit && !$rollback) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
|
||||
}
|
||||
if ($commit && $rollback) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time');
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now'));
|
||||
if ($now > $expiresAt) {
|
||||
throw new Exception(Exception::TRANSACTION_EXPIRED);
|
||||
}
|
||||
|
||||
if ($commit) {
|
||||
|
||||
$operations = [];
|
||||
$totalOperations = 0;
|
||||
$databaseOperations = [];
|
||||
|
||||
try {
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
|
||||
$operations = Authorization::skip(fn () => $dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX),
|
||||
]));
|
||||
|
||||
$state = [];
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
$collectionInternalId = $operation['collectionInternalId'];
|
||||
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
|
||||
$documentId = $operation['documentId'];
|
||||
$createdAt = new \DateTime($operation['$createdAt']);
|
||||
$action = $operation['action'];
|
||||
$data = $operation['data'];
|
||||
|
||||
if ($action === 'delete' && $documentId && empty($data)) {
|
||||
$doc = $dbForProject->getDocument($collectionId, $documentId);
|
||||
if (!$doc->isEmpty()) {
|
||||
$operation['data'] = $doc->getArrayCopy();
|
||||
$data = $operation['data'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array($action, ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
$totalOperations++;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
if ($data instanceof Document) {
|
||||
$data = $data->getArrayCopy();
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'create':
|
||||
$this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'update':
|
||||
$this->handleUpdateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'upsert':
|
||||
$this->handleUpsertOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'delete':
|
||||
$this->handleDeleteOperation($dbForProject, $collectionId, $documentId, $createdAt, $state);
|
||||
break;
|
||||
case 'increment':
|
||||
$this->handleIncrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'decrement':
|
||||
$this->handleDecrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'bulkCreate':
|
||||
$count = $this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkUpdate':
|
||||
$count = $this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkUpsert':
|
||||
$count = $this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkDelete':
|
||||
$count = $this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committed'])
|
||||
));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
});
|
||||
|
||||
} catch (NotFoundException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e);
|
||||
} catch (DuplicateException|ConflictException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e);
|
||||
} catch (StructureException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
|
||||
} catch (LimitException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage());
|
||||
} catch (TransactionException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage());
|
||||
} catch (QueryException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations);
|
||||
|
||||
foreach ($databaseOperations as $sequence => $count) {
|
||||
$queueForStatsUsage->addMetric(
|
||||
str_replace('{databaseInternalId}', $sequence, METRIC_DATABASE_ID_OPERATIONS_WRITES),
|
||||
$count
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
$collectionInternalId = $operation['collectionInternalId'];
|
||||
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
|
||||
$action = $operation['action'];
|
||||
$documentId = $operation['documentId'];
|
||||
$data = $operation['data'];
|
||||
|
||||
if ($data instanceof Document) {
|
||||
$data = $data->getArrayCopy();
|
||||
}
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->findOne('databases', [
|
||||
Query::equal('$sequence', [$databaseInternalId])
|
||||
]));
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->findOne('database_' . $databaseInternalId, [
|
||||
Query::equal('$sequence', [$collectionInternalId])
|
||||
]));
|
||||
|
||||
$groupId = $this->getGroupId();
|
||||
$resourceId = $this->getResourceId();
|
||||
$contextKey = $this->getContext();
|
||||
$resource = $this->getResource();
|
||||
$resourcePlural = $resource . 's';
|
||||
|
||||
$queueForEvents
|
||||
->setParam('databaseId', $database->getId())
|
||||
->setContext('database', $database)
|
||||
->setParam('collectionId', $collection->getId())
|
||||
->setParam('tableId', $collection->getId())
|
||||
->setContext($contextKey, $collection);
|
||||
|
||||
$eventAction = '';
|
||||
$documentsToTrigger = [];
|
||||
|
||||
switch ($action) {
|
||||
case 'create':
|
||||
$eventAction = 'create';
|
||||
$docId = $documentId ?? $data['$id'] ?? null;
|
||||
if ($docId) {
|
||||
$doc = $dbForProject->getDocument($collectionId, $docId);
|
||||
if (!$doc->isEmpty()) {
|
||||
$documentsToTrigger[] = $doc;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'update':
|
||||
case 'increment':
|
||||
case 'decrement':
|
||||
$eventAction = 'update';
|
||||
if ($documentId) {
|
||||
$doc = $dbForProject->getDocument($collectionId, $documentId);
|
||||
if (!$doc->isEmpty()) {
|
||||
$documentsToTrigger[] = $doc;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
$eventAction = 'delete';
|
||||
if ($documentId && !empty($data)) {
|
||||
$documentsToTrigger[] = new Document(array_merge($data, ['$id' => $documentId]));
|
||||
}
|
||||
break;
|
||||
case 'upsert':
|
||||
$eventAction = 'update';
|
||||
$docId = $documentId ?? $data['$id'] ?? null;
|
||||
if ($docId) {
|
||||
$doc = $dbForProject->getDocument($collectionId, $docId);
|
||||
if (!$doc->isEmpty()) {
|
||||
$documentsToTrigger[] = $doc;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'bulkCreate':
|
||||
case 'bulkUpdate':
|
||||
case 'bulkUpsert':
|
||||
case 'bulkDelete':
|
||||
break;
|
||||
}
|
||||
|
||||
$eventString = "databases.[databaseId].{$contextKey}s.[{$groupId}].{$resourcePlural}.[{$resourceId}]." . $eventAction;
|
||||
|
||||
$queueForEvents->setEvent($eventString);
|
||||
|
||||
foreach ($documentsToTrigger as $doc) {
|
||||
$payload = $doc->getArrayCopy();
|
||||
$payload['$tableId'] = $collection->getId();
|
||||
$payload['$collectionId'] = $collection->getId();
|
||||
|
||||
$queueForEvents
|
||||
->setParam('documentId', $doc->getId())
|
||||
->setParam('rowId', $doc->getId())
|
||||
->setPayload($payload);
|
||||
|
||||
$queueForRealtime->from($queueForEvents)->trigger();
|
||||
$queueForFunctions->from($queueForEvents)->trigger();
|
||||
$queueForWebhooks->from($queueForEvents)->trigger();
|
||||
}
|
||||
|
||||
$queueForEvents->reset();
|
||||
$queueForRealtime->reset();
|
||||
$queueForFunctions->reset();
|
||||
$queueForWebhooks->reset();
|
||||
}
|
||||
}
|
||||
|
||||
if ($rollback) {
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'failed'])
|
||||
));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle create operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param string|null $documentId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleCreateOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
?string $documentId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
if ($documentId && !isset($data['$id'])) {
|
||||
$data['$id'] = $documentId;
|
||||
}
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) {
|
||||
$doc = $dbForProject->createDocument(
|
||||
$collectionId,
|
||||
new Document($data),
|
||||
);
|
||||
$state[$collectionId][$doc->getId()] = $doc;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param string $documentId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleUpdateOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->updateDocument(
|
||||
$collectionId,
|
||||
$documentId,
|
||||
new Document($data),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) {
|
||||
$document = $dbForProject->updateDocument(
|
||||
$collectionId,
|
||||
$documentId,
|
||||
new Document($data),
|
||||
);
|
||||
if ($document->isEmpty()) {
|
||||
throw new NotFoundException('');
|
||||
}
|
||||
$state[$collectionId][$documentId] = $document;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle upsert operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param string|null $documentId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleUpsertOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
?string $documentId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
|
||||
if ($dependent) {
|
||||
// Merge partial upsert data with full document from transaction state
|
||||
$existingDoc = $state[$collectionId][$documentId];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
$existingDoc->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$state[$collectionId][$documentId] = $dbForProject->upsertDocument(
|
||||
$collectionId,
|
||||
$existingDoc,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) {
|
||||
$doc = $dbForProject->upsertDocument(
|
||||
$collectionId,
|
||||
new Document($data),
|
||||
);
|
||||
$state[$collectionId][$doc->getId()] = $doc;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delete operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param string $documentId
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function handleDeleteOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
|
||||
if ($dependent) {
|
||||
$dbForProject->deleteDocument($collectionId, $documentId);
|
||||
unset($state[$collectionId][$documentId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, &$state) {
|
||||
$deleted = $dbForProject->deleteDocument($collectionId, $documentId);
|
||||
if (!$deleted) {
|
||||
throw new NotFoundException('');
|
||||
}
|
||||
if (isset($state[$collectionId][$documentId])) {
|
||||
unset($state[$collectionId][$documentId]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attribute/column name from data, with fallback for cross-API compatibility
|
||||
*
|
||||
* @param array $data The operation data
|
||||
* @return string The attribute/column name
|
||||
*/
|
||||
private function getAttributeNameFromData(array $data): string
|
||||
{
|
||||
$expectedKey = $this->getAttributeKey();
|
||||
if (isset($data[$expectedKey])) {
|
||||
return $data[$expectedKey];
|
||||
}
|
||||
|
||||
// Try the opposite key for cross-API compatibility
|
||||
$fallbackKey = $expectedKey === 'attribute' ? 'column' : 'attribute';
|
||||
if (isset($data[$fallbackKey])) {
|
||||
return $data[$fallbackKey];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle increment operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param string $documentId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleIncrementOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
$attribute = $this->getAttributeNameFromData($data);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
max: $data['max'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state, $attribute) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
max: $data['max'] ?? null
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle decrement operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param string $documentId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleDecrementOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
$attribute = $this->getAttributeNameFromData($data);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
min: $data['min'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state, $attribute) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
min: $data['min'] ?? null
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk create operation
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param string $collectionId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return int Number of documents created
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleBulkCreateOperation(
|
||||
Database $dbForProject,
|
||||
string $collectionId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): int {
|
||||
$count = 0;
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state, &$count) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
|
||||
$count = $dbForProject->createDocuments(
|
||||
$collectionId,
|
||||
$documents,
|
||||
onNext: function (Document $document) use (&$state, $collectionId) {
|
||||
$state[$collectionId][$document->getId()] = $document;
|
||||
}
|
||||
);
|
||||
});
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk update operation with manual timestamp checking
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param TransactionState $transactionState
|
||||
* @param string $collectionId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return int Number of documents updated
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
* @throws ConflictException
|
||||
*/
|
||||
private function handleBulkUpdateOperation(
|
||||
Database $dbForProject,
|
||||
TransactionState $transactionState,
|
||||
string $collectionId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): int {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
$updateData = new Document($data['data']);
|
||||
|
||||
$dependentDocs = [];
|
||||
|
||||
$transactionState->applyBulkUpdateToState($collectionId, $updateData, $queries, $state);
|
||||
|
||||
// Clone the document before passing to updateDocuments to prevent mutation
|
||||
// The database layer mutates the input document, which would corrupt transaction state
|
||||
$count = $dbForProject->updateDocuments(
|
||||
$collectionId,
|
||||
clone $updateData,
|
||||
$queries,
|
||||
onNext: function (Document $updated, Document $old) use (&$state, $collectionId, $createdAt, &$dependentDocs) {
|
||||
$dependent = isset($state[$collectionId][$updated->getId()]);
|
||||
|
||||
if ($dependent) {
|
||||
$dependentDocs[] = $updated->getId();
|
||||
} else {
|
||||
$oldUpdatedAt = new \DateTime($old->getUpdatedAt());
|
||||
if ($oldUpdatedAt > $createdAt) {
|
||||
throw new ConflictException('Document was updated after the request timestamp');
|
||||
}
|
||||
$state[$collectionId][$updated->getId()] = $updated;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Re-write dependent documents from state to database to fix partial updates
|
||||
if (!empty($dependentDocs)) {
|
||||
$documentsToRewrite = [];
|
||||
foreach ($dependentDocs as $docId) {
|
||||
if (isset($state[$collectionId][$docId])) {
|
||||
$documentsToRewrite[] = $state[$collectionId][$docId];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($documentsToRewrite)) {
|
||||
$dbForProject->upsertDocuments(
|
||||
$collectionId,
|
||||
$documentsToRewrite,
|
||||
onNext: function (Document $upserted) use (&$state, $collectionId) {
|
||||
$state[$collectionId][$upserted->getId()] = $upserted;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk upsert operation with manual timestamp checking
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param TransactionState $transactionState
|
||||
* @param string $collectionId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return int Number of documents upserted
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleBulkUpsertOperation(
|
||||
Database $dbForProject,
|
||||
TransactionState $transactionState,
|
||||
string $collectionId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): int {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
|
||||
$mergedDocuments = $transactionState->applyBulkUpsertToState($collectionId, $documents, $state);
|
||||
|
||||
$count = $dbForProject->upsertDocuments(
|
||||
$collectionId,
|
||||
$mergedDocuments,
|
||||
onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) {
|
||||
if ($old !== null) {
|
||||
$dependent = isset($state[$collectionId][$upserted->getId()]);
|
||||
|
||||
if (!$dependent) {
|
||||
$oldUpdatedAt = new \DateTime($old->getUpdatedAt());
|
||||
if ($oldUpdatedAt > $createdAt) {
|
||||
throw new ConflictException('Document was updated after the request timestamp');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$state[$collectionId][$upserted->getId()] = $upserted;
|
||||
}
|
||||
);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk delete operation with manual timestamp checking
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param TransactionState $transactionState
|
||||
* @param string $collectionId
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return int Number of documents deleted
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleBulkDeleteOperation(
|
||||
Database $dbForProject,
|
||||
TransactionState $transactionState,
|
||||
string $collectionId,
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): int {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
|
||||
$count = $dbForProject->deleteDocuments(
|
||||
$collectionId,
|
||||
$queries,
|
||||
onNext: function (Document $deleted, Document $old) use (&$state, $collectionId, $createdAt) {
|
||||
$dependent = isset($state[$collectionId][$deleted->getId()]);
|
||||
|
||||
if (!$dependent) {
|
||||
$oldUpdatedAt = new \DateTime($old->getUpdatedAt());
|
||||
if ($oldUpdatedAt > $createdAt) {
|
||||
throw new ConflictException('Document was updated after the transaction operation');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($state[$collectionId][$deleted->getId()])) {
|
||||
unset($state[$collectionId][$deleted->getId()]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$transactionState->applyBulkDeleteToState($collectionId, $queries, $state);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Transactions;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDatabasesTransactions';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION_LIST;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/databases/transactions')
|
||||
->desc('List transactions')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'databases',
|
||||
group: 'transactions',
|
||||
name: 'listTransactions',
|
||||
description: '/docs/references/databases/list-transactions.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: UtopiaResponse::MODEL_TRANSACTION_LIST,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('queries', [], new Transactions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(array $queries, UtopiaResponse $response, Database $dbForProject): void
|
||||
{
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'transactions' => $dbForProject->find('transactions', $queries),
|
||||
'total' => $dbForProject->count('transactions', $queries),
|
||||
]), UtopiaResponse::MODEL_TRANSACTION_LIST);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -37,8 +37,8 @@ class Create extends BooleanCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-boolean-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -50,7 +50,7 @@ class Create extends BooleanCreate
|
||||
]
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Boolean(), 'Default value for column when not provided. Cannot be set when column is required.', true)
|
||||
|
||||
+3
-3
@@ -39,8 +39,8 @@ class Update extends BooleanUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-boolean-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -53,7 +53,7 @@ class Update extends BooleanUpdate
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Boolean()), 'Default value for column when not provided. Cannot be set when column is required.')
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ class Create extends DatetimeCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-datetime-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ class Update extends DatetimeUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-datetime-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -38,8 +38,8 @@ class Delete extends AttributesDelete
|
||||
->label('audits.event', 'column.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/delete-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ class Create extends EmailCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-email-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ class Update extends EmailUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-email-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ class Create extends EnumCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-enum-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ class Update extends EnumUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-enum-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ class Create extends FloatCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-float-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ class Update extends FloatUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-float-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -44,8 +44,8 @@ class Get extends AttributesGet
|
||||
->label('scope', ['tables.read', 'collections.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/get-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -38,8 +38,8 @@ class Create extends IPCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-ip-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -40,8 +40,8 @@ class Update extends IPUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-ip-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ class Create extends IntegerCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-integer-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ class Update extends IntegerUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-integer-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+3
-3
@@ -40,8 +40,8 @@ class Create extends LineCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-line-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -53,7 +53,7 @@ class Create extends LineCreate
|
||||
]
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when column is required.', true)
|
||||
|
||||
+3
-3
@@ -41,8 +41,8 @@ class Update extends LineUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-line-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -55,7 +55,7 @@ class Update extends LineUpdate
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when column is required.', true)
|
||||
|
||||
+3
-3
@@ -40,8 +40,8 @@ class Create extends PointCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-point-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -53,7 +53,7 @@ class Create extends PointCreate
|
||||
]
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when column is required.', true)
|
||||
|
||||
+3
-3
@@ -41,8 +41,8 @@ class Update extends PointUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-point-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -55,7 +55,7 @@ class Update extends PointUpdate
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when column is required.', true)
|
||||
|
||||
+3
-3
@@ -40,8 +40,8 @@ class Create extends PolygonCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-polygon-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -53,7 +53,7 @@ class Create extends PolygonCreate
|
||||
]
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when column is required.', true)
|
||||
|
||||
+3
-3
@@ -41,8 +41,8 @@ class Update extends PolygonUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-polygon-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -55,7 +55,7 @@ class Update extends PolygonUpdate
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when column is required.', true)
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ class Create extends RelationshipCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-relationship-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ class Update extends RelationshipUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-relationship-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
+3
-3
@@ -40,8 +40,8 @@ class Create extends StringCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-string-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -53,7 +53,7 @@ class Create extends StringCreate
|
||||
]
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Validator::TYPE_INTEGER), 'Column size for text columns, in number of characters.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
|
||||
+3
-3
@@ -42,8 +42,8 @@ class Update extends StringUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-string-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
@@ -56,7 +56,7 @@ class Update extends StringUpdate
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).')
|
||||
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
|
||||
->param('key', '', new Key(), 'Column Key.')
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('default', null, new Nullable(new Text(0, 0)), 'Default value for column when not provided. Cannot be set when column is required.')
|
||||
|
||||
@@ -38,8 +38,8 @@ class Create extends URLCreate
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-url-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -40,8 +40,8 @@ class Update extends URLUpdate
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-url-column.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -33,8 +33,8 @@ class XList extends AttributesXList
|
||||
->label('scope', ['tables.read', 'collections.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
group: $this->getSdkGroup(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/list-columns.md',
|
||||
auth: [AuthType::KEY],
|
||||
|
||||
@@ -40,7 +40,7 @@ class Create extends CollectionCreate
|
||||
->label('audits.event', 'table.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: 'tables',
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-table.md',
|
||||
|
||||
@@ -36,7 +36,7 @@ class Delete extends CollectionDelete
|
||||
->label('audits.event', 'table.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSdkNamespace(),
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: 'tables',
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/delete-table.md',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user