1
0
Fork 0
mirror of synced 2024-05-05 21:32:42 +12:00
appwrite/app/workers/databases.php
Damodar Lohani 8f14f5aa21
Database layer (#3338)
* database response model

* database collection config

* new database scopes

* database service update

* database execption codes

* remove read write permission from database model

* updating tests and fixing some bugs

* server side tests are now passing

* databases api

* tests for database endpoint

* composer update

* fix error

* formatting

* formatting fixes

* get database test

* more updates to events and usage

* more usage updates

* fix delete type

* fix test

* delete database

* more fixes

* databaseId in attributes and indexes

* more fixes

* fix issues

* fix index subquery

* fix console scope and index query

* updating tests as required

* fix phpcs errors and warnings

* updates to review suggestions

* UI progress

* ui updates and cleaning up

* fix type

* rework database events

* update tests

* update types

* event generation fixed

* events config updated

* updating context to support multiple

* realtime updates

* fix ids

* update context

* validator updates

* fix naming conflict

* fix tests

* fix lint errors

* fix wprler and realtime tests

* fix webhooks test

* fix event validator and other tests

* formatting fixes

* removing leftover var_dumps

* remove leftover comment

* update usage params

* usage metrics updates

* update database usage

* fix usage

* specs update

* updates to usage

* fix UI and usage

* fix lints

* internal id fixes

* fixes for internal Id

* renaming services and related files

* rename tests

* rename doc link

* rename readme

* fix test name

* tests: fixes for 0.15.x sync

Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
2022-06-22 12:51:49 +02:00

350 lines
14 KiB
PHP

<?php
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Utopia\CLI\Console;
use Utopia\Database\Document;
require_once __DIR__ . '/../init.php';
Console::title('Database V1 Worker');
Console::success(APP_NAME . ' database worker v1 has started' . "\n");
class DatabaseV1 extends Worker
{
public function init(): void
{
}
public function run(): void
{
$type = $this->args['type'];
$project = new Document($this->args['project']);
$collection = new Document($this->args['collection'] ?? []);
$document = new Document($this->args['document'] ?? []);
$database = new Document($this->args['database'] ?? []);
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
}
if ($document->isEmpty()) {
throw new Exception('Missing document');
}
switch (strval($type)) {
case DATABASE_TYPE_CREATE_ATTRIBUTE:
$this->createAttribute($database, $collection, $document, $project->getId());
break;
case DATABASE_TYPE_DELETE_ATTRIBUTE:
$this->deleteAttribute($database, $collection, $document, $project->getId());
break;
case DATABASE_TYPE_CREATE_INDEX:
$this->createIndex($database, $collection, $document, $project->getId());
break;
case DATABASE_TYPE_DELETE_INDEX:
$this->deleteIndex($database, $collection, $document, $project->getId());
break;
default:
Console::error('No database operation for type: ' . $type);
break;
}
}
public function shutdown(): void
{
}
/**
* @param Document $database
* @param Document $collection
* @param Document $attribute
* @param string $projectId
*/
protected function createAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].update', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
/**
* Fetch attribute from the database, since with Resque float values are loosing informations.
*/
$attribute = $dbForProject->getDocument('attributes', $attribute->getId());
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$type = $attribute->getAttribute('type', '');
$size = $attribute->getAttribute('size', 0);
$required = $attribute->getAttribute('required', false);
$default = $attribute->getAttribute('default', null);
$signed = $attribute->getAttribute('signed', true);
$array = $attribute->getAttribute('array', false);
$format = $attribute->getAttribute('format', '');
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []);
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
throw new Exception('Failed to create Attribute');
}
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $attribute,
project: $project,
);
Realtime::send(
projectId: 'console',
payload: $attribute->getArrayCopy(),
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
}
/**
* @param Document $database
* @param Document $collection
* @param Document $attribute
* @param string $projectId
*/
protected function deleteAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
$project = $dbForConsole->getDocument('projects', $projectId);
// possible states at this point:
// - available: should not land in queue; controller flips these to 'deleting'
// - processing: hasn't finished creating
// - deleting: was available, in deletion queue for first time
// - failed: attribute was never created
// - stuck: attribute was available but cannot be removed
try {
if ($status !== 'failed' && !$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete Attribute');
}
$dbForProject->deleteDocument('attributes', $attribute->getId());
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $attribute,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $attribute->getArrayCopy(),
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
// The underlying database removes/rebuilds indexes when attribute is removed
// Update indexes table with changes
/** @var Document[] $indexes */
$indexes = $collection->getAttribute('indexes', []);
foreach ($indexes as $index) {
/** @var string[] $attributes */
$attributes = $index->getAttribute('attributes');
$lengths = $index->getAttribute('lengths');
$orders = $index->getAttribute('orders');
$found = \array_search($key, $attributes);
if ($found !== false) {
// If found, remove entry from attributes, lengths, and orders
// array_values wraps array_diff to reindex array keys
// when found attribute is removed from array
$attributes = \array_values(\array_diff($attributes, [$attributes[$found]]));
$lengths = \array_values(\array_diff($lengths, [$lengths[$found]]));
$orders = \array_values(\array_diff($orders, [$orders[$found]]));
if (empty($attributes)) {
$dbForProject->deleteDocument('indexes', $index->getId());
} else {
$index
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN)
;
// Check if an index exists with the same attributes and orders
$exists = false;
foreach ($indexes as $existing) {
if (
$existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
&& $existing->getAttribute('attributes') === $index->getAttribute('attributes')
&& $existing->getAttribute('orders') === $index->getAttribute('orders')
) {
$exists = true;
break;
}
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($database, $collection, $index, $projectId);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
}
}
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
$dbForProject->deleteCachedCollection('collection_' . $collection->getInternalId());
}
/**
* @param Document $database
* @param Document $collection
* @param Document $index
* @param string $projectId
*/
protected function createIndex(Document $database, Document $collection, Document $index, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].update', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
$collectionId = $collection->getId();
$key = $index->getAttribute('key', '');
$type = $index->getAttribute('type', '');
$attributes = $index->getAttribute('attributes', []);
$lengths = $index->getAttribute('lengths', []);
$orders = $index->getAttribute('orders', []);
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
throw new Exception('Failed to create Index');
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $index,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $index->getArrayCopy(),
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
}
/**
* @param Document $database
* @param Document $collection
* @param Document $index
* @param string $projectId
*/
protected function deleteIndex(Document $database, Document $collection, Document $index, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].delete', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete index');
}
$dbForProject->deleteDocument('indexes', $index->getId());
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'stuck'));
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $index,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $index->getArrayCopy(),
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
$dbForProject->deleteCachedDocument('collections', $collection->getId());
}
}