Initial commit

This commit is contained in:
2025-03-12 16:01:40 +01:00
commit 3b31ce9afc
30 changed files with 4193 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/vendor/
/*.phar
/*.yaml

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
.PHONY: help all phar
help:
@echo "\e[1mTargets:\e[0m"
@echo " \e[3mall\e[0m — build everything\n \e[3mphar\e[0m — build the phar"
all: phar
phar:
box compile
mv bin/slotdbd.phar ./slotdbd.phar

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# SlotDB
SlotDB is an application designed to store information about property or assets, to
be able to query on that information, and implement basic schemas.
## Installing
### Docker
```bash
$ docker run -it \
-v $PWD/slotdb.json:/app/slotdb.json \
dev.noccylabs.info/slotdb/slotdb
```
## Configuration
## API
### /api/slotdb/v1/slots [GET]
**Get a list of all slots, or slots matching conditions**
```json
GET /api/slotdb/v1/slots
HTTP 200 OK
Content-Type: application/json
{
"slots": [
{
"id": "1.1",
"name": "First thing",
"available": true
}
]
}
```
```json
GET /api/slotdb/v1/slots?k=id&available.eq=true
HTTP 200 OK
Content-Type: application/json
{
"slots": [
{
"id": "1.1"
},
{
"id": "1.2"
}
]
}
```
| Params | Value
| ------------- | --------
| p | Page
| k | Comma-separated list of keys to return
| {key}.{cond} | Match condition
### /api/slotdb/v1/slots [POST]
**Create one or more new slots**
### /api/slotdb/v1/slot/{slot} [GET]
### /api/slotdb/v1/slot/{slot} [PUT]
### /api/slotdb/v1/slot/{slot} [DELETE]

4
bin/slotdbd Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require_once __DIR__."/../src/bootstrap.php";

30
composer.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "slotdb/slotdb",
"description": "A key-value store for fixed inventory or assets",
"type": "application",
"license": "GPL-2.0-or-later",
"autoload": {
"psr-4": {
"SlotDb\\SlotDb\\": "src/"
}
},
"authors": [
{
"name": "Christopher Vagnetoft",
"email": "labs@noccy.com"
}
],
"require": {
"react/react": "^1.4",
"noccylabs/react-http": "^0.2.5",
"doctrine/dbal": "^4.2",
"doctrine/orm": "^3.3",
"symfony/cache": "^7.2",
"psr/log": "^3.0",
"monolog/monolog": "^3.8",
"scienta/doctrine-json-functions": "^6.3"
},
"bin": [
"bin/slotdbd"
]
}

3028
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

110
doc/slotdb.openapi.yaml Normal file
View File

@@ -0,0 +1,110 @@
openapi: '3.0.3'
info:
title: SlotDB
version: '1.0'
servers:
- url: http://127.0.0.1:8080/api/slotdb/v1
paths:
/slots:
get:
description: Query all slots
tags: [ Slots ]
parameters:
- in: query
name: view
description: Comma-separated list of keys to return
schema: { type: string }
- in: query
name: group
description: Comma-separated list of groups to filter on
schema: { type: string }
- in: query
name: limit
description: Number of results per page
schema: { type: number }
- in: query
name: page
description: Page number (from 0)
schema: { type: number }
- in: query
name: match
description: Match expression for returned results
schema: { type: string }
responses:
'200':
description: OK
post:
description: Create or update slots
tags: [ Slots ]
requestBody:
description: Array of slots
required: true
content:
application/json:
schema:
type: array
items:
type: object
required: [ id ]
properties:
id: { type: string, description: "Unique slot ID", example: "4B-1" }
name: { type: string, description: "Like ID, but for humans", example: "4B Unit 1" }
properties: { type: object, additionalProperties: true, example: { is-available: true, monthly-rate: 191 } }
group: { type: string, description: "The group that this slot belongs to", example: "units" }
responses:
'201':
description: Slots created
'409':
description: Slot already exists
'422':
description: Unprocessable content
/slot/{slot}:
get:
tags: [ Slots ]
responses:
'200':
description: OK
post:
tags: [ Slots ]
responses:
'200':
description: OK
delete:
tags: [ Slots ]
responses:
'200':
description: OK
/slot/{slot}/{key}:
get:
description: Query a specific property from a slot
tags: [ Slots ]
responses:
'200':
description: OK
/groups:
get:
tags: [ Groups ]
responses:
'200':
description: OK
post:
tags: [ Groups ]
responses:
'200':
description: OK
/group/{group}:
get:
tags: [ Groups ]
responses:
'200':
description: OK
post:
tags: [ Groups ]
responses:
'200':
description: OK
delete:
tags: [ Groups ]
responses:
'200':
description: OK

14
src/Bus/Message.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace SlotDb\SlotDb\Bus;
class Message
{
public function __construct(
public readonly string $message,
public readonly array $data,
)
{
}
}

38
src/Bus/MessageBus.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace SlotDb\SlotDb\Bus;
class MessageBus
{
private array $subscribers = [];
private array $queue = [];
public function onMessage(string $message, callable $handler) {
if (!isset($this->subscribers[$message])) {
$this->subscribers[$message] = [];
}
$this->subscribers[$message][] = $handler;
}
public function pushMessage(Message $message): void
{
$this->queue[] = $message;
}
public function handle(): void
{
while ($message = array_shift($this->queue)) {
if (isset($this->subscribers[$message->message])) {
foreach ($this->subscribers[$message->message] as $handler) {
call_user_func($handler, $message);
}
}
}
}
public function push(string $message, array $data): void
{
$this->pushMessage(new Message($message, $data));
}
}

79
src/Daemon.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace SlotDb\SlotDb;
use Doctrine\DBAL\DriverManager;
use NoccyLabs\React\Http\Handler\RequestHandler;
use NoccyLabs\React\Http\Middleware\RoutingMiddleware;
use NoccyLabs\React\Http\Routing\RouteCollection;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;
use SlotDb\SlotDb\Bus\MessageBus;
use SlotDb\SlotDb\Data\Database;
use SlotDb\SlotDb\Data\Group\Group;
use SlotDb\SlotDb\Data\Slot\Slot;
use SlotDb\SlotDb\Data\Slot\SlotRepository;
class Daemon
{
private string $dataDir = "./var";
private Database $database;
private HttpServer $server;
private LoggerInterface $logger;
private MessageBus $messageBus;
public function __construct(
private readonly string $listen = '0.0.0.0:4949',
?string $dataDir = null,
?LoggerInterface $logger = null,
)
{
$this->logger = $logger ?? new NullLogger();
$this->dataDir = $dataDir ?? $this->dataDir;
$upgradeDb = !file_exists($this->dataDir."/slots.db");
$conn = DriverManager::getConnection([
'driver' => 'pdo_sqlite',
'path' => $this->dataDir."/slots.db",
]);
$this->database = new Database($conn, $logger);
$this->database->updateSchema($upgradeDb);
$em = $this->database->getEntityManager();
$this->messageBus = new MessageBus();
$routes = new RouteCollection();
$routes->addController(new Http\Controller\SlotsController($em->getRepository(Slot::class), $this->messageBus));
$routes->addController(new Http\Controller\GroupsController($em->getRepository(Group::class)));
$this->server = new HttpServer(
new Http\Middleware\LoggingMiddleware(),
new RoutingMiddleware($routes),
new RequestHandler(),
function (ServerRequestInterface $request) {
return Response::plaintext("Not found")->withStatus(404);
}
);
}
public function start(): void
{
$this->logger->info("Starting up...");
$this->server->listen(new SocketServer($this->listen));
$this->logger->debug(sprintf("Listening for HTTP connections on %s", $this->listen));
}
}

61
src/Data/Database.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
namespace SlotDb\SlotDb\Data;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\SimpleArrayType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\SchemaTool;
use Psr\Log\LoggerInterface;
use Scienta\DoctrineJsonFunctions\Query\AST\Functions\Sqlite as DqlFunctions;
class Database
{
private ?EntityManager $em = null;
public function __construct(private Connection $connection, private LoggerInterface $logger)
{
$config = ORMSetup::createAttributeMetadataConfiguration([
dirname(__DIR__)
], true, null, null, false);
$config->addCustomStringFunction(DqlFunctions\JsonExtract::FUNCTION_NAME, DqlFunctions\JsonExtract::class);
// $config->addCustomStringFunction(DqlFunctions\JsonSearch::FUNCTION_NAME, DqlFunctions\JsonSearch::class);
$this->em = new EntityManager($connection, $config);
}
public function getEntityManager(): EntityManager
{
return $this->em;
}
public function updateSchema(bool $create = false): void
{
$tool = new SchemaTool($this->em);
$cmf = new ClassMetadataFactory();
$cmf->setEntityManager($this->em);
$metadata = $cmf->getAllMetadata();
if ($create) {
$queries = $tool->getCreateSchemaSql($metadata);
} else {
$queries = $tool->getUpdateSchemaSql($metadata);
}
if (count($queries) > 0) {
$this->logger->info(sprintf("%s database schema...", $create?"Creating":"Upgrading"));
} else {
$this->logger->debug("Database schema is up-to-date");
}
for($i=0; $i<count($queries);$i++){
$this->logger->debug(sprintf("-- %s", $queries[$i]));
$this->em->getConnection()->prepare($queries[$i])->executeQuery();
}
}
}

57
src/Data/Group/Group.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace SlotDb\SlotDb\Data\Group;
use Doctrine\ORM\Mapping as ORM;
use ArrayIterator;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
#[ORM\Table(name: 'groups')]
#[ORM\Entity(repositoryClass: GroupRepository::class)]
class Group implements IteratorAggregate, JsonSerializable
{
#[ORM\Id()]
#[ORM\GeneratedValue()]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 96, unique: true)]
private string $groupId;
#[ORM\Column(type: 'string', length: 96)]
private string $name;
#[ORM\Column(type: 'simple_array')]
private array $props = [];
public function __construct(
string $groupId,
string $name,
)
{
$this->groupId = $groupId;
$this->name = $name;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->props);
}
public function jsonSerialize(): mixed
{
return [
'id' => $this->id,
'name' => $this->name,
'props' => (object)$this->props,
];
}
public function getChanges(): array
{
return [];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace SlotDb\SlotDb\Data\Group;
use Doctrine\ORM\EntityRepository;
class GroupRepository extends EntityRepository
{
public function createGroup(string $id, ?string $name): Group
{
return new Group(
groupId: $id,
name: $name??$id,
);
}
public function findGroup(string $id): Group
{
$slot = $this->findOneBy([ 'groupId' => $id ]);
return $slot;
}
public function findGroups(): array
{
return [];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace SlotDb\SlotDb\Data\Hook\Action;
// --8<-- Action.php
abstract class Action
{
}
// --8<-- RestAction.php
class RestAction extends Action
{
public function __construct(
public readonly string $url,
public readonly string $method,
public readonly ?string $template,
)
{
}
}

19
src/Data/Hook/Hook.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
namespace SlotDb\SlotDb\Data\Hook;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name:"hooks")]
#[ORM\Entity()]
class Hook
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: "integer")]
private int $id;
#[ORM\Column(type: "string", length: 64, unique: true)]
private string $name;
}

View File

@@ -0,0 +1,29 @@
<?php
namespace SlotDb\SlotDb\Data\Hook\Trigger;
use SlotDb\SlotDb\Bus\Message;
use SlotDb\SlotDb\Bus\MessageBus;
class PropertyTrigger extends Trigger
{
public function __construct(
public readonly string $property,
public readonly ?string $condition,
public readonly mixed $compare,
)
{
}
public function connect(MessageBus $bus): void
{
$bus->onMessage('property.change', function (Message $message) {
// TODO check if $this->property matches $message->data->property
// TODO emit triggered event
$this->emit(self::EVT_TRIGGERED, [ ]);
});
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace SlotDb\SlotDb\Data\Hook\Trigger;
use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait;
use SlotDb\SlotDb\Bus\Message;
use SlotDb\SlotDb\Bus\MessageBus;
abstract class Trigger implements EventEmitterInterface
{
use EventEmitterTrait;
const string EVT_TRIGGERED = 'triggered';
abstract public function connect(MessageBus $bus): void;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace SlotDb\SlotDb\Data\Hook\Trigger;
use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait;
use SlotDb\SlotDb\Bus\Message;
use SlotDb\SlotDb\Bus\MessageBus;
class TriggerFactory
{
private array $triggers = [];
public function createTrigger(string $type, array $options): Trigger
{
$hash = hash('sha256', json_encode([ $type, $options ]));
if (!isset($this->triggers[$hash])) {
$this->triggers[$hash] = match ($type) {
'property' => new PropertyTrigger(...$options),
default => throw new \Exception("InvalidTriggerException"),
};
}
return $this->triggers[$hash];
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace SlotDb\SlotDb\Data\Schema;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'schemas')]
#[ORM\Entity()]
class Schema
{
#[ORM\Id()]
#[ORM\GeneratedValue()]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
}

58
src/Data/Slot/Slot.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace SlotDb\SlotDb\Data\Slot;
use Doctrine\ORM\Mapping as ORM;
use ArrayIterator;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
#[ORM\Table(name: 'slots')]
#[ORM\Entity(repositoryClass: SlotRepository::class)]
class Slot implements IteratorAggregate, JsonSerializable
{
#[ORM\Id()]
#[ORM\GeneratedValue()]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 96, unique: true)]
private string $slotId;
#[ORM\Column(type: 'string', length: 96)]
private string $name;
#[ORM\Column(type: 'json', nullable: true)]
private array $props = [];
public function __construct(
string $slotId,
string $name,
)
{
$this->slotId = $slotId;
$this->name = $name;
$this->props = [];
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->props);
}
public function jsonSerialize(): mixed
{
return [
'id' => $this->id,
'name' => $this->name,
'props' => (object)$this->props,
];
}
public function getChanges(): array
{
return [];
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace SlotDb\SlotDb\Data\Slot;
use Doctrine\ORM\EntityRepository;
class SlotRepository extends EntityRepository
{
public function createSlot(string $id, ?string $name): Slot
{
$slot = new Slot(
slotId: $id,
name: $name??$id,
);
$this->getEntityManager()->persist($slot);
$this->getEntityManager()->flush();
return $slot;
}
public function findSlot(string $id): Slot
{
$slot = $this->findOneBy([ 'slotId' => $id ]);
return $slot;
}
public function findSlots(): array
{
return $this->findAll();
}
public function findSlotsByProps(array $props): array
{
$builder = $this->createQueryBuilder('s');
$v = 0;
foreach ($props as $prop => $value) {
$exprid = "expr".($v);
$valid = "val".($v++);
if (is_array($value)) {
$test = array_shift($value);
$value = array_shift($value);
} else {
$test = 'eq';
}
$cond = match ($test) {
'eq' => '=',
'neq' => '!=',
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<='
};
$match = 'json_extract(s.props,:'.$exprid.')';
$builder->andWhere("{$match} {$cond} :{$valid}")
->setParameter($exprid, '$.'.$prop)
->setParameter($valid, $value);
}
$res = $builder->getQuery()->getResult();
return $res;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace SlotDb\SlotDb\Http\Attribute;
use Psr\Http\Message\ServerRequestInterface;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Route
{
public readonly string $pattern;
public function __construct(
public readonly string $path,
public readonly array $methods,
)
{
}
public function matches(ServerRequestInterface $request): bool
{
if (!in_array($request->getMethod(), $this->methods)) {
return false;
}
if (!preg_match($this->pattern, $request->getUri()->getPath(), $matches)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace SlotDb\SlotDb\Http\Controller;
use Reflection;
use ReflectionClass;
use ReflectionMethod;
use Reflector;
use SlotDb\SlotDb\Http\Attribute\Route;
abstract class Controller
{
public function getRoutes(): array
{
$routes = [];
$rc = new ReflectionClass($this);
foreach ($rc->getMethods() as $method) {
$route = $this->getAttribute($method, Route::class);
if ($route) $routes[] = new class($route, $method->getClosure($this)) {
private $handler;
public function __construct(
public readonly Route $route,
callable $handler
)
{
$this->handler = $handler;
}
public function getPattern(): string
{
return ".+";
}
public function __invoke(...$args)
{
return call_user_func_array($this->handler, $args);
}
};
}
return $routes;
}
private function getAttribute(ReflectionMethod $reflector, string $attribute): mixed
{
$attrs = $reflector->getAttributes($attribute);
if (empty($attrs)) return null;
$attr = reset($attrs);
return $attr->newInstance();
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace SlotDb\SlotDb\Http\Controller;
use NoccyLabs\React\Http\Attributes\Route;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use SlotDb\SlotDb\Data\Group\Group;
use SlotDb\SlotDb\Data\Group\GroupRepository;
class GroupsController extends Controller
{
public function __construct(
private readonly GroupRepository $groups
)
{
}
#[Route(path:"/api/slotdb/v1/groups", methods:["GET"])]
public function findGroups(ServerRequestInterface $request)
{
$slots = $this->groups->findSlots();
return Response::json($slots);
}
#[Route(path:"/api/slotdb/v1/groups", methods:["POST"])]
public function createGroups(ServerRequestInterface $request)
{
$data = json_decode($request->getBody());
$groups = [];
foreach ($data as $item) {
$groupId = $item->id;
$groupName = $item->name??null;
$groups[] = $this->groups->createSlot($groupId, $groupName);
}
return Response::json($groups);
}
#[Route(path:"/api/slotdb/v1/group/{group}", methods:["GET"])]
public function queryGroup(ServerRequestInterface $request, string $group)
{
return Response::json(true);
}
#[Route(path:"/api/slotdb/v1/group/{group}", methods:["POST"])]
public function updateGroup(ServerRequestInterface $request, string $group)
{
$data = $this->groups->findGroup($group);
$posted = json_decode($request->getBody());
foreach ((array)$posted as $prop=>$op) {
}
return Response::json(true);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace SlotDb\SlotDb\Http\Controller;
use NoccyLabs\React\Http\Attributes\Route;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use SlotDb\SlotDb\Bus\MessageBus;
use SlotDb\SlotDb\Data\Slot\Slot;
use SlotDb\SlotDb\Data\Slot\SlotRepository;
class SlotsController extends Controller
{
public function __construct(
private readonly SlotRepository $slots,
private readonly MessageBus $bus,
)
{
}
#[Route(path:"/api/slotdb/v1/slots", methods:["GET"])]
public function findSlots(ServerRequestInterface $request)
{
$wheres = (array)$request->getQueryParams()['where']??[];
if (count($wheres) > 0) {
$props = [];
foreach ($wheres as $where) {
[$k,$v] = explode(":", $where, 2);
$props[$k] = json_decode($v) ?? $v;
}
$slots = $this->slots->findSlotsByProps($props);
} else {
$slots = $this->slots->findSlots();
}
return Response::json($slots);
}
#[Route(path:"/api/slotdb/v1/slots", methods:["POST"])]
public function createSlots(ServerRequestInterface $request)
{
$data = json_decode($request->getBody());
$slots = [];
foreach ($data as $item) {
$slotId = $item->id;
$slotName = $item->name??null;
try {
$slot = $this->slots->createSlot($slotId, $slotName);
$slots[] = $slot;
}
catch (\Exception $e)
{
return Response::plaintext((string)$e)->withStatus(500);
}
}
return Response::json($slots);
}
#[Route(path:"/api/slotdb/v1/slot/{slot}", methods:["GET"])]
public function querySlot(ServerRequestInterface $request, string $slot)
{
return Response::json(true);
}
#[Route(path:"/api/slotdb/v1/slot/{slot}", methods:["POST"])]
public function updateSlot(ServerRequestInterface $request, string $slot)
{
$data = $this->slots->findSlot($slot);
$posted = json_decode($request->getBody());
foreach ((array)$posted as $prop=>$op) {
}
return Response::json(true);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace SlotDb\SlotDb\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
class LoggingMiddleware
{
public function __invoke(ServerRequestInterface $request, ?callable $next = null)
{
try {
$response = $next($request);
if ($response instanceof PromiseInterface) {
$response->then(function ($response) use ($request) {
$this->logRequest($request, $response);
});
} else {
$this->logRequest($request, $response);
}
return $response;
} catch (\Throwable $t) {
fwrite(STDERR, $t."\n");
return Response::plaintext($t)->withStatus(500);
}
}
private function logRequest(ServerRequestInterface $request, ResponseInterface $response): void
{
fprintf(STDERR, "%s [%s] %d\n", $request->getMethod(), $request->getUri(), $response->getStatusCode());
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace SlotDb\SlotDb\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
class RoutingMiddleware
{
private array $routes = [];
private array $controllers = [];
public function __construct(
array $controllers
)
{
foreach ($controllers as $controller) {
$this->controllers[get_class($controller)] = $controller;
$routes = $controller->getRoutes();
foreach ($routes as $route) {
$this->routes[$route->getPattern()] = $route;
}
}
}
public function __invoke(ServerRequestInterface $request, ?callable $next = null)
{
foreach ($this->routes as $route) {
if ($route->route->matches($request)) {
return $route($request);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace SlotDb\SlotDb\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
class SecurityMiddleware
{
public function __invoke(ServerRequestInterface $request, ?callable $next = null)
{
$response = $next($request);
return $response;
}
}

81
src/bootstrap.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
require_once __DIR__."/../vendor/autoload.php";
use SlotDb\SlotDb\Daemon;
$opts = getopt("hl:", [
"help", "listen:", "config:",
"db:", "init-db", "upgrade-db",
"root:", "cert:", "key:",
"jwt-secret:", "jwt-claim:"
]);
function print_usage(): never {
echo <<<HELP
Usage:
slotdbd [options]
Options:
-h,--help Show this help
-l,--listen ADDR Set listen address for HTTP requests (env: SLOTDB_LISTEN)
-c,--config FILE Read configuration from file (env: SLOTDB_CONFIG)
--db URI Use custom database connection URI (env: SLOTDB_DATABASE)
--init-db Initialize database schema
--upgrade-db Upgrade database schema
--root FILE Set root CA certificate for SSL (env: SLOTDB_SSL_ROOT)
--cert FILE Set certificate for SSL (env: SLOTDB_SSL_CERT)
--key FILE Set private key file for SSL (env: SLOTDB_SSL_KEY)
--jwt-secret SECRET Use JWT auth with secret (env: SLOTDB_JWT_SECRET)
--jwt-claim CLAIM Claim key to use for matching properties (env:SLOTDB_JWT_CLAIM)
Defaults:
HTTP listen address: 0.0.0.0:6680
Certificates/key: none
JWT auth: none
JWT Claims:
The key pointed to by the claim option should be a string or an array of
strings, with each string granting access to a group, slot or property.
"*#*:*/r" Read all groups, all slots, all properties
"first#/rw" Read-write everything in the group first
"slot*/rw" Read-write all slots starting with 'slot' in all groups
"slota,slotb/w" Write-only access to slots 'slota' and 'slotb'
"*:available" Read-only access to the 'available' prop on all slots
Examples:
SLOTDB_LISTEN=0.0.0.0:9876 slotdb
slotdb -l 0.0.0.0:9876
Start daemon and listen on 0.0.0.0:9876
SLOTDB_LISTEN=0.0.0.0:9876 slotdb -c slotdb.yaml
Read config from slotdb.yaml, and then set listen address
HELP;
exit(0);
}
if (isset($opts['h']) || isset($opts['help'])) {
print_usage();
}
if (isset($opts['db'])) {
$initDb = isset($opts['init-db']);
$upgradeDb = isset($opts['upgrade-db']);
}
$listen = getenv("SLOTDB_LISTEN")
?: ($opts['listen']
?? ($opts['l'] ?? null));
$logHandlers = [
new Monolog\Handler\StreamHandler(STDOUT),
];
$logger = new Monolog\Logger("main", $logHandlers);
$daemon = new Daemon(
listen: $listen ?? '0.0.0.0:8080',
logger: $logger,
);
$daemon->start();

2
var/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/*.db
/*/*.json