records and edgs
This commit is contained in:
+3
-3
@@ -18,7 +18,8 @@
|
||||
"mustache/mustache": "^2.14"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8"
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"laravel/framework": "^10.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -26,8 +27,7 @@
|
||||
},
|
||||
"files": [
|
||||
"src/Response.php",
|
||||
"src/macros.php",
|
||||
"src/File/Uploader.php"
|
||||
"src/macros.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
||||
Generated
+3924
-510
File diff suppressed because it is too large
Load Diff
@@ -11,13 +11,8 @@
|
||||
// if (edges[0]) {
|
||||
// firstRecord = record._children.find((r) => r.data.id === edges[0].to);
|
||||
// }
|
||||
|
||||
console.log(filePreviews)
|
||||
</script>
|
||||
|
||||
<!-- {#if firstRecord}
|
||||
<Preview record={firstRecord} size="tiny" />
|
||||
{/if} -->
|
||||
<div class="d-flex me-1">
|
||||
{#each filePreviews as file}
|
||||
<div class="me-1">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import Index from "../content/Index.svelte";
|
||||
import axios from "axios";
|
||||
|
||||
let dialogEl;
|
||||
|
||||
@@ -38,6 +39,7 @@
|
||||
dispatch("insert", {
|
||||
records: selectedRecords,
|
||||
action: "insert",
|
||||
schema: data.schema.name,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
export function open( schema) {
|
||||
export function open(schema) {
|
||||
dialogEl.showModal()
|
||||
load(schema);
|
||||
}
|
||||
|
||||
@@ -3,29 +3,12 @@ import {stripHtml} from "../../helpers";
|
||||
|
||||
export function previewTitle(schemas, record, graph) {
|
||||
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
|
||||
if (!schema?.titleTemplate) {
|
||||
if (!schema?.cardTitle) {
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
let recordData = record.data;
|
||||
let template = Mustache.parse(schema.titleTemplate);
|
||||
|
||||
let referencePreviews = template
|
||||
.filter(segment => segment[0] === "name") // keep only template tags
|
||||
.map((segment) => segment[1]) // map to fieldNames
|
||||
.filter(fieldName => { // keep only references
|
||||
let schemaField = schema.fields.find(f => f.name === fieldName)
|
||||
return ["reference","file"].includes(schemaField?.info.name);
|
||||
}).reduce((carry, field) => { // map to records
|
||||
let edge = graph?.edges.find(edge => edge.source === record.id && edge.field === field)
|
||||
let referenceRecord = graph?.records.find(rec => rec.id === edge?.target)
|
||||
carry[field] = previewTitle(schemas, referenceRecord, graph);
|
||||
return carry;
|
||||
}, {});
|
||||
|
||||
recordData = {...recordData, ...referencePreviews}
|
||||
|
||||
let render = Mustache.render(schema.titleTemplate, recordData);
|
||||
let render = Mustache.render(schema.cardTitle, recordData);
|
||||
if (!render || render === "") {
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import PreviewReference from "../previews/PreviewReference.svelte";
|
||||
import axios from "axios";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let record;
|
||||
@@ -39,7 +40,16 @@
|
||||
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
|
||||
// axios.post(channel.lucentUrl + "/edges/insert-many", {
|
||||
// source: record.id,
|
||||
// sourceSchema: record.schema,
|
||||
// targetSchema: e.detail.schema,
|
||||
// field: field.name,
|
||||
// targets: e.detail.records.map(r => r.id),
|
||||
// }).then(function (response) {
|
||||
// graph = response.data.graph;
|
||||
// })
|
||||
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -51,10 +61,10 @@
|
||||
{/if}
|
||||
<div class="inline-card-wrapper">
|
||||
<ReferenceInlineButtons
|
||||
recordId={null}
|
||||
schemas={collections}
|
||||
on:insert={insert}
|
||||
on:save={insert}
|
||||
recordId={null}
|
||||
schemas={collections}
|
||||
on:insert={insert}
|
||||
on:save={insert}
|
||||
/>
|
||||
</div>
|
||||
{#if references.length > 0}
|
||||
@@ -63,9 +73,9 @@
|
||||
<div>
|
||||
<PreviewReference
|
||||
{graph}
|
||||
record={reference}
|
||||
hasDelete={true}
|
||||
on:remove={removeReference}
|
||||
record={reference}
|
||||
hasDelete={true}
|
||||
on:remove={removeReference}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
const channel = getContext("channel");
|
||||
export let schemas;
|
||||
export let recordId;
|
||||
$: showOptions = false;
|
||||
let browseModal;
|
||||
let dialogRecord;
|
||||
let inLineCreateRecord;
|
||||
@@ -35,9 +34,9 @@
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
browseModal.close();
|
||||
showOptions = false;
|
||||
dispatch("insert", {
|
||||
records: e.detail.records,
|
||||
schema: e.detail.schema,
|
||||
after: recordId,
|
||||
});
|
||||
}
|
||||
@@ -49,7 +48,6 @@
|
||||
.then((response) => {
|
||||
inLineCreateRecord = response.data;
|
||||
dialogRecord.open()
|
||||
showOptions = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
|
||||
<Tinymce bind:this={editor} bind:value {additionalConfig}/>
|
||||
{#if field.collections}
|
||||
{#if field.collections.length > 0}
|
||||
<RichEditorFiles
|
||||
bind:graph
|
||||
{record}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script>
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import { previewTitle } from "./../Preview";
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import {previewTitle} from "./../Preview";
|
||||
import Status from "./../Status.svelte";
|
||||
import Preview from "../../files/Preview.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
export let graph;
|
||||
@@ -12,9 +14,11 @@
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle(channel.schemas, record, graph);
|
||||
const cardImageEdge = graph.edges.find(e => e.source === record.id && e.field === schema.cardImage);
|
||||
let cardImageRecord = graph.records.find(r => r.id === cardImageEdge?.target);
|
||||
|
||||
function remove(e) {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch("remove", record.id);
|
||||
}
|
||||
</script>
|
||||
@@ -23,6 +27,11 @@
|
||||
<div class="preview-reference">
|
||||
<div style="display: flex;align-items: center;gap: 10px;">
|
||||
|
||||
{#if cardImageRecord}
|
||||
<div class="image">
|
||||
<Preview record={cardImageRecord} size="small"/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="title">
|
||||
<div>
|
||||
<a
|
||||
@@ -43,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if hasDelete}
|
||||
<div class="trash-action">
|
||||
<div class="reference-action">
|
||||
<button
|
||||
class="button"
|
||||
on:click={remove}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Edge;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Support\Collection<int|string, Edge>
|
||||
*/
|
||||
final class EdgeCollection extends Collection
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
Edge ...$array
|
||||
) {
|
||||
parent::__construct($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Edge[]
|
||||
**/
|
||||
public function toArray(): array
|
||||
{
|
||||
return collect($this)->values()->toArray();
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): EdgeCollection
|
||||
{
|
||||
$edges = array_map([Edge::class, 'fromArray'], $data);
|
||||
return new EdgeCollection(...$edges);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+47
-3
@@ -25,7 +25,31 @@ class EdgeRepo
|
||||
|
||||
}
|
||||
|
||||
public function update(string $from, EdgeCollection $edges): void
|
||||
/**
|
||||
* @param list<Edge> $edges
|
||||
* @return void
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function insertMany(array $edges): void
|
||||
{
|
||||
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
|
||||
try {
|
||||
DB::table("edges")->insert($edgesDB);
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == 23505) {
|
||||
throw new LucentException("Edge already exists");
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param list<Edge> $edges
|
||||
* @return void
|
||||
*/
|
||||
public function replaceForRecord(string $from, array $edges): void
|
||||
{
|
||||
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
|
||||
DB::table("edges")->where("source", $from)->delete();
|
||||
@@ -33,10 +57,19 @@ class EdgeRepo
|
||||
}
|
||||
|
||||
|
||||
public function findAll(): EdgeCollection
|
||||
/**
|
||||
* @return list<Edge>
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
$edges = DB::table("edges")->get();
|
||||
return new EdgeCollection(...$edges->map([$this, 'mapEdge'])->toArray());
|
||||
return $edges->map([$this, 'mapEdge'])->toArray();
|
||||
}
|
||||
|
||||
public function findForSource(string $recordId): array
|
||||
{
|
||||
$edges = DB::table("edges")->where("source", $recordId)->get();
|
||||
return $edges->map([$this, 'mapEdge'])->toArray();
|
||||
}
|
||||
|
||||
public function mapEdge(stdClass $edge): Edge
|
||||
@@ -65,4 +98,15 @@ class EdgeRepo
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function findLastEdgeRank(string $source, string $field): string
|
||||
{
|
||||
$data = DB::table("edges")
|
||||
->where("source", $source)
|
||||
->where("field", $field)
|
||||
->orderBy("rank", "desc")
|
||||
->first();
|
||||
|
||||
return $data->rank ?? "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php namespace Lucent\Edge;
|
||||
|
||||
use Lucent\Lexorank\Lexorank;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Record\InputData\EdgeInputData;
|
||||
|
||||
class EdgeService
|
||||
{
|
||||
@@ -8,9 +10,7 @@ class EdgeService
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
|
||||
public function create(
|
||||
string $source,
|
||||
string $target,
|
||||
@@ -35,12 +35,73 @@ class EdgeService
|
||||
return $edge;
|
||||
}
|
||||
|
||||
public function update(string $from, EdgeCollection $edges): void
|
||||
/**
|
||||
* @param string $source
|
||||
* @param string $sourceSchema
|
||||
* @param list<EdgeInputData> $edges
|
||||
* @return list<Edge>
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function createManyForRecord(
|
||||
string $source,
|
||||
string $sourceSchema,
|
||||
array $edges,
|
||||
): array
|
||||
{
|
||||
$this->edgeRepo->update($from, $edges);
|
||||
$lastRank = Lexorank::fromString("0");
|
||||
$edgeCollection = [];
|
||||
foreach ($edges as $edge) {
|
||||
$lastRank = Lexorank::after($lastRank);
|
||||
$edgeCollection[] = new Edge(
|
||||
source: $source,
|
||||
target: $edge->target,
|
||||
sourceSchema: $sourceSchema,
|
||||
targetSchema: $edge->targetSchema,
|
||||
field: $edge->field,
|
||||
rank: $lastRank->get(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->edgeRepo->insertMany($edgeCollection);
|
||||
return $edgeCollection;
|
||||
}
|
||||
|
||||
public function findAll(): EdgeCollection
|
||||
/**
|
||||
* @param string $source
|
||||
* @param string $sourceSchema
|
||||
* @param list<EdgeInputData> $edges
|
||||
* @return list<Edge>
|
||||
*/
|
||||
public function replaceManyForRecord(
|
||||
string $source,
|
||||
string $sourceSchema,
|
||||
array $edges,
|
||||
): array
|
||||
{
|
||||
$lastRank = Lexorank::fromString("0");
|
||||
$edgeCollection = [];
|
||||
foreach ($edges as $edge) {
|
||||
$lastRank = Lexorank::after($lastRank);
|
||||
$edgeCollection[] = new Edge(
|
||||
source: $source,
|
||||
target: $edge->target,
|
||||
sourceSchema: $sourceSchema,
|
||||
targetSchema: $edge->targetSchema,
|
||||
field: $edge->field,
|
||||
rank: $lastRank->get(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->edgeRepo->replaceForRecord($source, $edgeCollection);
|
||||
return $edgeCollection;
|
||||
}
|
||||
|
||||
public function findForSource(string $recordId): array
|
||||
{
|
||||
return $this->edgeRepo->findForSource($recordId);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->edgeRepo->findAll();
|
||||
}
|
||||
@@ -50,4 +111,9 @@ class EdgeService
|
||||
$this->edgeRepo->remove($edge);
|
||||
}
|
||||
|
||||
private function findLastEdgeRank(string $source, string $field): string
|
||||
{
|
||||
return $this->edgeRepo->findLastEdgeRank($source, $field);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php namespace Lucent\Edge\Event;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
|
||||
class EdgesUpdated
|
||||
{
|
||||
use Dispatchable;
|
||||
|
||||
public function __construct(
|
||||
public string $recordId,
|
||||
public string $field,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+134
-37
@@ -2,13 +2,20 @@
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\ImageManagerStatic;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Record\File;
|
||||
use Lucent\Record\FileData as RecordFile;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Schema\FilesSchema;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\Type;
|
||||
use Spatie\ImageOptimizer\OptimizerChainFactory;
|
||||
|
||||
class FileService
|
||||
{
|
||||
@@ -21,43 +28,11 @@ class FileService
|
||||
|
||||
public function getPath(QueryRecord $file): string
|
||||
{
|
||||
return $this->channelService->channel->url. "/storage/".$file->_file->path;
|
||||
return $this->channelService->channel->url . "/storage/" . $file->_file->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public static function create(Schema $schema, string $uploadFromUrl, array $file): FileUploadResult
|
||||
public function createFromUrl(FilesSchema $schema, string $url): FileUploadResult
|
||||
{
|
||||
$emptyUploadUrl = empty($uploadFromUrl);
|
||||
$emptyFileData = empty($file);
|
||||
|
||||
if ($schema->type === Type::FILES && $emptyUploadUrl && $emptyFileData) {
|
||||
throw new LucentException("No file data submitted");
|
||||
} elseif ($schema->type !== Type::FILES && !($emptyUploadUrl && $emptyFileData)) {
|
||||
throw new LucentException("You can't upload a file to a regular record");
|
||||
} elseif ($schema->type !== Type::FILES) {
|
||||
return new FileUploadResult(
|
||||
recordFile: null, duplicateId: "", isDuplicate: false
|
||||
);
|
||||
}
|
||||
|
||||
if (!$emptyUploadUrl) {
|
||||
$file = self::uploadFileFromUrl($uploadFromUrl);
|
||||
return uploadFile($schema, $file);
|
||||
}
|
||||
|
||||
return new FileUploadResult(
|
||||
recordFile: File::fromArray($file), duplicateId: "", isDuplicate: false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
private static function uploadFileFromUrl(string $url): UploadedFile
|
||||
{
|
||||
|
||||
$pathinfo = pathinfo($url);
|
||||
$contents = file_get_contents($url);
|
||||
if ($contents === false) {
|
||||
@@ -65,6 +40,128 @@ class FileService
|
||||
}
|
||||
$file = '/tmp/' . $pathinfo['basename'];
|
||||
file_put_contents($file, $contents);
|
||||
return new UploadedFile($file, $pathinfo['basename']);
|
||||
$uploadedFile = new UploadedFile($file, $pathinfo['basename']);
|
||||
return $this->upload($schema, $uploadedFile);
|
||||
}
|
||||
|
||||
public function upload(FilesSchema $schema, UploadedFile $file): FileUploadResult
|
||||
{
|
||||
$originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$extension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION);
|
||||
$originalFilename = $file->getClientOriginalName();
|
||||
$filename = $this->createFileName($originalName, $extension);
|
||||
$mimetype = $file->getMimeType();
|
||||
|
||||
$optimizerChain = OptimizerChainFactory::create();
|
||||
$optimizerChain->setTimeout(30)->optimize($file->getPathName());
|
||||
|
||||
$checksum = sha1_file($file);
|
||||
$recordId = $this->checkDuplicate($schema->name, $checksum, $file->getSize());
|
||||
|
||||
if (!empty($recordId)) {
|
||||
return new FileUploadResult(
|
||||
recordFile: null,
|
||||
duplicateId: $recordId,
|
||||
isDuplicate: true
|
||||
);
|
||||
}
|
||||
|
||||
$disk = $this->loadDisk();
|
||||
$path = $schema->path . "/" . $filename;
|
||||
$res = $disk->put(
|
||||
$path,
|
||||
file_get_contents($file),
|
||||
// 'public' // now managed by aws policy
|
||||
);
|
||||
|
||||
if ($res === false) {
|
||||
throw new LucentException("File $filename not uploaded");
|
||||
}
|
||||
|
||||
$this->createThumbnail($disk, $schema->path, $filename, $file);
|
||||
|
||||
list($width, $height) = $this->isImage($mimetype) ? getimagesize($file) : [0, 0];
|
||||
$recordFile = new RecordFile(
|
||||
originalName: $originalFilename,
|
||||
mime: $mimetype,
|
||||
path: $path,
|
||||
size: $file->getSize(),
|
||||
width: $width,
|
||||
height: $height,
|
||||
checksum: $checksum
|
||||
);
|
||||
|
||||
return new FileUploadResult(
|
||||
recordFile: $recordFile,
|
||||
duplicateId: "",
|
||||
isDuplicate: false
|
||||
);
|
||||
}
|
||||
|
||||
private function createFileName(string $originalName, string $extension): string
|
||||
{
|
||||
return Str::slug($originalName, '-') . '-' . uniqid() . '.' . $extension;
|
||||
}
|
||||
|
||||
private function isImage(string $mimetype): bool
|
||||
{
|
||||
$imageMimes = ['image/webp', 'image/gif', 'image/jpeg', 'image/png', 'image/tiff'];
|
||||
return in_array($mimetype, $imageMimes);
|
||||
}
|
||||
|
||||
public function loadDisk(): Filesystem
|
||||
{
|
||||
return Storage::build([
|
||||
'driver' => 'local',
|
||||
// 'key' => config("filesystems.disks.s3.key"),
|
||||
// 'secret' => config("filesystems.disks.s3.secret"),
|
||||
// 'region' => config("filesystems.disks.s3.region"),
|
||||
// 'bucket' => config("filesystems.disks.s3.bucket"),
|
||||
// // 'url' => $schema->objectStorageUrl,
|
||||
// 'endpoint' => $schema->objectStorageEndpoint,
|
||||
'use_path_style_endpoint' => false,
|
||||
'visibility' => 'public', // now managed by aws policy
|
||||
'root' => storage_path('app/public'),
|
||||
'throw' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function checkDuplicate(string $schemaName, string $checksum, int $filesize): string
|
||||
{
|
||||
|
||||
$record = DB::table("records")
|
||||
->where("schema", $schemaName)
|
||||
->where("_file->checksum", $checksum)
|
||||
->where("_file->size", $filesize)
|
||||
->first();
|
||||
|
||||
return $record->id ?? "";
|
||||
}
|
||||
|
||||
private function createThumbnail(Filesystem $disk, string $schemaPath, string $filename, UploadedFile $file): void
|
||||
{
|
||||
|
||||
|
||||
$thumbDir = storage_path("app/public/thumbs/" . $schemaPath . "/");
|
||||
if (!file_exists($thumbDir)) {
|
||||
make_dir_r($thumbDir);
|
||||
}
|
||||
|
||||
try {
|
||||
ImageManagerStatic::configure(['driver' => 'imagick']);
|
||||
$image = ImageManagerStatic::make($file);
|
||||
} catch (Exception $e) {
|
||||
logger($e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$image->fit(300, 300);
|
||||
try {
|
||||
$image->encode('webp', 75)->save($thumbDir . $filename);
|
||||
} catch (Exception $e) {
|
||||
logger($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Lucent\Record\File;
|
||||
use Lucent\Record\FileData;
|
||||
|
||||
class FileUploadResult
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public ?File $recordFile,
|
||||
public string $duplicateId,
|
||||
public bool $isDuplicate,
|
||||
public ?FileData $recordFile,
|
||||
public string $duplicateId,
|
||||
public bool $isDuplicate,
|
||||
)
|
||||
{
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\ImageManagerStatic;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Record\File as RecordFile;
|
||||
use Lucent\Schema\Schema;
|
||||
use Spatie\ImageOptimizer\OptimizerChainFactory;
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
function uploadFile(Schema $schema, UploadedFile $file): FileUploadResult
|
||||
{
|
||||
$originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$extension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION);
|
||||
$originalFilename = $file->getClientOriginalName();
|
||||
$filename = createFileName($originalName, $extension);
|
||||
$mimetype = $file->getMimeType();
|
||||
|
||||
$optimizerChain = OptimizerChainFactory::create();
|
||||
$optimizerChain->setTimeout(30)->optimize($file->getPathName());
|
||||
|
||||
$checksum = sha1_file($file);
|
||||
$recordId = checkDuplicate($schema->name, $checksum, $file->getSize());
|
||||
if (!empty($recordId)) {
|
||||
return new FileUploadResult(
|
||||
recordFile: null,
|
||||
duplicateId: $recordId,
|
||||
isDuplicate: true
|
||||
);
|
||||
}
|
||||
|
||||
$disk = loadDisk();
|
||||
$path = $schema->path . "/" . $filename;
|
||||
$res = $disk->put(
|
||||
$path,
|
||||
file_get_contents($file),
|
||||
// 'public' // now managed by aws policy
|
||||
);
|
||||
|
||||
if ($res === false) {
|
||||
throw new LucentException("File $filename not uploaded");
|
||||
}
|
||||
|
||||
createThumbnail($disk, $schema->path, $filename, $file);
|
||||
|
||||
list($width, $height) = isImage($mimetype) ? getimagesize($file) : [0, 0];
|
||||
$recordFile = new RecordFile(
|
||||
originalName: $originalFilename,
|
||||
mime: $mimetype,
|
||||
path: $path,
|
||||
size: $file->getSize(),
|
||||
width: $width,
|
||||
height: $height,
|
||||
checksum: $checksum
|
||||
);
|
||||
|
||||
return new FileUploadResult(
|
||||
recordFile: $recordFile,
|
||||
duplicateId: "",
|
||||
isDuplicate: false
|
||||
);
|
||||
}
|
||||
|
||||
function createFileName(string $originalName, string $extension): string
|
||||
{
|
||||
return Str::slug($originalName, '-') . '-' . uniqid() . '.' . $extension;
|
||||
}
|
||||
|
||||
function isImage(string $mimetype): bool
|
||||
{
|
||||
$imageMimes = ['image/webp', 'image/gif', 'image/jpeg', 'image/png', 'image/tiff'];
|
||||
return in_array($mimetype, $imageMimes);
|
||||
}
|
||||
|
||||
function loadDisk(): Filesystem
|
||||
{
|
||||
return Storage::build([
|
||||
'driver' => 'local',
|
||||
// 'key' => config("filesystems.disks.s3.key"),
|
||||
// 'secret' => config("filesystems.disks.s3.secret"),
|
||||
// 'region' => config("filesystems.disks.s3.region"),
|
||||
// 'bucket' => config("filesystems.disks.s3.bucket"),
|
||||
// // 'url' => $schema->objectStorageUrl,
|
||||
// 'endpoint' => $schema->objectStorageEndpoint,
|
||||
'use_path_style_endpoint' => false,
|
||||
'visibility' => 'public', // now managed by aws policy
|
||||
'root' => storage_path('app/public'),
|
||||
'throw' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
function checkDuplicate(string $schemaName, string $checksum, int $filesize): string
|
||||
{
|
||||
|
||||
$record = DB::table("records")
|
||||
->where("schema", $schemaName)
|
||||
->where("_file->checksum", $checksum)
|
||||
->where("_file->size", $filesize)
|
||||
->first();
|
||||
|
||||
return $record->id ?? "";
|
||||
}
|
||||
|
||||
function createThumbnail(Filesystem $disk, string $schemaPath, string $filename, UploadedFile $file): void
|
||||
{
|
||||
|
||||
|
||||
$thumbDir = storage_path("app/public/thumbs/" . $schemaPath . "/");
|
||||
if (!file_exists($thumbDir)) {
|
||||
make_dir_r($thumbDir);
|
||||
}
|
||||
|
||||
try {
|
||||
ImageManagerStatic::configure(['driver' => 'imagick']);
|
||||
$image = ImageManagerStatic::make($file);
|
||||
} catch (Exception $e) {
|
||||
logger($e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$image->fit(300, 300);
|
||||
try {
|
||||
$image->encode('webp', 75)->save($thumbDir .$filename);
|
||||
} catch (Exception $e) {
|
||||
logger($e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Lucent\Edge\EdgeService;
|
||||
use Lucent\LucentException;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class EdgeController extends Controller
|
||||
{
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$edge = EdgeService::create(
|
||||
source: $request->input("source"),
|
||||
target: $request->input("target"),
|
||||
sourceSchema: $request->input("sourceSchema"),
|
||||
targetSchema: $request->input("targetSchema"),
|
||||
field: $request->input("field"),
|
||||
rank: $request->input("rank") ?? "",
|
||||
);
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
|
||||
return ok([
|
||||
"edge" => $edge,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Channel\ChannelRepo;
|
||||
use Lucent\File\FileUploadResult;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\RecordService;
|
||||
use function Lucent\File\uploadFile;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class FileController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecordService $recordService,
|
||||
private readonly Query $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$validator = Validator::make(request()->all(), [
|
||||
'files.*' => 'required|file|max:100000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return fail($validator->errors()->first());
|
||||
}
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->firstWhere("name", $request->input("schema"));
|
||||
$files = request()->file('files');
|
||||
|
||||
|
||||
$uploadResults = collect($files)->map(fn($file) => uploadFile($schema, $file))->toArray();
|
||||
$insertedIds = collect($uploadResults)
|
||||
->filter(fn(FileUploadResult $res) => !$res->isDuplicate)
|
||||
->values()
|
||||
->map(function (FileUploadResult $uploadResult) use ($schema, $request) {
|
||||
|
||||
|
||||
return $this->recordService->create(
|
||||
userId: AuthService::currentUserId($request),
|
||||
schemaName: $schema->name,
|
||||
data: [],
|
||||
file: (array)$uploadResult->recordFile,
|
||||
edges: [],
|
||||
status: $request->input("status") ?? "published",
|
||||
uploadFromUrl: ""
|
||||
);
|
||||
|
||||
})->toArray();
|
||||
|
||||
|
||||
return ok($insertedIds);
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Channel\ChannelRepo;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\RecordService;
|
||||
use Lucent\Schema\Validator\ValidatorException;
|
||||
use Throwable;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class RecordController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecordService $recordService,
|
||||
private readonly Query $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function records(Request $request)
|
||||
{
|
||||
$channel = ChannelRepo::current();
|
||||
$urlParams = $request->all();
|
||||
$sort = data_get($urlParams, "sort") ?? "-_sys.updatedAt";
|
||||
$filter = data_get($urlParams, "filter") ?? [];
|
||||
$arguments = array_merge([
|
||||
|
||||
], $filter);
|
||||
|
||||
$skip = data_get($urlParams, "skip") ?? 0;
|
||||
$limit = data_get($urlParams, "limit") ?? 15;
|
||||
$queryResult = $this->query
|
||||
->filter($arguments)
|
||||
->limit($limit)
|
||||
->skip($skip)
|
||||
->sort($sort)
|
||||
->childrenDepth($request->input("childrenDepth") ?? 1)
|
||||
->parentsDepth($request->input("parentsDepth") ?? 0)
|
||||
->runWithCount();
|
||||
|
||||
$graph = $queryResult->getQueryRecords($channel->schemas);
|
||||
$total = $queryResult->getTotal();
|
||||
|
||||
return ok([
|
||||
"graph" => $graph->toArray(),
|
||||
"sort" => $sort,
|
||||
"limit" => $limit,
|
||||
"skip" => $skip,
|
||||
"total" => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
|
||||
try {
|
||||
|
||||
$recordId = $this->recordService->create(
|
||||
userId: $request->input("userId"),
|
||||
schemaName: $request->input("schema"),
|
||||
data: $request->input("data") ?? [],
|
||||
file: $request->input("file") ?? [],
|
||||
edges: $request->input("edges") ?? [],
|
||||
status: $request->input("status") ?? "draft",
|
||||
uploadFromUrl: $request->input("uploadFromUrl") ?? ""
|
||||
);
|
||||
|
||||
} catch (ValidatorException $th) {
|
||||
return fail($th->getValidatorErrors());
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok(["id" => $recordId]);
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
|
||||
try {
|
||||
$this->recordService->update(
|
||||
userId: $request->input("userId"),
|
||||
id: $request->route("id"),
|
||||
data: $request->input("data"),
|
||||
status: $request->input("status"),
|
||||
edges: $request->input("edges") ?? [],
|
||||
updateEdges: false,
|
||||
);
|
||||
} catch (ValidatorException $th) {
|
||||
return fail($th->getValidatorErrors());
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
} catch (Throwable $th) {
|
||||
if ($th->getCode() == 11000) {
|
||||
return fail("ID has to be unique in the channel");
|
||||
}
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Schema\SchemaService;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class SchemaController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SchemaService $schemaService
|
||||
)
|
||||
{
|
||||
}
|
||||
// public function find(Request $request)
|
||||
// {
|
||||
// $cid = $request->header("CHANNEL-ID");
|
||||
// $channelContext = ChannelContext::fromId($cid)->withSchemas();
|
||||
// return ok($channelContext->getSchemas());
|
||||
// }
|
||||
|
||||
// public function findOne(Request $request, string $name)
|
||||
// {
|
||||
// $cid = $request->header("CHANNEL-ID");
|
||||
// $channelContext = ChannelContext::fromId($cid)->withSchemas();
|
||||
// $schema = SchemaRepo::context($channelContext)->findByName($name);
|
||||
// return ok($schema->toArray());
|
||||
// }
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$schema = $this->schemaService->create(
|
||||
name: $request->input("name"),
|
||||
label: $request->input("label"),
|
||||
type: $request->input("type"),
|
||||
isEntry: $request->input("isEntry"),
|
||||
revisionRetentionDays: $request->input("revisionRetentionDays"),
|
||||
revisionRetentionNumber: $request->input("revisionRetentionNumber"),
|
||||
trashedRetentionDays: $request->input("trashedRetentionDays"),
|
||||
fields: $request->input("fields"),
|
||||
titleTemplate: $request->input("titleTemplate") ?? "",
|
||||
visible: $request->input("visible") ?? [],
|
||||
path: $request->input("path") ?? "",
|
||||
);
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok($schema->toArray());
|
||||
}
|
||||
|
||||
// public function update(Request $request)
|
||||
// {
|
||||
// $cid = $request->header("CHANNEL-ID");
|
||||
// try {
|
||||
// $channelContext = ChannelContext::fromId($cid);
|
||||
// SchemaRepo::context($channelContext)->update($request->all());
|
||||
// } catch (\Throwable $th) {
|
||||
// return fail($th);
|
||||
// }
|
||||
|
||||
// $schema = SchemaRepo::context($channelContext)->findByName($request->input("name"));
|
||||
// return ok($schema->toArray());
|
||||
// }
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->schemaService->delete($request->route("name"));
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
|
||||
return ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Edge\EdgeService;
|
||||
use Lucent\Query\Query;
|
||||
|
||||
class EdgeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
public EdgeService $edgeService,
|
||||
public Query $query,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function insertMany(Request $request)
|
||||
{
|
||||
|
||||
$this->edgeService->createMany(
|
||||
source: $request->input("source"),
|
||||
sourceSchema: $request->input("sourceSchema"),
|
||||
targetSchema: $request->input("targetSchema"),
|
||||
field: $request->input("field"),
|
||||
targets: $request->input("targets"),
|
||||
);
|
||||
$graph = $this->query
|
||||
->filter(["id" => $request->input("source")])
|
||||
->limit(1)
|
||||
->skip(0)
|
||||
->childrenDepth(2)
|
||||
->childrenLimit(200)
|
||||
->parentsDepth(1)
|
||||
->parentsLimit(200)
|
||||
->run();
|
||||
|
||||
return [
|
||||
"graph" => toArray($graph),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,11 +6,12 @@ use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\File\FileUploadResult;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\InputData\RecordInputData;
|
||||
use Lucent\Record\RecordService;
|
||||
use function Lucent\File\loadDisk;
|
||||
use function Lucent\File\uploadFile;
|
||||
use Lucent\Record\Status;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
@@ -21,6 +22,7 @@ class FileController extends Controller
|
||||
public function __construct(
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly RecordService $recordService,
|
||||
private readonly FileService $fileService,
|
||||
private readonly Query $query
|
||||
)
|
||||
{
|
||||
@@ -28,7 +30,7 @@ class FileController extends Controller
|
||||
|
||||
public function download(Request $request)
|
||||
{
|
||||
$disk = loadDisk();
|
||||
$disk = $this->fileService->loadDisk();
|
||||
return $disk->download($request->input("path"));
|
||||
}
|
||||
|
||||
@@ -46,19 +48,18 @@ class FileController extends Controller
|
||||
$files = $request->file('files');
|
||||
|
||||
|
||||
$uploadResults = collect($files)->map(fn($file) => uploadFile($schema, $file))->toArray();
|
||||
$uploadResults = collect($files)->map(fn($file) => $this->fileService->upload($schema, $file))->toArray();
|
||||
collect($uploadResults)
|
||||
->filter(fn(FileUploadResult $res) => !$res->isDuplicate)
|
||||
->values()
|
||||
->map(function (FileUploadResult $uploadResult) use ($schema) {
|
||||
|
||||
return $this->recordService->create(
|
||||
schemaName: $schema->name,
|
||||
data: [],
|
||||
file: (array)$uploadResult->recordFile,
|
||||
edges: [],
|
||||
status: "published",
|
||||
uploadFromUrl: ""
|
||||
new RecordInputData(
|
||||
schemaName: $schema->name,
|
||||
status: Status::PUBLISHED,
|
||||
|
||||
),
|
||||
file: $uploadResult->recordFile,
|
||||
);
|
||||
|
||||
})->toArray();
|
||||
|
||||
@@ -10,10 +10,12 @@ use Lucent\Channel\ChannelService;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Operator;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\InputData\EdgeInputData;
|
||||
use Lucent\Record\InputData\RecordInputData;
|
||||
use Lucent\Record\Manager;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\RecordService;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Record\Status;
|
||||
use Lucent\Schema\System;
|
||||
use Lucent\Schema\Validator\ValidatorException;
|
||||
use Lucent\Svelte\Svelte;
|
||||
@@ -250,37 +252,6 @@ class RecordController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// public function editInline(Request $request)
|
||||
// {
|
||||
// $rid = $request->route("rid");
|
||||
//
|
||||
// $graph = $this->query
|
||||
// ->filter(["id" => $rid])
|
||||
// ->limit(1)
|
||||
// ->childrenDepth(2)
|
||||
// ->parentsDepth(1)
|
||||
// ->run();
|
||||
//
|
||||
// $record = $graph->records->first();
|
||||
//
|
||||
// if(!in_array($record->schema,$this->accountService->currentReadableSchemas())){
|
||||
// return $this->svelte->render(
|
||||
// layout: "channel",
|
||||
// view: "recordNotFound",
|
||||
// title: "Schema Not Found",
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return ok(
|
||||
// [
|
||||
// "graph" => toArray($graph),
|
||||
// "record" => toArray($record)
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
public function suggestions(Request $request)
|
||||
{
|
||||
$arguments = [
|
||||
@@ -317,22 +288,21 @@ class RecordController extends Controller
|
||||
|
||||
if ($request->input("isCreateMode")) {
|
||||
$recordId = $this->recordService->create(
|
||||
schemaName: $request->input("record.schema"),
|
||||
data: $request->input("record.data"),
|
||||
id: $recordId ?? "",
|
||||
file: $request->input("record._file") ?? [],
|
||||
edges: $request->input("edges") ?? [],
|
||||
status: $request->input("record.status"),
|
||||
uploadFromUrl: ""
|
||||
data: new RecordInputData(
|
||||
$request->input("record.schema"),
|
||||
$recordId ?? "",
|
||||
$request->input("record.data"),
|
||||
Status::from($request->input("record.status")),
|
||||
),
|
||||
edges: array_map(EdgeInputData::fromArray(...), $request->input("edges") ?? [])
|
||||
);
|
||||
} else {
|
||||
|
||||
$this->recordService->update(
|
||||
$this->recordService->updateWithEdges(
|
||||
id: $request->input("record.id"),
|
||||
data: $request->input("record.data"),
|
||||
status: $request->input("record.status"),
|
||||
edges: $request->input("edges"),
|
||||
updateEdges: true,
|
||||
status: Status::from($request->input("record.status")),
|
||||
edges: array_map(EdgeInputData::fromArray(...), $request->input("edges") ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+16
-19
@@ -1,25 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Lucent\Http\Controller\Api\EdgeController;
|
||||
use Lucent\Http\Controller\Api\FileController;
|
||||
use Lucent\Http\Controller\Api\RecordController;
|
||||
use Lucent\Http\Controller\Api\SchemaController;
|
||||
|
||||
|
||||
Route::middleware('auth.api:editor')->group(function () {
|
||||
|
||||
Route::post('/records', [RecordController::class, 'create']);
|
||||
Route::put('/records/{id}', [RecordController::class, 'update']);
|
||||
|
||||
Route::post('/edges', [EdgeController::class, 'create']);
|
||||
Route::post('/files', [FileController::class, 'upload']);
|
||||
});
|
||||
|
||||
Route::middleware('auth.api:reader')->group(function () {
|
||||
Route::get('/schemas', [SchemaController::class, 'find']);
|
||||
Route::get('/schemas/{name}', [SchemaController::class, 'findOne']);
|
||||
|
||||
Route::get('/records', [RecordController::class, 'records']);
|
||||
});
|
||||
//
|
||||
//Route::middleware('auth.api:editor')->group(function () {
|
||||
//
|
||||
// Route::post('/records', [RecordController::class, 'create']);
|
||||
// Route::put('/records/{id}', [RecordController::class, 'update']);
|
||||
//
|
||||
// Route::post('/edges', [EdgeController::class, 'create']);
|
||||
// Route::post('/files', [FileController::class, 'upload']);
|
||||
//});
|
||||
//
|
||||
//Route::middleware('auth.api:reader')->group(function () {
|
||||
// Route::get('/schemas', [SchemaController::class, 'find']);
|
||||
// Route::get('/schemas/{name}', [SchemaController::class, 'findOne']);
|
||||
//
|
||||
// Route::get('/records', [RecordController::class, 'records']);
|
||||
//});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use Lucent\Http\Controller\AccountController;
|
||||
use Lucent\Http\Controller\AuthController;
|
||||
use Lucent\Http\Controller\BuildController;
|
||||
use Lucent\Http\Controller\EdgeController;
|
||||
use Lucent\Http\Controller\FileController;
|
||||
use Lucent\Http\Controller\HomeController;
|
||||
use Lucent\Http\Controller\MemberController;
|
||||
@@ -60,6 +61,10 @@ Route::group([
|
||||
Route::post('/{rid}/rollback/{version}', [RecordController::class, 'rollback']);
|
||||
});
|
||||
|
||||
Route::middleware(["lucent.auth"])->prefix("/edges")->group(function () {
|
||||
Route::post('/insert-many', [EdgeController::class, 'insertMany']);
|
||||
});
|
||||
|
||||
Route::middleware(["lucent.auth"])->group(function () {
|
||||
Route::get('/records/{rid}/revisions', [RevisionController::class, 'index']);
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Lexorank;
|
||||
use function array_filter;
|
||||
use function chr;
|
||||
use function in_array;
|
||||
use function ord;
|
||||
use function str_split;
|
||||
use function strcmp;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
final class Lexorank
|
||||
{
|
||||
public const string MIN_CHAR = '0';
|
||||
|
||||
public const string MAX_CHAR = 'z';
|
||||
|
||||
/** Usually, database like MySQL order using only the first 1024 chars */
|
||||
public const int MAX_RANK_LEN = 1024;
|
||||
|
||||
/**
|
||||
* @var non-empty-string
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private string $rank;
|
||||
|
||||
/**
|
||||
* @throws LexorankException
|
||||
*/
|
||||
private function __construct(string $rank)
|
||||
{
|
||||
if(empty($rank)){
|
||||
$this->rank = "0";
|
||||
return;
|
||||
}
|
||||
self::rankValidator($rank);
|
||||
|
||||
$this->rank = $rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $rank
|
||||
*
|
||||
* @psalm-pure
|
||||
* @throws LexorankException
|
||||
*/
|
||||
private static function rankValidator(string $rank): void
|
||||
{
|
||||
|
||||
|
||||
if (strlen($rank) > self::MAX_RANK_LEN) {
|
||||
throw new LexorankException($rank);
|
||||
}
|
||||
|
||||
$invalidChars = array_filter(
|
||||
str_split($rank),
|
||||
static function ($char) {
|
||||
return ord($char) < ord(self::MIN_CHAR) || ord($char) > ord(self::MAX_CHAR);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
if ($invalidChars !== []) {
|
||||
throw new LexorankException($rank);
|
||||
}
|
||||
|
||||
$lastChar = substr($rank, -1);
|
||||
if ($lastChar === self::MIN_CHAR) {
|
||||
throw new LexorankException($rank);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function get(): string
|
||||
{
|
||||
return $this->rank;
|
||||
}
|
||||
|
||||
public static function fromString(string $rank): self
|
||||
{
|
||||
return new self($rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function forEmptySequence(): self
|
||||
{
|
||||
return self::fromString(self::mid(self::MIN_CHAR, self::MAX_CHAR));
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function after(self $prevRank): self
|
||||
{
|
||||
$char = substr($prevRank->get(), -1);
|
||||
|
||||
if (ord($char) + 1 >= ord(self::MAX_CHAR)) {
|
||||
return self::fromString(
|
||||
$prevRank->get() . chr(ord(self::MIN_CHAR) + 1)
|
||||
);
|
||||
}
|
||||
|
||||
$return = substr($prevRank->get(), 0, -1) . chr(ord($char) + 1);
|
||||
|
||||
|
||||
return self::fromString($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function before(self $nextRank): self
|
||||
{
|
||||
$char = substr($nextRank->get(), -1);
|
||||
|
||||
if (ord($char) - 1 <= ord(self::MIN_CHAR)) {
|
||||
$return = substr($nextRank->get(), 0, -1) . chr(ord($char) - 1) . chr(ord(self::MAX_CHAR) - 1);
|
||||
return self::fromString($return);
|
||||
}
|
||||
|
||||
$return = substr($nextRank->get(), 0, -1) . chr(ord($char) - 1);
|
||||
return self::fromString($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function betweenRanks(self $prevRank, self $nextRank): self
|
||||
{
|
||||
if (strcmp($prevRank->get(), $nextRank->get()) >= 0) {
|
||||
throw new LexorankException("problem");
|
||||
}
|
||||
|
||||
$rank = '';
|
||||
$i = 0;
|
||||
while ($i <= self::MAX_RANK_LEN) {
|
||||
$prevChar = $prevRank->getChar($i, self::MIN_CHAR);
|
||||
$nextChar = $nextRank->getChar($i, self::MAX_CHAR);
|
||||
$i++;
|
||||
|
||||
$midChar = self::mid($prevChar, $nextChar);
|
||||
if (in_array($midChar, [$prevChar, $nextChar])) {
|
||||
$rank .= $prevChar;
|
||||
continue;
|
||||
}
|
||||
|
||||
$rank .= $midChar;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return self::fromString($rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $i
|
||||
* @param non-empty-string $defaultChar
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function getChar(int $i, string $defaultChar): string
|
||||
{
|
||||
return $this->rank[$i] ?? $defaultChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $prev
|
||||
* @param non-empty-string $next
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function mid(string $prev, string $next): string
|
||||
{
|
||||
if (ord($prev) >= ord($next)) {
|
||||
return $prev;
|
||||
}
|
||||
|
||||
return chr((int)((ord($prev) + ord($next)) / 2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Lexorank;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class LexorankException extends Exception
|
||||
{
|
||||
// Redefine the exception so message isn't optional
|
||||
public function __construct(string $message, int $code = 0, Exception $previous = null)
|
||||
{
|
||||
// make sure everything is assigned properly
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Lucent\Edge\Event\EdgesUpdated;
|
||||
|
||||
class LucentEventServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $listen = [
|
||||
EdgesUpdated::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use Lucent\Commands\CompileSchemas;
|
||||
use Lucent\Commands\LiveLink;
|
||||
use Lucent\Commands\RebuildThumbnails;
|
||||
use Lucent\Commands\RemoveOrphanEdges;
|
||||
use Lucent\Event\Dispatcher;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\File\ImageService;
|
||||
use Lucent\Query\DatabaseGraph\DatabaseGraph;
|
||||
@@ -29,6 +30,10 @@ class LucentServiceProvider extends ServiceProvider
|
||||
return ChannelService::fromConfig();
|
||||
});
|
||||
|
||||
$this->app->singleton(Dispatcher::class, function () {
|
||||
return new Dispatcher();
|
||||
});
|
||||
|
||||
$this->app->bind(ImageManager::class, function () {
|
||||
return new ImageManager(['driver' => 'imagick']);
|
||||
});
|
||||
@@ -40,6 +45,8 @@ class LucentServiceProvider extends ServiceProvider
|
||||
};
|
||||
});
|
||||
|
||||
$this->app->register(LucentEventServiceProvider::class);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +84,7 @@ class LucentServiceProvider extends ServiceProvider
|
||||
View::share('manifest', $manifest);
|
||||
View::share('image', app()->make(ImageService::class));
|
||||
View::share('file', app()->make(FileService::class));
|
||||
Blade::anonymousComponentPath(__DIR__.'../front/views/components',"lucent");
|
||||
Blade::anonymousComponentPath(__DIR__ . '../front/views/components', "lucent");
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/Config/main.php' => config_path('lucent.php'),
|
||||
@@ -87,5 +94,7 @@ class LucentServiceProvider extends ServiceProvider
|
||||
__DIR__ . '/../front/dist' => public_path('vendor/lucent/dist'),
|
||||
__DIR__ . '/../front/public' => public_path('vendor/lucent/public'),
|
||||
], 'lucent');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Lucent\Record;
|
||||
|
||||
|
||||
class File
|
||||
class FileData
|
||||
{
|
||||
|
||||
function __construct(
|
||||
@@ -18,9 +18,9 @@ class File
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): File
|
||||
public static function fromArray(array $data): FileData
|
||||
{
|
||||
return new File(
|
||||
return new FileData(
|
||||
originalName: data_get($data, "originalName"),
|
||||
mime: data_get($data, "mime"),
|
||||
path: data_get($data, "path"),
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record\InputData;
|
||||
|
||||
|
||||
class EdgeInputData
|
||||
{
|
||||
public function __construct(
|
||||
public string $target,
|
||||
public string $targetSchema,
|
||||
public string $field,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data['target'], $data['targetSchema'], $data['field']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record\InputData;
|
||||
|
||||
use Lucent\Record\Status;
|
||||
|
||||
class RecordInputData
|
||||
{
|
||||
public function __construct(
|
||||
public string $schemaName,
|
||||
public ?string $id = null,
|
||||
public array $data = [],
|
||||
public Status $status = Status::DRAFT,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class QueryRecord
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public bool $isRoot,
|
||||
public ?File $_file = null,
|
||||
public ?FileData $_file = null,
|
||||
public array $_children = [],
|
||||
public array $_parents = [],
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ class Record implements JsonSerializable
|
||||
public Status $status,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public ?File $_file = null,
|
||||
public ?FileData $_file = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class Record implements JsonSerializable
|
||||
$file = json_decode($data->_file, true);
|
||||
if (!empty($file)) {
|
||||
|
||||
$file = new File(...$file);
|
||||
$file = new FileData(...$file);
|
||||
} else {
|
||||
$file = null;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ class RecordRepo
|
||||
|
||||
public static function update(Record $record): void
|
||||
{
|
||||
|
||||
$recordToDB = $record->toDB();
|
||||
DB::table("records")->where("id", $record->id)->update($recordToDB);
|
||||
}
|
||||
|
||||
+129
-83
@@ -2,19 +2,22 @@
|
||||
|
||||
namespace Lucent\Record;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Edge\EdgeCollection;
|
||||
use Lucent\Edge\EdgeService;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\Id\Id;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\InputData\EdgeInputData;
|
||||
use Lucent\Record\InputData\RecordInputData;
|
||||
use Lucent\Revision\RevisionService;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\Type;
|
||||
use Lucent\Schema\Validator\Validator;
|
||||
use Lucent\Schema\Validator\ValidatorException;
|
||||
|
||||
@@ -29,75 +32,101 @@ readonly class RecordService
|
||||
private Query $query,
|
||||
private InputFormatter $inputFormatter,
|
||||
private RecordRepo $recordRepo,
|
||||
private EdgeService $edgeService
|
||||
private EdgeService $edgeService,
|
||||
private FileService $fileService,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param RecordInputData $data
|
||||
* @param list<EdgeInputData> $edges
|
||||
* @return string
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public
|
||||
function create(
|
||||
string $schemaName,
|
||||
array $data,
|
||||
?string $id = null,
|
||||
array $file = [],
|
||||
array $edges = [],
|
||||
string $status = "draft",
|
||||
string $uploadFromUrl = "",
|
||||
public function createFromUrl(
|
||||
string $url,
|
||||
RecordInputData $data,
|
||||
array $edges
|
||||
): string
|
||||
{
|
||||
$schema = $this->channelService->getSchema($schemaName)->get();
|
||||
|
||||
$formattedData = $this->inputFormatter->fill($schemaName, new RecordData($data));
|
||||
if (empty($formattedData["id"])) {
|
||||
$formattedData["id"] = Id::new();
|
||||
$schema = $this->channelService->getSchema($data->schemaName)->get();
|
||||
if ($schema->type !== Type::FILES) {
|
||||
throw new LucentException("You can't upload a file to a regular record");
|
||||
}
|
||||
$uploadResult = FileService::create($schema, $uploadFromUrl, $file);
|
||||
$fileData = $this->fileService->createFromUrl($schema, $url);
|
||||
return $this->create($data, $fileData->recordFile, $edges);
|
||||
}
|
||||
|
||||
$newRecordId = empty($id) ? Id::new() : $id;
|
||||
$uniqueEdges = collect($edges)
|
||||
->map(function ($edge, $index) use ($newRecordId, $schemaName) {
|
||||
$edge['source'] = $newRecordId;
|
||||
$edge['sourceSchema'] = $schemaName;
|
||||
$edge["rank"] = $index;
|
||||
return (array)(new Edge(...$edge));
|
||||
})
|
||||
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
||||
->values()->toArray();
|
||||
$uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
|
||||
if ($uploadResult->isDuplicate) {
|
||||
|
||||
$this->edgeService->update($uploadResult->duplicateId, $uniqueEdgesCollection);
|
||||
return $uploadResult->duplicateId;
|
||||
public function createFromUploadedFile(
|
||||
UploadedFile $uploadedFile,
|
||||
RecordInputData $data,
|
||||
array $edges
|
||||
)
|
||||
{
|
||||
$schema = $this->channelService->getSchema($data->schemaName)->get();
|
||||
if ($schema->type !== Type::FILES) {
|
||||
throw new LucentException("You can't upload a file to a regular record");
|
||||
}
|
||||
$fileData = $this->fileService->upload($schema, $uploadedFile);
|
||||
return $this->create($data, $fileData->recordFile, $edges);
|
||||
}
|
||||
|
||||
public function createFromFileData(
|
||||
FileData $fileData,
|
||||
RecordInputData $data,
|
||||
array $edges
|
||||
)
|
||||
{
|
||||
$schema = $this->channelService->getSchema($data->schemaName)->get();
|
||||
if ($schema->type !== Type::FILES) {
|
||||
throw new LucentException("You can't upload a file to a regular record");
|
||||
}
|
||||
return $this->create($data, $fileData, $edges);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RecordInputData $data
|
||||
* @param FileData|null $file
|
||||
* @param list<EdgeInputData> $edges
|
||||
* @return string
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public function create(
|
||||
RecordInputData $data,
|
||||
?FileData $file = null,
|
||||
array $edges = []
|
||||
): string
|
||||
{
|
||||
|
||||
$formattedData = $this->inputFormatter->fill($data->schemaName, new RecordData($data->data));
|
||||
$newRecordId = empty($data->id) ? Id::new() : $data->id;
|
||||
|
||||
$record = new Record(
|
||||
id: $newRecordId,
|
||||
schema: $schema->name,
|
||||
status: Status::from($status),
|
||||
schema: $data->schemaName,
|
||||
status: $data->status,
|
||||
_sys: System::newRecord($this->authService->currentUserId()),
|
||||
data: $formattedData,
|
||||
_file: $uploadResult->recordFile,
|
||||
_file: !empty($file) ? FileData::fromArray(toArray($file)) : null,
|
||||
);
|
||||
|
||||
if (Status::from($status) === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
|
||||
if ($data->status === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($data->schemaName, $record->data);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
RecordRepo::create($record);
|
||||
$this->edgeService->update($record->id, $uniqueEdgesCollection);
|
||||
$this->revisionService->create($record, $uniqueEdgesCollection);
|
||||
$newEdges = $this->edgeService->createManyForRecord($record->id, $record->schema, $edges);
|
||||
$this->revisionService->create($record, $newEdges);
|
||||
return $record->id;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
@@ -105,9 +134,7 @@ readonly class RecordService
|
||||
public function update(
|
||||
string $id,
|
||||
array $data,
|
||||
string $status = "draft",
|
||||
array $edges = [],
|
||||
bool $updateEdges = false
|
||||
Status $status,
|
||||
): void
|
||||
{
|
||||
$record = $this->query->filter(["id" => $id])->run()->records->first();
|
||||
@@ -117,52 +144,68 @@ readonly class RecordService
|
||||
}
|
||||
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));
|
||||
|
||||
$uniqueEdgesCollection = new EdgeCollection();
|
||||
if ($updateEdges) {
|
||||
$uniqueEdges = collect($edges)
|
||||
->map(function ($edge, $index) {
|
||||
$edge["rank"] = $index;
|
||||
$edgeData = (array)(new Edge(...$edge));
|
||||
return $edgeData;
|
||||
})
|
||||
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
||||
->values()->toArray();
|
||||
$uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
|
||||
}
|
||||
|
||||
if (Status::from($status) === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($record->schema, $formattedData, $uniqueEdgesCollection);
|
||||
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
|
||||
if ($status === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($record->schema, $formattedData);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$newRecord = new Record(
|
||||
id: $record->id,
|
||||
schema: $record->schema,
|
||||
status: Status::from($status),
|
||||
status: $status,
|
||||
_sys: $record->_sys->update($this->authService->currentUserId()),
|
||||
data: $record->data->merge($formattedData),
|
||||
_file: $record->_file,
|
||||
);
|
||||
|
||||
RecordRepo::update($newRecord);
|
||||
if ($updateEdges) {
|
||||
$this->edgeService->update($newRecord->id, $uniqueEdgesCollection);
|
||||
}
|
||||
|
||||
$this->revisionService->create($newRecord, $uniqueEdgesCollection);
|
||||
$newEdges = $this->edgeService->findForSource($record->id);
|
||||
$this->revisionService->create($newRecord, $newEdges);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
* @throws ValidatorException
|
||||
*/
|
||||
public function updateWithEdges(
|
||||
string $id,
|
||||
array $data,
|
||||
Status $status,
|
||||
array $edges
|
||||
): void
|
||||
{
|
||||
$record = $this->query->filter(["id" => $id])->run()->records->first();
|
||||
|
||||
if (empty($record)) {
|
||||
throw new LucentException("Record id is missing");
|
||||
}
|
||||
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));
|
||||
|
||||
if ($status === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($record->schema, $formattedData);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
$newRecord = new Record(
|
||||
id: $record->id,
|
||||
schema: $record->schema,
|
||||
status: $status,
|
||||
_sys: $record->_sys->update($this->authService->currentUserId()),
|
||||
data: $record->data->merge($formattedData),
|
||||
_file: $record->_file,
|
||||
);
|
||||
|
||||
RecordRepo::update($newRecord);
|
||||
$newEdges = $this->edgeService->replaceManyForRecord($record->id, $record->schema, $edges);
|
||||
$this->revisionService->create($newRecord, $newEdges);
|
||||
|
||||
}
|
||||
|
||||
public function changeStatusBulk(
|
||||
string $status,
|
||||
array $recordsIds,
|
||||
@@ -195,20 +238,23 @@ readonly class RecordService
|
||||
$newEdgesData = $graph->edges
|
||||
->filter(fn(Edge $edge) => $edge->source == $recordId)
|
||||
->values()
|
||||
->map(function (Edge $edge) use ($newRecordId) {
|
||||
$edge->source = $newRecordId;
|
||||
return $edge->toArray();
|
||||
})->toArray();
|
||||
->map(fn(Edge $edge) => new EdgeInputData(
|
||||
target: $edge->target,
|
||||
targetSchema: $edge->targetSchema,
|
||||
field: $edge->field,
|
||||
))->toArray();
|
||||
|
||||
$record->id = $newRecordId;
|
||||
|
||||
return $this->create(
|
||||
schemaName: $record->schema,
|
||||
data: $record->data->toArray(),
|
||||
id: $record->id,
|
||||
file: $record->_file?->toArray() ?? [],
|
||||
edges: $newEdgesData,
|
||||
status: "draft"
|
||||
new RecordInputData(
|
||||
schemaName: $record->schema,
|
||||
id: $record->id,
|
||||
data: $record->data->toArray(),
|
||||
status: Status::DRAFT
|
||||
),
|
||||
file: $record->_file,
|
||||
edges: $newEdgesData
|
||||
);
|
||||
|
||||
}
|
||||
@@ -241,11 +287,11 @@ readonly class RecordService
|
||||
): void
|
||||
{
|
||||
$revision = $this->revisionService->getByRecordIdAndVersion($recordId, $version)->get();
|
||||
$this->update(
|
||||
$this->updateWithEdges(
|
||||
id: $revision->recordId,
|
||||
data: $revision->data->toArray(),
|
||||
edges: toArray($revision->_edges),
|
||||
updateEdges: true
|
||||
status: Status::DRAFT,
|
||||
edges: array_map(fn(Edge $edge) => new EdgeInputData($edge->target, $edge->targetSchema, $edge->field), $revision->_edges),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Revision\Listener;
|
||||
|
||||
use Lucent\Edge\Event\EdgesUpdated;
|
||||
|
||||
class CreateRevision
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(EdgesUpdated $event): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+18
-12
@@ -4,32 +4,38 @@ namespace Lucent\Revision;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Edge\EdgeCollection;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\File;
|
||||
use Lucent\Record\FileData;
|
||||
use Lucent\Record\Record;
|
||||
use Lucent\Record\RecordData;
|
||||
use Lucent\Record\System;
|
||||
use stdClass;
|
||||
|
||||
readonly class Revision
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string $recordId
|
||||
* @param string $schema
|
||||
* @param System $_sys
|
||||
* @param RecordData $data
|
||||
* @param list<Edge> $_edges
|
||||
* @param FileData|null $_file
|
||||
*/
|
||||
function __construct(
|
||||
public string $id,
|
||||
public string $recordId,
|
||||
public string $schema,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public EdgeCollection $_edges,
|
||||
public ?File $_file = null,
|
||||
public string $id,
|
||||
public string $recordId,
|
||||
public string $schema,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public array $_edges,
|
||||
public ?FileData $_file = null,
|
||||
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public static function fromRecord(Record $record, EdgeCollection $edges): Revision
|
||||
public static function fromRecord(Record $record, array $edges): Revision
|
||||
{
|
||||
return new Revision(
|
||||
id: (string)Str::uuid(),
|
||||
|
||||
@@ -4,9 +4,8 @@ namespace Lucent\Revision;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Edge\EdgeCollection;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\File;
|
||||
use Lucent\Record\FileData;
|
||||
use Lucent\Record\RecordData;
|
||||
use Lucent\Record\System;
|
||||
use PhpOption\Option;
|
||||
@@ -82,7 +81,7 @@ class RevisionRepo
|
||||
"_sys" => json_encode($revision->_sys),
|
||||
"_file" => json_encode($revision->_file),
|
||||
"data" => json_encode($revision->data),
|
||||
"_edges" => $revision->_edges->toJson(),
|
||||
"_edges" => json_encode($revision->_edges),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ class RevisionRepo
|
||||
$file = json_decode($data->_file, true);
|
||||
if (!empty($file)) {
|
||||
|
||||
$file = new File(...$file);
|
||||
$file = new FileData(...$file);
|
||||
} else {
|
||||
$file = null;
|
||||
}
|
||||
@@ -104,7 +103,7 @@ class RevisionRepo
|
||||
schema: $data->schema,
|
||||
_sys: System::fromArray(json_decode($data->_sys, true)),
|
||||
data: new RecordData(json_decode($data->data, true)),
|
||||
_edges: new EdgeCollection(...$edges),
|
||||
_edges: $edges,
|
||||
_file: $file
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Lucent\Revision;
|
||||
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Edge\EdgeCollection;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\Record;
|
||||
use PhpOption\Option;
|
||||
@@ -14,7 +13,7 @@ readonly class RevisionService
|
||||
|
||||
public function __construct(
|
||||
private ChannelService $channelService,
|
||||
private RevisionRepo $revisionRepo,
|
||||
private RevisionRepo $revisionRepo,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -38,15 +37,20 @@ readonly class RevisionService
|
||||
}
|
||||
|
||||
|
||||
public function create(Record $record, EdgeCollection $edges): void
|
||||
/**
|
||||
* @param Record $record
|
||||
* @param list<Edge> $edges
|
||||
* @return void
|
||||
*/
|
||||
public function create(Record $record, array $edges): void
|
||||
{
|
||||
$schema = $this->channelService->getSchema($record->schema)->get();
|
||||
if($schema->revisions <= 0){
|
||||
if ($schema->revisions <= 0) {
|
||||
return;
|
||||
}
|
||||
$revision = Revision::fromRecord($record, $edges);
|
||||
$this->revisionRepo->create($revision);
|
||||
$this->revisionRepo->cleanupRecord($record->id,$schema->revisions);
|
||||
$this->revisionRepo->cleanupRecord($record->id, $schema->revisions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
|
||||
class BlockSchema implements Schema
|
||||
{
|
||||
|
||||
public Type $type = Type::BLOCK;
|
||||
|
||||
/**
|
||||
* @param Collection<FieldInterface> $fields
|
||||
*/
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public Collection $fields
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -21,7 +21,8 @@ class CollectionSchema implements Schema
|
||||
public bool $isEntry = false,
|
||||
public string $color = "",
|
||||
public string $sortBy = "-_sys.updatedAt",
|
||||
public string $titleTemplate = "",
|
||||
public ?string $cardTitle = null,
|
||||
public ?string $cardImage = null,
|
||||
public int $revisions = 0,
|
||||
public array $read = [],
|
||||
public array $write = [],
|
||||
|
||||
@@ -22,7 +22,8 @@ class FilesSchema implements Schema
|
||||
public bool $isEntry = false,
|
||||
public string $sortBy = "-_sys.updatedAt",
|
||||
public string $color = "",
|
||||
public string $titleTemplate = "",
|
||||
public ?string $cardTitle = null,
|
||||
public ?string $cardImage = null,
|
||||
public int $revisions = 0,
|
||||
public array $read = [],
|
||||
public array $write = [],
|
||||
|
||||
@@ -25,7 +25,8 @@ class SchemaService
|
||||
isEntry: $schemaArr["isEntry"] ?? false,
|
||||
color: $schemaArr["color"] ?? "",
|
||||
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
|
||||
titleTemplate: $schemaArr["titleTemplate"] ?? "",
|
||||
cardTitle: $schemaArr["titleTemplate"] ?? $schemaArr["cardTitle"] ?? null,
|
||||
cardImage: $schemaArr["cardImage"] ?? null,
|
||||
revisions: $schemaArr["revisions"] ?? 0,
|
||||
read: $schemaArr["read"] ?? [],
|
||||
write: $schemaArr["write"] ?? [],
|
||||
@@ -39,15 +40,11 @@ class SchemaService
|
||||
isEntry: $schemaArr["isEntry"] ?? false,
|
||||
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
|
||||
color: $schemaArr["color"] ?? "",
|
||||
titleTemplate: $schemaArr["titleTemplate"] ?? "",
|
||||
cardTitle: $schemaArr["titleTemplate"] ?? $schemaArr["cardTitle"] ?? null,
|
||||
cardImage: $schemaArr["cardImage"] ?? null,
|
||||
revisions: $schemaArr["revisions"] ?? 0,
|
||||
read: $schemaArr["read"] ?? [],
|
||||
write: $schemaArr["write"] ?? [],
|
||||
),
|
||||
"block" => new BlockSchema(
|
||||
name: $schemaArr["name"],
|
||||
label: $schemaArr["label"],
|
||||
fields: (new Collection($schemaArr["fields"]))->map([$this, 'mapBlockFields'])
|
||||
)
|
||||
};
|
||||
|
||||
@@ -67,103 +64,4 @@ class SchemaService
|
||||
unset($field["ui"]);
|
||||
return new $className(...$field);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// /**
|
||||
// * @param array<string> $visible
|
||||
// * @throws LucentException
|
||||
// */
|
||||
// public function create(
|
||||
// string $name,
|
||||
// string $label,
|
||||
// string $type,
|
||||
// bool $isEntry,
|
||||
// int $revisionRetentionDays,
|
||||
// int $revisionRetentionNumber,
|
||||
// int $trashedRetentionDays,
|
||||
// array $fields,
|
||||
// string $titleTemplate = "",
|
||||
// array $visible = [],
|
||||
// string $path = ""
|
||||
// ): Schema
|
||||
// {
|
||||
// if (empty($name) || empty($label)) {
|
||||
// throw new LucentException("Name and Label are required");
|
||||
// }
|
||||
//
|
||||
// $newFields = [];
|
||||
// if (!empty($fields)) {
|
||||
// $newFields = array_map([Field::class, 'fromArray'], $fields);
|
||||
// }
|
||||
//
|
||||
// $schema = new Schema(
|
||||
// name: new SchemaName($name),
|
||||
// label: $label,
|
||||
// type: Type::from($type),
|
||||
// visible: new Collection($visible),
|
||||
// fields: new Collection($newFields),
|
||||
// isEntry: $isEntry,
|
||||
// color: "",
|
||||
// titleTemplate: $titleTemplate,
|
||||
// views: new Collection(),
|
||||
// revisionRetentionDays: $revisionRetentionDays,
|
||||
// revisionRetentionNumber: $revisionRetentionNumber,
|
||||
// trashedRetentionDays: $trashedRetentionDays,
|
||||
// path: $path,
|
||||
// );
|
||||
//
|
||||
// $this->schemaRepo->insert($schema);
|
||||
// return $schema;
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @param array<string> $visible
|
||||
// * @throws LucentException
|
||||
// */
|
||||
// public function update(
|
||||
// string $name,
|
||||
// string $label,
|
||||
// bool $isEntry,
|
||||
// string $color,
|
||||
// array $visible,
|
||||
// string $titleTemplate,
|
||||
// int $revisionRetentionDays,
|
||||
// int $revisionRetentionNumber,
|
||||
// int $trashedRetentionDays,
|
||||
// string $path = ""
|
||||
// ): Schema
|
||||
// {
|
||||
// if (empty($name) || empty($label)) {
|
||||
// throw new LucentException("Name and Label are required");
|
||||
// }
|
||||
//
|
||||
//
|
||||
// $channel = ChannelRepo::current();
|
||||
// $schema = $channel->schemas->firstWhere("name", $name);
|
||||
// $schema->label = $label;
|
||||
// $schema->isEntry = $isEntry;
|
||||
// $schema->color = $color;
|
||||
// $schema->visible = new Collection($visible);
|
||||
// $schema->titleTemplate = $titleTemplate;
|
||||
// $schema->revisionRetentionDays = $revisionRetentionDays;
|
||||
// $schema->revisionRetentionNumber = $revisionRetentionNumber;
|
||||
// $schema->trashedRetentionDays = $trashedRetentionDays;
|
||||
// $schema->path = $path;
|
||||
// $this->schemaRepo->update($schema);
|
||||
// return $schema;
|
||||
// }
|
||||
//
|
||||
// public function delete(string $name): void
|
||||
// {
|
||||
// $channel = ChannelRepo::current();
|
||||
// $schema = $channel->schemas->firstWhere("name", $name);
|
||||
// if ($schema) {
|
||||
// $this->schemaRepo->delete($schema);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Lucent\Schema\Validator;
|
||||
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Edge\EdgeCollection;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\RecordData;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
@@ -23,21 +22,20 @@ class Validator
|
||||
* @return Collection<ValidatorError>
|
||||
*/
|
||||
public function check(
|
||||
string $schemaName,
|
||||
RecordData $data,
|
||||
?EdgeCollection $edges,
|
||||
string $schemaName,
|
||||
RecordData $data,
|
||||
): Collection
|
||||
{
|
||||
|
||||
$schema = $this->channelService->getSchema($schemaName)->get();
|
||||
return $schema->fields
|
||||
->map(fn(FieldInterface $f) => $this->validate($f, $data, $edges))
|
||||
->map(fn(FieldInterface $f) => $this->validate($f, $data))
|
||||
->filter(fn(?ValidatorError $error) => !empty($error))
|
||||
->values();
|
||||
}
|
||||
|
||||
|
||||
public function validate(FieldInterface $field, RecordData $recordData, ?EdgeCollection $edges): ?ValidatorError
|
||||
public function validate(FieldInterface $field, RecordData $recordData): ?ValidatorError
|
||||
{
|
||||
$value = $recordData->get($field->name);
|
||||
if ($field instanceof RequiredInterface && $field->required && $field->failRequired($value)) {
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Lucent\ViewModel;
|
||||
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\Status;
|
||||
use Lucent\Schema\CollectionSchema;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\FilesSchema;
|
||||
@@ -36,6 +35,6 @@ class ViewModel
|
||||
}
|
||||
|
||||
$m = new Mustache_Engine(array('entity_flags' => ENT_QUOTES));
|
||||
return $m->render($schema->titleTemplate, $record->data);
|
||||
return $m->render($schema->cardTitle, $record->data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user