1
0
Fork 0
mirror of synced 2024-05-20 12:42:39 +12:00

fix realtime

This commit is contained in:
Torsten Dittmann 2021-09-30 13:18:50 +02:00
parent aa5d4cfc9b
commit 80e0df8a69
9 changed files with 159 additions and 185 deletions

View file

@ -1,10 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
use Appwrite\Database\Database;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
@ -18,7 +14,10 @@ use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Swoole\Request;
use Utopia\WebSocket\Server;
use Utopia\WebSocket\Adapter;
@ -51,116 +50,114 @@ $server = new Server($adapter);
$server->onStart(function () use ($stats, $register, $containerId, &$documentId) {
Console::success('Server started succefully');
$getConsoleDb = function () use ($register) {
$db = $register->get('dbPool')->get();
$cache = $register->get('redisPool')->get();
// $getConsoleDb = function () use ($register) {
// $db = $register->get('dbPool')->get();
// $cache = $register->get('redisPool')->get();
$consoleDb = new Database();
$consoleDb->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
$consoleDb->setNamespace('app_console');
$consoleDb->setMocks(Config::getParam('collections', []));
// $cache = new Cache(new RedisCache($cache));
// $database = new Database(new MariaDB($db), $cache);
return [
$consoleDb,
function () use ($register, $db, $cache) {
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($cache);
}
];
};
// return [
// $database,
// function () use ($register, $db, $cache) {
// $register->get('dbPool')->put($db);
// $register->get('redisPool')->put($cache);
// }
// ];
// };
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($getConsoleDb, $containerId, &$documentId) {
try {
[$consoleDb, $returnConsoleDb] = call_user_func($getConsoleDb);
$document = [
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
'$permissions' => [
'read' => ['*'],
'write' => ['*'],
],
'container' => $containerId,
'timestamp' => time(),
'value' => '{}'
];
Authorization::disable();
$document = $consoleDb->createDocument($document);
Authorization::enable();
$documentId = $document->getId();
} catch (\Throwable $th) {
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
} finally {
call_user_func($returnConsoleDb);
}
});
// go(function () use ($getConsoleDb, $containerId, &$documentId) {
// try {
// [$consoleDb, $returnConsoleDb] = call_user_func($getConsoleDb);
// // $document = [
// // '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
// // '$permissions' => [
// // 'read' => ['*'],
// // 'write' => ['*'],
// // ],
// // 'container' => $containerId,
// // 'timestamp' => time(),
// // 'value' => '{}'
// // ];
// // Authorization::disable();
// // $document = $consoleDb->createDocument($document);
// // Authorization::enable();
// // $documentId = $document->getId();
// } catch (\Throwable $th) {
// Console::error('[Error] Type: ' . get_class($th));
// Console::error('[Error] Message: ' . $th->getMessage());
// Console::error('[Error] File: ' . $th->getFile());
// Console::error('[Error] Line: ' . $th->getLine());
// } finally {
// call_user_func($returnConsoleDb);
// }
// });
/**
* Save current connections to the Database every 5 seconds.
*/
Timer::tick(5000, function () use ($stats, $getConsoleDb, $containerId, &$documentId) {
foreach ($stats as $projectId => $value) {
if (empty($value['connections']) && empty($value['messages'])) {
continue;
}
// /**
// * Save current connections to the Database every 5 seconds.
// */
// Timer::tick(5000, function () use ($stats, $getConsoleDb, $containerId, &$documentId) {
// foreach ($stats as $projectId => $value) {
// if (empty($value['connections']) && empty($value['messages'])) {
// continue;
// }
$connections = $stats->get($projectId, 'connections');
$messages = $stats->get($projectId, 'messages');
// $connections = $stats->get($projectId, 'connections');
// $messages = $stats->get($projectId, 'messages');
$usage = new Event('v1-usage', 'UsageV1');
$usage
->setParam('projectId', $projectId)
->setParam('realtimeConnections', $connections)
->setParam('realtimeMessages', $messages)
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0);
// $usage = new Event('v1-usage', 'UsageV1');
// $usage
// ->setParam('projectId', $projectId)
// ->setParam('realtimeConnections', $connections)
// ->setParam('realtimeMessages', $messages)
// ->setParam('networkRequestSize', 0)
// ->setParam('networkResponseSize', 0);
$stats->set($projectId, [
'messages' => 0,
'connections' => 0
]);
// $stats->set($projectId, [
// 'messages' => 0,
// 'connections' => 0
// ]);
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$usage->trigger();
}
}
$payload = [];
foreach ($stats as $projectId => $value) {
if (!empty($value['connectionsTotal'])) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
}
}
if (empty($payload)) {
return;
}
// if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
// $usage->trigger();
// }
// }
// $payload = [];
// foreach ($stats as $projectId => $value) {
// if (!empty($value['connectionsTotal'])) {
// $payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
// }
// }
// if (empty($payload)) {
// return;
// }
try {
[$consoleDb, $returnConsoleDb] = call_user_func($getConsoleDb);
// try {
// [$consoleDb, $returnConsoleDb] = call_user_func($getConsoleDb);
$consoleDb->updateDocument([
'$id' => $documentId,
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
'$permissions' => [
'read' => ['*'],
'write' => ['*'],
],
'container' => $containerId,
'timestamp' => time(),
'value' => json_encode($payload)
]);
} catch (\Throwable $th) {
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
} finally {
call_user_func($returnConsoleDb);
}
});
// // $consoleDb->updateDocument([
// // '$id' => $documentId,
// // '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
// // '$permissions' => [
// // 'read' => ['*'],
// // 'write' => ['*'],
// // ],
// // 'container' => $containerId,
// // 'timestamp' => time(),
// // 'value' => json_encode($payload)
// // ]);
// } catch (\Throwable $th) {
// Console::error('[Error] Type: ' . get_class($th));
// Console::error('[Error] Message: ' . $th->getMessage());
// Console::error('[Error] File: ' . $th->getFile());
// Console::error('[Error] Line: ' . $th->getLine());
// } finally {
// call_user_func($returnConsoleDb);
// }
// });
});
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
@ -175,20 +172,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
*/
if ($realtime->hasSubscriber('console', 'role:member', 'project')) {
$db = $register->get('dbPool')->get();
$cache = $register->get('redisPool')->get();
$redis = $register->get('redisPool')->get();
$consoleDb = new Database();
$consoleDb->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
$consoleDb->setNamespace('app_console');
$consoleDb->setMocks(Config::getParam('collections', []));
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setNamespace('project_console_internal');
$payload = [];
$list = $consoleDb->getCollection([
'filters' => [
'$collection=' . Database::SYSTEM_COLLECTION_CONNECTIONS,
'timestamp>' . (time() - 15)
],
]);
$list = [];
// $list = $consoleDb->getCollection([
// 'filters' => [
// '$collection=' . Database::SYSTEM_COLLECTION_CONNECTIONS,
// 'timestamp>' . (time() - 15)
// ],
// ]);
/**
* Aggregate stats across containers.
@ -228,7 +225,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($cache);
$register->get('redisPool')->put($redis);
}
/**
* Sending test message for SDK E2E tests every 5 seconds.
@ -290,12 +287,11 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$db = $register->get('dbPool')->get();
$cache = $register->get('redisPool')->get();
$projectDB = new Database();
$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
$projectDB->setNamespace('app_' . $projectId);
$projectDB->setMocks(Config::getParam('collections', []));
$cache = new Cache(new RedisCache($cache));
$database = new Database(new MariaDB($db), $cache);
$database->setNamespace('project_' . $projectId .'_internal');
$user = $projectDB->getDocument($userId);
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
@ -367,15 +363,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
});
try {
/** @var \Appwrite\Database\Document $user */
/** @var \Utopia\Database\Document $user */
$user = $app->getResource('user');
/** @var \Appwrite\Database\Document $project */
/** @var \Utopia\Database\Document $project */
$project = $app->getResource('project');
/** @var \Appwrite\Database\Document $console */
/** @var \Utopia\Database\Document $console */
$console = $app->getResource('console');
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setNamespace('project_' . $project->getId() .'_internal');
/*
* Project Check
*/
@ -388,17 +388,17 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
*
* Abuse limits are connecting 128 times per minute and ip address.
*/
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $db);
$timeLimit
->setNamespace('app_' . $project->getId())
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getURI());
// $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $database);
// $timeLimit
// ->setParam('{ip}', $request->getIP())
// ->setParam('{url}', $request->getURI())
// ->setup();
$abuse = new Abuse($timeLimit);
// $abuse = new Abuse($timeLimit);
if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
throw new Exception('Too many requests', 1013);
}
// if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
// throw new Exception('Too many requests', 1013);
// }
/*
* Validate Client Domain - Check to avoid CSRF attack.
@ -457,6 +457,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Console::error('[Error] Connection Error');
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
var_dump($th->getFile(), $th->getLine(), $th->getTraceAsString());
}
if ($th instanceof PDOException) {
@ -477,27 +478,26 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$db = $register->get('dbPool')->get();
$cache = $register->get('redisPool')->get();
$projectDB = new Database();
$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
$projectDB->setNamespace('app_' . $realtime->connections[$connection]['projectId']);
$projectDB->setMocks(Config::getParam('collections', []));
$cache = new Cache(new RedisCache($cache));
$database = new Database(new MariaDB($db), $cache);
$database->setNamespace('project_' . $realtime->connections[$connection]['projectId'] .'_internal');
/*
* Abuse Check
*
* Abuse limits are sending 32 times per minute and connection.
*/
$timeLimit = new TimeLimit('url:{url},conection:{connection}', 32, 60, $db);
$timeLimit
->setNamespace('app_' . $realtime->connections[$connection]['projectId'])
->setParam('{connection}', $connection)
->setParam('{container}', $containerId);
// $timeLimit = new TimeLimit('url:{url},conection:{connection}', 32, 60, $database);
// $timeLimit
// ->setParam('{connection}', $connection)
// ->setParam('{container}', $containerId)
// ->setup();
$abuse = new Abuse($timeLimit);
// $abuse = new Abuse($timeLimit);
if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
throw new Exception('Too many messages', 1013);
}
// if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
// throw new Exception('Too many messages', 1013);
// }
$message = json_decode($message, true);
@ -518,11 +518,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
Auth::$unique = $session['id'];
Auth::$secret = $session['secret'];
$user = $projectDB->getDocument(Auth::$unique);
$user = $database->getDocument('users', Auth::$unique);
if (
empty($user->getId()) // Check a document has been found in the DB
|| Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
) {
// cookie not valid

View file

@ -12,8 +12,6 @@ Console::success(APP_NAME.' database worker v1 has started'."\n");
class DatabaseV1 extends Worker
{
public $args = [];
public function init(): void
{
}

View file

@ -18,11 +18,6 @@ Console::success(APP_NAME . ' deletes worker v1 has started' . "\n");
class DeletesV1 extends Worker
{
/**
* @var array
*/
public $args = [];
/**
* @var Database
*/

View file

@ -170,32 +170,6 @@ services:
- _APP_DB_PASS
- _APP_USAGE_STATS
appwrite-worker-usage:
entrypoint: worker-usage
container_name: appwrite-worker-usage
build:
context: .
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- telegraf
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_STATSD_HOST
- _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-worker-audits:
entrypoint: worker-audits
container_name: appwrite-worker-audits

View file

@ -11,7 +11,8 @@ this.realtime.socket=new WebSocket(this.config.endpointRealtime+'/realtime?'+cha
this.realtime.socket.addEventListener('close',event=>{var _a,_b,_c;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.type)==='error'&&((_c=this.realtime)===null||_c===void 0?void 0:_c.lastMessage.data).code===1008){return;}
console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},authenticate:(event)=>{var _a,_b,_c;const message=JSON.parse(event.data);if(message.type==='connected'&&((_a=this.realtime.socket)===null||_a===void 0?void 0:_a.readyState)===WebSocket.OPEN){const cookie=JSON.parse((_b=window.localStorage.getItem('cookieFallback'))!==null&&_b!==void 0?_b:"{}");const session=cookie===null||cookie===void 0?void 0:cookie[`a_session_${this.config.project}`];const data=message.data;if(session&&!data.user){(_c=this.realtime.socket)===null||_c===void 0?void 0:_c.send(JSON.stringify({type:"authentication",data:{session}}));}}},onMessage:(channel,callback)=>(event)=>{try{const message=JSON.parse(event.data);this.realtime.lastMessage=message;if(message.type==='event'){let data=message.data;if(data.channels&&data.channels.includes(channel)){callback(data);}}
else if(message.type==='error'){throw message.data;}}
catch(e){console.error(e);}}};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
catch(e){console.error(e);}}};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(userId,email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/account';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
@ -2353,7 +2354,10 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim
return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("runtimeName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;}
return'';}).add("runtimeLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;}
return'';}).add("runtimeVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;}
return'';}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);}
return'';}).add("indexAttributes",function($value){let output='';for(let i=0;i<$value.attributes.length;i++){output+=$value.attributes[i]+' ('+$value.orders[i]+'), '}
return output.slice(0,-2);}).add("collectionAttributes",function($value){if(!Array.isArray($value)){return[];}
$value.unshift({$id:'$id'});return $value;}).add("documentAttribute",function($value,attribute){if($value[attribute.key]){return $value[attribute.key];}
return null;}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);}
let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";}
return annotate(number,maxPlaces,forcePlaces,abbr);}
function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;}

