init
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
|
||||
class File
|
||||
{
|
||||
|
||||
function __construct(
|
||||
public readonly string $originalName,
|
||||
public readonly string $mime,
|
||||
public readonly string $path,
|
||||
public readonly int $size,
|
||||
public readonly int $width,
|
||||
public readonly int $height,
|
||||
public readonly string $checksum,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): File
|
||||
{
|
||||
return new File(
|
||||
originalName: data_get($data, "originalName"),
|
||||
mime: data_get($data, "mime"),
|
||||
path: data_get($data, "path"),
|
||||
size: data_get($data, "size"),
|
||||
width: data_get($data, "width"),
|
||||
height: data_get($data, "height"),
|
||||
checksum: data_get($data, "checksum"),
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return \json_decode(\json_encode($this), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
|
||||
class InputFormatter
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public ChannelService $channelService,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function fill(string $schemaName, RecordData $input): RecordData
|
||||
{
|
||||
$schema = $this->channelService->getSchema($schemaName)->get();
|
||||
$data = $schema->fields->reduce(fn(array $carry, FieldInterface $field) => $field->format($input->toArray(), $carry), []);
|
||||
return new RecordData($data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Schema\Schema;
|
||||
|
||||
class Manager
|
||||
{
|
||||
|
||||
public array $records = [];
|
||||
public Session $session;
|
||||
|
||||
public function fromSession(Session $session): Manager
|
||||
{
|
||||
$this->session = $session;
|
||||
$this->records = $session->get("manager") ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function __construct(
|
||||
private readonly Query $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $records
|
||||
*/
|
||||
|
||||
public function addRecords(array $records): Manager
|
||||
{
|
||||
$this->records = $records;
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
public function push(string $recordId): Manager
|
||||
{
|
||||
|
||||
$records = $this->getIdsExcept($recordId);
|
||||
$records[] = $recordId;
|
||||
$records = array_unique($records);
|
||||
$records = array_values($records);
|
||||
$records = array_slice($records, -5);
|
||||
$records = array_values($records);
|
||||
$this->records = $records;
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function save(): void
|
||||
{
|
||||
|
||||
$this->session->put("manager", $this->records);
|
||||
|
||||
}
|
||||
|
||||
public function getIdsExcept(?string $id): array
|
||||
{
|
||||
return collect($this->records)->filter(fn($arec) => $arec !== $id)->values()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryRecord[] $records
|
||||
* @return QueryRecord[] $records
|
||||
*/
|
||||
public function order(array $records): array
|
||||
{
|
||||
$recordsById = collect($records)->keyBy("id")->toArray();
|
||||
return collect($this->records)->reverse()->values()->reduce(function ($carry, $arecId) use ($recordsById) {
|
||||
if (isset($recordsById[$arecId])) {
|
||||
$carry[] = $recordsById[$arecId];
|
||||
}
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<Schema> $schemas
|
||||
*/
|
||||
public function getRecords(?string $ignoreId = null): array
|
||||
{
|
||||
|
||||
$queryResult = $this->query
|
||||
->filter(["id_in" => $this->getIdsExcept($ignoreId)])
|
||||
->limit(7)
|
||||
->run();
|
||||
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
|
||||
return $this->order($graph->records->toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Lucent\LucentException;
|
||||
|
||||
class QueryRecord
|
||||
{
|
||||
|
||||
function __construct(
|
||||
public string $id,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public bool $isRoot,
|
||||
public ?File $_file = null,
|
||||
public array $_children = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return json_decode(json_encode($this), true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public static function fromArray(array $data): QueryRecord
|
||||
{
|
||||
|
||||
|
||||
return new QueryRecord(
|
||||
id: $data["id"],
|
||||
_sys: System::fromArray($data["_sys"]),
|
||||
data: new RecordData($data["data"]),
|
||||
isRoot: $data["isRoot"] ?? false,
|
||||
_file: $data["_file"] ? new File(...$data["_file"]) : null,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromRecord(Record $record): QueryRecord
|
||||
{
|
||||
return new QueryRecord(
|
||||
id: $record->id,
|
||||
_sys: $record->_sys,
|
||||
data: $record->data,
|
||||
isRoot: false,
|
||||
_file: $record->_file,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use JsonSerializable;
|
||||
use stdClass;
|
||||
|
||||
class Record implements JsonSerializable
|
||||
{
|
||||
|
||||
|
||||
function __construct(
|
||||
public string $id,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public ?File $_file = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return \json_decode(\json_encode($this), true);
|
||||
}
|
||||
|
||||
public function toDB(): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"_sys" => json_encode($this->_sys),
|
||||
"_file" => json_encode($this->_file),
|
||||
"data" => json_encode($this->data),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromDB(stdClass $data): Record
|
||||
{
|
||||
|
||||
$file = json_decode($data->_file, true);
|
||||
if (!empty($file)) {
|
||||
|
||||
$file = new File(...$file);
|
||||
} else {
|
||||
$file = null;
|
||||
}
|
||||
|
||||
return new Record(
|
||||
id: $data->id,
|
||||
_sys: System::fromArray(json_decode($data->_sys, true)),
|
||||
data: new RecordData(json_decode($data->data, true)),
|
||||
_file: $file,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function jsonSerialize(): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): Record
|
||||
{
|
||||
|
||||
$file = null;
|
||||
if (!empty($data["_file"])) {
|
||||
$file = File::fromArray($data["_file"]);
|
||||
}
|
||||
|
||||
return new Record(
|
||||
id: $data["id"],
|
||||
_sys: System::fromArray($data["_sys"]),
|
||||
data: new RecordData($data["data"]),
|
||||
_file: $file,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Lucent\ArrayContainer;
|
||||
|
||||
class RecordData extends ArrayContainer
|
||||
{
|
||||
|
||||
|
||||
public function merge(RecordData $data): RecordData
|
||||
{
|
||||
|
||||
$this->data = array_merge($this->data, $data->toArray());
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Lucent\Edge\QueryEdge;
|
||||
|
||||
class RecordGraph
|
||||
{
|
||||
/**
|
||||
* @param \Lucent\Edge\QueryEdge[] $edges
|
||||
* @param \Lucent\Record\EdgeRecord[] $nodes
|
||||
*/
|
||||
function __construct(
|
||||
public readonly array $edges,
|
||||
public readonly array $nodes,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return \json_decode(\json_encode($this), true);
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): RecordGraph
|
||||
{
|
||||
return new RecordGraph(
|
||||
edges: collect($data["edges"] ?? [])->map(fn ($edge) => new QueryEdge(...$edge))->toArray(),
|
||||
nodes: collect($data["nodes"] ?? [])->map(fn ($node) => EdgeRecord::fromArray($node))->toArray(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RecordRepo
|
||||
{
|
||||
|
||||
|
||||
public static function create(Record $record): void
|
||||
{
|
||||
$recordToDB = $record->toDB();
|
||||
DB::table("records")->insert($recordToDB);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $ids
|
||||
*/
|
||||
public static function updateStatusBulk(RecordStatus $status, array $ids): void
|
||||
{
|
||||
DB::table("records")->whereIn("id", $ids)->update([
|
||||
'_sys->status' => $status->value()
|
||||
]);
|
||||
}
|
||||
|
||||
public static function update(Record $record): void
|
||||
{
|
||||
|
||||
$recordToDB = $record->toDB();
|
||||
DB::table("records")->where("id", $record->id)->update($recordToDB);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string[] $ids
|
||||
*/
|
||||
public static function deleteMany(
|
||||
array $ids,
|
||||
): void
|
||||
{
|
||||
|
||||
DB::table("records")
|
||||
->whereIn("id", $ids)->delete();
|
||||
DB::table("edges")->whereIn("source", $ids)->delete();
|
||||
DB::table("edges")->whereIn("target", $ids)->delete();
|
||||
DB::table("revisions")->whereIn("recordId", $ids)->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Edge\EdgeCollection;
|
||||
use Lucent\Edge\EdgeRepo;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\Id\Id;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Revision\RevisionService;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\Validator\Validator;
|
||||
use Lucent\Schema\Validator\ValidatorException;
|
||||
|
||||
readonly class RecordService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private AuthService $authService,
|
||||
private RevisionService $revisionService,
|
||||
private ChannelService $channelService,
|
||||
private Validator $recordValidator,
|
||||
private Query $query,
|
||||
private InputFormatter $inputFormatter
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public
|
||||
function create(
|
||||
string $schemaName,
|
||||
array $data,
|
||||
?string $id = null,
|
||||
array $file = [],
|
||||
array $edges = [],
|
||||
string $status = "draft",
|
||||
string $uploadFromUrl = "",
|
||||
): string
|
||||
{
|
||||
$schema = $this->channelService->channel->schemas->where("name", $schemaName)->first();
|
||||
|
||||
if (empty($schema)) {
|
||||
throw new LucentException("The schema " . $schemaName . " does not exist");
|
||||
}
|
||||
|
||||
$formattedData = $this->inputFormatter->fill($schemaName, new RecordData($data));
|
||||
if (empty($formattedData["id"])) {
|
||||
$formattedData["id"] = Id::new();
|
||||
}
|
||||
$uploadResult = FileService::create($schema, $uploadFromUrl, $file);
|
||||
|
||||
$uniqueEdges = collect($edges)
|
||||
->map(fn($e) => (array)(new Edge(...$e)))
|
||||
->map(function ($edge, $index) {
|
||||
$edgeData = (array)(new Edge(...$edge));
|
||||
$edgeData["rank"] = $index;
|
||||
return $edgeData;
|
||||
})
|
||||
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
||||
->values()->toArray();
|
||||
$uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
|
||||
|
||||
if ($uploadResult->isDuplicate) {
|
||||
EdgeRepo::update($uploadResult->duplicateId, $uniqueEdgesCollection);
|
||||
return $uploadResult->duplicateId;
|
||||
}
|
||||
|
||||
$record = new Record(
|
||||
id: $id ?? Id::new(),
|
||||
_sys: System::newRecord($schema, $this->authService->currentUserId(), $status),
|
||||
data: $formattedData,
|
||||
_file: $uploadResult->recordFile,
|
||||
);
|
||||
|
||||
$errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
|
||||
RecordRepo::create($record);
|
||||
EdgeRepo::update($record->id, $uniqueEdgesCollection);
|
||||
$this->revisionService->create($record);
|
||||
return $record->id;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public
|
||||
function update(
|
||||
string $id,
|
||||
array $data,
|
||||
string $status = "draft",
|
||||
array $edges = [],
|
||||
bool $updateEdges = false
|
||||
): void
|
||||
{
|
||||
|
||||
$queryResult = $this->query->filter(["id" => $id])->run();
|
||||
$record = $queryResult->getQueryRecords()->records[0] ?? null;
|
||||
|
||||
if (empty($record)) {
|
||||
throw new LucentException("Record id is missing");
|
||||
}
|
||||
$formattedData = $this->inputFormatter->fill($record->_sys->schema, new RecordData($data));
|
||||
|
||||
if ($updateEdges) {
|
||||
$uniqueEdges = collect($edges)
|
||||
->map(function ($edge, $index) {
|
||||
$edgeData = (array)(new Edge(...$edge));
|
||||
$edgeData["rank"] = $index;
|
||||
return $edgeData;
|
||||
})
|
||||
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
||||
->values()->toArray();
|
||||
$uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
|
||||
$errors = $this->recordValidator->check($record->_sys->schema, $formattedData, $uniqueEdgesCollection);
|
||||
} else {
|
||||
$errors = $this->recordValidator->check($record->_sys->schema, $formattedData, null);
|
||||
}
|
||||
|
||||
|
||||
$newRecord = new Record(
|
||||
id: $record->id,
|
||||
_sys: $record->_sys->update($this->authService->currentUserId(), $status),
|
||||
data: $record->data->merge($formattedData),
|
||||
_file: $record->_file,
|
||||
);
|
||||
|
||||
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
|
||||
RecordRepo::update($newRecord);
|
||||
if ($updateEdges) {
|
||||
EdgeRepo::update($newRecord->id, $uniqueEdgesCollection);
|
||||
}
|
||||
|
||||
$this->revisionService->create($newRecord);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public
|
||||
function changeStatusBulk(
|
||||
string $status,
|
||||
array $recordsIds,
|
||||
): void
|
||||
{
|
||||
$recordsStatus = (new RecordStatus($status));
|
||||
RecordRepo::updateStatusBulk($recordsStatus, $recordsIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public
|
||||
function clone(
|
||||
string $recordId,
|
||||
): string
|
||||
{
|
||||
$queryResult = $this->query
|
||||
->filter(["id" => $recordId])
|
||||
->limit(1)
|
||||
->childrenDepth(1)
|
||||
->runWithCount();
|
||||
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
$record = $graph->records[0] ?? null;
|
||||
if (empty($record)) {
|
||||
throw new LucentException("Record id is missing");
|
||||
}
|
||||
$newRecordId = (string)Str::uuid();
|
||||
$newEdgesData = $graph->edges
|
||||
->filter(fn(Edge $edge) => $edge->source == $recordId)
|
||||
->values()
|
||||
->map(function (Edge $edge) use ($newRecordId) {
|
||||
$edge->source = $newRecordId;
|
||||
return $edge->toArray();
|
||||
})->toArray();
|
||||
|
||||
$record->id = $newRecordId;
|
||||
|
||||
return $this->create(
|
||||
schemaName: $record->_sys->schema,
|
||||
data: $record->data->toArray(),
|
||||
id: $record->id,
|
||||
file: $record->_file?->toArray() ?? [],
|
||||
edges: $newEdgesData,
|
||||
status: "draft"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function deleteMany(
|
||||
array $recordsIds,
|
||||
): void
|
||||
{
|
||||
RecordRepo::deleteMany($recordsIds);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public function rollback(
|
||||
string $userId,
|
||||
string $recordId,
|
||||
int $version,
|
||||
): void
|
||||
{
|
||||
$revision = $this->revisionService->getByRecordIdAndVersion($recordId, $version)->get();
|
||||
$this->update(
|
||||
userId: $userId,
|
||||
id: $revision->recordId,
|
||||
data: $revision->data->toArray(),
|
||||
status: $revision->_sys->status,
|
||||
updateEdges: false
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function createEmpty(
|
||||
Schema $schema,
|
||||
string $userId,
|
||||
): Record
|
||||
{
|
||||
|
||||
$defaultValues = $schema->fields->reduce(function ($carry, FieldInterface $f) {
|
||||
$carry[$f->name] = $f->default ?? null;
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
$formattedData = $this->inputFormatter->fill($schema->name, new RecordData($defaultValues));
|
||||
|
||||
return new Record(
|
||||
id: Id::new(),
|
||||
_sys: System::newRecord($schema, $userId),
|
||||
data: $formattedData,
|
||||
_file: null,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
class RecordStatus
|
||||
{
|
||||
|
||||
private string $value;
|
||||
|
||||
public function __construct(
|
||||
public readonly ?string $status = null,
|
||||
)
|
||||
{
|
||||
|
||||
if (empty($status)) {
|
||||
$this->value = "draft";
|
||||
return;
|
||||
}
|
||||
$validStatuses = ["trashed", "published", "draft"];
|
||||
if (!in_array($status, $validStatuses)) {
|
||||
$status = "draft";
|
||||
}
|
||||
|
||||
$this->value = $status;
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Lucent\Schema\Schema;
|
||||
|
||||
class System
|
||||
{
|
||||
|
||||
|
||||
function __construct(
|
||||
public readonly string $schema,
|
||||
public readonly int $version,
|
||||
public readonly string $status,
|
||||
public readonly string $createdBy,
|
||||
public readonly string $updatedBy,
|
||||
public readonly string $createdAt,
|
||||
public readonly string $updatedAt,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static function fromArray(array $data): System
|
||||
{
|
||||
return new System(
|
||||
schema: data_get($data, "schema"),
|
||||
version: data_get($data, "version"),
|
||||
status: data_get($data, "status"),
|
||||
createdBy: data_get($data, "createdBy"),
|
||||
updatedBy: data_get($data, "updatedBy"),
|
||||
createdAt: data_get($data, "createdAt"),
|
||||
updatedAt: data_get($data, "updatedAt"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public static function newRecord(Schema $schema, string $userId, ?string $status = null): System
|
||||
{
|
||||
$now = Carbon::now()->toJson();
|
||||
return new System(
|
||||
schema: $schema->name,
|
||||
version: 1,
|
||||
status: (new RecordStatus($status))->value(),
|
||||
createdBy: $userId,
|
||||
updatedBy: $userId,
|
||||
createdAt: $now,
|
||||
updatedAt: $now,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function update(string $userId, ?string $status = null): System
|
||||
{
|
||||
$now = Carbon::now()->toJson();
|
||||
$newStatus = $status ?? $this->status;
|
||||
return new System(
|
||||
schema: $this->schema,
|
||||
version: $this->version + 1,
|
||||
status: (new RecordStatus($newStatus))->value(),
|
||||
createdBy: $this->createdBy,
|
||||
updatedBy: $userId,
|
||||
createdAt: $this->createdAt,
|
||||
updatedAt: $now,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user