View file

@ -11,7 +11,8 @@ this.realtime.socket=new WebSocket(this.config.endpointRealtime+'/realtime?'+cha
this.realtime.socket.addEventListener('close',event=>{var _a,_b,_c;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.type)==='error'&&((_c=this.realtime)===null||_c===void 0?void 0:_c.lastMessage.data).code===1008){return;}
console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},authenticate:(event)=>{var _a,_b,_c;const message=JSON.parse(event.data);if(message.type==='connected'&&((_a=this.realtime.socket)===null||_a===void 0?void 0:_a.readyState)===WebSocket.OPEN){const cookie=JSON.parse((_b=window.localStorage.getItem('cookieFallback'))!==null&&_b!==void 0?_b:"{}");const session=cookie===null||cookie===void 0?void 0:cookie[`a_session_${this.config.project}`];const data=message.data;if(session&&!data.user){(_c=this.realtime.socket)===null||_c===void 0?void 0:_c.send(JSON.stringify({type:"authentication",data:{session}}));}}},onMessage:(channel,callback)=>(event)=>{try{const message=JSON.parse(event.data);this.realtime.lastMessage=message;if(message.type==='event'){let data=message.data;if(data.channels&&data.channels.includes(channel)){callback(data);}}
else if(message.type==='error'){throw message.data;}}
catch(e){console.error(e);}}};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
catch(e){console.error(e);}}};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(userId,email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/account';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}

View file

@ -300,7 +300,10 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim
return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("runtimeName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;}
return'';}).add("runtimeLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;}
return'';}).add("runtimeVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;}
return'';}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);}
return'';}).add("indexAttributes",function($value){let output='';for(let i=0;i<$value.attributes.length;i++){output+=$value.attributes[i]+' ('+$value.orders[i]+'), '}
return output.slice(0,-2);}).add("collectionAttributes",function($value){if(!Array.isArray($value)){return[];}
$value.unshift({$id:'$id'});return $value;}).add("documentAttribute",function($value,attribute){if($value[attribute.key]){return $value[attribute.key];}
return null;}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);}
let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";}
return annotate(number,maxPlaces,forcePlaces,abbr);}
function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long