init
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\AccessKey;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Member\Role;
|
||||
|
||||
class AccessKey
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public readonly string $_id,
|
||||
public readonly string $name,
|
||||
public readonly Role $role,
|
||||
public readonly string $token,
|
||||
private readonly ?string $showOnceToken = null,
|
||||
) {
|
||||
$validator = Validator::make($this->toArray(), [
|
||||
'name' => 'min:3,max:120',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new LucentException($validator->errors()->first());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function fromArray(array $data): AccessKey
|
||||
{
|
||||
|
||||
return new AccessKey(
|
||||
_id: data_get($data, "_id"),
|
||||
name: data_get($data, "name"),
|
||||
role: Role::from(data_get($data, "role")),
|
||||
token: data_get($data, "token"),
|
||||
);
|
||||
}
|
||||
|
||||
public function getShowOnceToken(): ?string
|
||||
{
|
||||
return $this->showOnceToken;
|
||||
}
|
||||
|
||||
public function isValid(string $token): bool
|
||||
{
|
||||
return Hash::check($token, $this->token);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return \json_decode(\json_encode($this), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\AccessKey;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Lucent\Channel\ChannelContext;
|
||||
use Lucent\DB\Monger;
|
||||
|
||||
class AccessKeyRepo
|
||||
{
|
||||
|
||||
public static function findByToken(string $token): ?AccessKey
|
||||
{
|
||||
$channel = ChannelContext::get()->channel;
|
||||
return $channel->accessKeys->firstWhere("token",$token);
|
||||
}
|
||||
|
||||
public static function add(AccessKey $accessKey): void
|
||||
{
|
||||
$channel = ChannelContext::get()->channel;
|
||||
Monger::central()->updateOne("channels", ["_id" => $channel->_id], [
|
||||
'$push' => [
|
||||
"accessKeys" => $accessKey->toArray()
|
||||
],
|
||||
'$set' => [
|
||||
"updatedAt" => Carbon::now()->toJson(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public static function remove(string $id): void
|
||||
{
|
||||
$channel = ChannelContext::get()->channel;
|
||||
Monger::central()->updateOne("channels", ["_id" => $channel->_id], [
|
||||
'$pull' => [
|
||||
"accessKeys" => ["_id" => $id]
|
||||
],
|
||||
'$set' => [
|
||||
"updatedAt" => Carbon::now()->toJson(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\AccessKey;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Lucent\Id\Id;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Member\Role;
|
||||
|
||||
class AccessKeyService
|
||||
{
|
||||
|
||||
public static function create(string $name, string $role): AccessKey
|
||||
{
|
||||
$showOnceToken = Str::random(48);
|
||||
$accessKey = new AccessKey(
|
||||
_id: Id::new(),
|
||||
name: $name,
|
||||
token: hash("sha256", $showOnceToken),
|
||||
role: Role::from($role),
|
||||
showOnceToken: $showOnceToken,
|
||||
);
|
||||
|
||||
AccessKeyRepo::add($accessKey);
|
||||
return $accessKey;
|
||||
}
|
||||
|
||||
public static function findByToken(string $token): ?AccessKey
|
||||
{
|
||||
$hashedToken = hash("sha256", $token);
|
||||
return AccessKeyRepo::findByToken($hashedToken);
|
||||
}
|
||||
|
||||
public static function remove(string $id): void
|
||||
{
|
||||
|
||||
AccessKeyRepo::remove($id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\AccessKey;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Support\Collection<int|string, AccessKey>
|
||||
*/
|
||||
final class AccessKeysCollection extends Collection
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
AccessKey ...$array
|
||||
) {
|
||||
parent::__construct($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AccessKey[]
|
||||
**/
|
||||
public function toArray(): array
|
||||
{
|
||||
return collect($this)->values()->toArray();
|
||||
}
|
||||
|
||||
|
||||
public function toDB(): array
|
||||
{
|
||||
return \json_decode(\json_encode($this), true);
|
||||
}
|
||||
|
||||
|
||||
public static function fromArray(array $data): AccessKeysCollection
|
||||
{
|
||||
$item = array_map([AccessKey::class, 'fromArray'], $data);
|
||||
return new AccessKeysCollection(...$item);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\AccessKey;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Lucent\Account\Role;
|
||||
|
||||
class ApiMiddleware
|
||||
{
|
||||
|
||||
public function handle(Request $request, Closure $next, string $accessLevel): Response
|
||||
{
|
||||
|
||||
$bearerToken = $request->header('Authorization');
|
||||
$token = str_replace('Bearer ', '', $bearerToken);
|
||||
|
||||
$role = match ($token) {
|
||||
config("lucent.read_key") => Role::READER,
|
||||
config("lucent.write_key") => Role::EDITOR,
|
||||
config("lucent.developer_key") => Role::DEVELOPER,
|
||||
default => ""
|
||||
};
|
||||
|
||||
if (empty($role)) {
|
||||
abort(401);
|
||||
}
|
||||
|
||||
|
||||
if (!$role->hasAccess($accessLevel)) {
|
||||
abort(401);
|
||||
}
|
||||
|
||||
$request->mergeIfMissing(['userId' => "system"]);
|
||||
$request->mergeIfMissing(['userRole' => $role->value]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
|
||||
readonly class AccountService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private UserRepo $userRepo,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<User>
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
return $this->userRepo->all();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Collection<UserProfile>
|
||||
*/
|
||||
public function allProfiles(): Collection
|
||||
{
|
||||
return $this->all()->map(fn($user) => $user->safe());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Mail\LoginMail;
|
||||
|
||||
readonly class AuthService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private ChannelService $channelService,
|
||||
private UserRepo $userRepo,
|
||||
public Session $session,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function currentUserId(): ?string
|
||||
{
|
||||
return $this->session->get("user.id");
|
||||
}
|
||||
|
||||
public function isLoggedIn(): bool
|
||||
{
|
||||
return !empty($this->currentUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function login(string $email, string $token): void
|
||||
{
|
||||
|
||||
$user = $this->userRepo->findByEmail(new Email($email));
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new LucentException("You account was not found");
|
||||
}
|
||||
|
||||
if ($user->get()->role === Role::REMOVED) {
|
||||
throw new LucentException("Your account is not active");
|
||||
}
|
||||
|
||||
if ($user->get()->mailToken !== $token) {
|
||||
throw new LucentException("Token has expired or is invalid");
|
||||
}
|
||||
|
||||
if (Carbon::parse($user->get()->loggedInAt)->lte(Carbon::now()->subHours(1))) {
|
||||
throw new LucentException("Token has expired.");
|
||||
}
|
||||
|
||||
$newUser = $user->get();
|
||||
$newUser->updatedAt = Carbon::now()->toJson();
|
||||
$newUser->mailToken = null;
|
||||
$this->userRepo->update($newUser);
|
||||
|
||||
$this->session->put(["user" => $user->get()->safe()]);
|
||||
}
|
||||
|
||||
|
||||
public function create(string $name, string $email, string $role): User
|
||||
{
|
||||
$user = new User(
|
||||
id: (string)Str::uuid(),
|
||||
name: new Name($name),
|
||||
email: new Email($email),
|
||||
role: Role::from($role),
|
||||
createdAt: Carbon::now()->toJson(),
|
||||
updatedAt: Carbon::now()->toJson(),
|
||||
loggedInAt: Carbon::now()->toJson(),
|
||||
mailToken: Token::new(32),
|
||||
);
|
||||
|
||||
$this->userRepo->insert($user);
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function sendLoginEmail(string $email): void
|
||||
{
|
||||
$emailAddress = (new Email($email));
|
||||
$user = $this->userRepo->findByEmail($emailAddress);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new LucentException("User not found");
|
||||
}
|
||||
|
||||
if ($user->get()->role === Role::REMOVED) {
|
||||
throw new LucentException("Cannot reset email if the user is not active");
|
||||
}
|
||||
|
||||
|
||||
$newToken = $this->userRepo->updateLoginToken($user->get()->id);
|
||||
|
||||
Mail::to($email)->send(
|
||||
new LoginMail(
|
||||
$email,
|
||||
$newToken,
|
||||
$this->channelService->channel->lucentUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function changeRole(string $userId, string $newRole): void
|
||||
{
|
||||
$user = $this->userRepo->findById($userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new LucentException("User not found");
|
||||
}
|
||||
|
||||
$newUser = $user->get();
|
||||
$newUser->role = Role::from($newRole);
|
||||
$newUser->updatedAt = Carbon::now()->toJson();
|
||||
$this->userRepo->update($newUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function updateName(string $userId, string $name): void
|
||||
{
|
||||
$name = (new Name($name));
|
||||
$this->userRepo->updateName($userId, $name);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function invite(
|
||||
string $name,
|
||||
string $email,
|
||||
string $role
|
||||
): User
|
||||
{
|
||||
$user = $this->create($name, $email, $role);
|
||||
$this->sendLoginEmail($user->email);
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
use JsonSerializable;
|
||||
use Lucent\Validator\Validator;
|
||||
|
||||
|
||||
class Email implements JsonSerializable
|
||||
{
|
||||
private string $value;
|
||||
function __construct(string $value)
|
||||
{
|
||||
Validator::single("Email", $value, "required|email|min:6|max:50");
|
||||
$this->value = \strtolower($value);
|
||||
}
|
||||
|
||||
function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
function __toString(): string{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
use JsonSerializable;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Validator\Validator;
|
||||
|
||||
|
||||
class Name implements JsonSerializable
|
||||
{
|
||||
public string $value;
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
function __construct(string $value)
|
||||
{
|
||||
Validator::single("Name", $value, "required|min:2|max:50");
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
enum Role: string
|
||||
{
|
||||
case ADMIN = 'admin';
|
||||
case EDITOR = 'editor';
|
||||
case READER = 'reader';
|
||||
case REMOVED = 'removed';
|
||||
|
||||
function hasAccess(string $roleName): bool
|
||||
{
|
||||
$trialRole = Role::from($roleName);
|
||||
|
||||
$access = match ($trialRole) {
|
||||
Role::ADMIN => [Role::ADMIN, Role::DEVELOPER, Role::EDITOR, Role::READER],
|
||||
Role::EDITOR => [Role::EDITOR, Role::READER],
|
||||
Role::READER => [Role::READER],
|
||||
Role::REMOVED => [],
|
||||
};
|
||||
|
||||
return in_array($trialRole, $access);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
|
||||
class Token
|
||||
{
|
||||
|
||||
public static function new(int $length = 64): string
|
||||
{
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
|
||||
class User
|
||||
{
|
||||
|
||||
function __construct(
|
||||
public string $id,
|
||||
public Name $name,
|
||||
public Email $email,
|
||||
public Role $role,
|
||||
public string $createdAt,
|
||||
public string $updatedAt,
|
||||
public ?string $loggedInAt = null,
|
||||
public ?string $mailToken = null,
|
||||
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): User
|
||||
{
|
||||
return new User(
|
||||
id: $data["id"],
|
||||
name: new Name($data["name"] ?? ""),
|
||||
email: new Email($data["email"]),
|
||||
role: Role::tryFrom($data["role"]),
|
||||
createdAt: $data["createdAt"],
|
||||
updatedAt: $data["updatedAt"],
|
||||
loggedInAt: $data["loggedInAt"] ?? null,
|
||||
mailToken: $data["mailToken"] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function safe(): array
|
||||
{
|
||||
$userData = collect(toArray($this));
|
||||
return $userData->only(["id", "name", "email", "role", "status"])->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
readonly class UserProfile
|
||||
{
|
||||
|
||||
function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public string $email,
|
||||
public Role $role,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromUser(User $user): UserProfile
|
||||
{
|
||||
return new UserProfile(
|
||||
$user->id,
|
||||
$user->name,
|
||||
$user->email,
|
||||
$user->role,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): ?UserProfile
|
||||
{
|
||||
if (empty($data)) {
|
||||
return null;
|
||||
}
|
||||
return new UserProfile(
|
||||
id: data_get($data, "id"),
|
||||
name: data_get($data, "name"),
|
||||
email: data_get($data, "email"),
|
||||
role: data_get($data, "role"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Account;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Primitive\Collection;
|
||||
use PhpOption\Option;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return DB::table("users")->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<User>
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
$usersData = DB::table("users")->whereNot("status", "invite")->get();
|
||||
|
||||
$users = array_map(fn($userData) => User::fromArray((array)$userData), $usersData->toArray());
|
||||
return new Collection($users);
|
||||
}
|
||||
|
||||
|
||||
public static function insert(User $user): void
|
||||
{
|
||||
$userData = toArray($user);
|
||||
DB::table("users")->insert($userData);
|
||||
}
|
||||
|
||||
public function update(User $user): void
|
||||
{
|
||||
$userData = toArray($user);
|
||||
DB::table("users")->where("id", $user->id)->update($userData);
|
||||
}
|
||||
|
||||
|
||||
public function updateLoginToken(string $id): string
|
||||
{
|
||||
$newToken = Token::new(32);
|
||||
|
||||
DB::table("users")
|
||||
->where("id", $id)
|
||||
->update([
|
||||
'loggedInAt' => Carbon::now()->toJson(),
|
||||
'mailToken' => $newToken,
|
||||
]);
|
||||
|
||||
return $newToken;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Option<User>
|
||||
*/
|
||||
public function findByEmail(Email $email): Option
|
||||
{
|
||||
$user = DB::table("users")->where("email", $email->value())->first();
|
||||
|
||||
if (empty($user)) {
|
||||
return none();
|
||||
}
|
||||
|
||||
return some(User::fromArray((array)$user));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Option<User>
|
||||
*/
|
||||
public function findById(string $id): Option
|
||||
{
|
||||
$user = DB::table("users")->where("id", $id)->first();
|
||||
|
||||
if (empty($user)) {
|
||||
return none();
|
||||
}
|
||||
|
||||
return some(User::fromArray((array)$user));
|
||||
}
|
||||
|
||||
|
||||
public function updateName(string $userId, Name $name): void
|
||||
{
|
||||
DB::table("users")->where("id", $userId)->update(["name" => $name->value]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent;
|
||||
|
||||
use ArrayAccess;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @implements ArrayAccess<string,mixed>
|
||||
*/
|
||||
class ArrayContainer implements ArrayAccess, JsonSerializable
|
||||
{
|
||||
public function __construct(public array $data)
|
||||
{
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->data[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$this->data[] = $value;
|
||||
} else {
|
||||
$this->data[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->data[$offset]);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->data[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->data[$offset] ?? null;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Channel;
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Schema\Schema;
|
||||
|
||||
final class Channel
|
||||
{
|
||||
public string $lucentUrl;
|
||||
public string $filesUrl;
|
||||
|
||||
/**
|
||||
* @param Collection<Schema> $schemas
|
||||
*/
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $url,
|
||||
public Collection $schemas,
|
||||
public array $imageFilters,
|
||||
// public Collection $previewTargets,
|
||||
)
|
||||
{
|
||||
$this->lucentUrl = $url . "/lucent";
|
||||
$this->filesUrl = $url . "/storage";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Channel;
|
||||
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\SchemaService;
|
||||
use PhpOption\Option;
|
||||
|
||||
final class ChannelService
|
||||
{
|
||||
public Channel $channel;
|
||||
|
||||
private function __construct(
|
||||
public SchemaService $schemaService
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static function fromConfig(): ChannelService
|
||||
{
|
||||
$configJson = file_get_contents(storage_path("lucent.config.json"));
|
||||
$configArray = json_decode($configJson, true);
|
||||
$schemaService = new SchemaService();
|
||||
$schemasCollection = (new Collection($configArray["schemas"]))->map([$schemaService, 'fromArray']);
|
||||
|
||||
|
||||
$channel = new Channel(
|
||||
name: $configArray["name"],
|
||||
url: rtrim($configArray["url"], "/"),
|
||||
schemas: $schemasCollection,
|
||||
imageFilters: $configArray["imageFilters"] ?? [],
|
||||
);
|
||||
$channelService = new ChannelService($schemaService);
|
||||
$channelService->channel = $channel;
|
||||
return $channelService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Option<Schema>
|
||||
*/
|
||||
public function getSchema(string $name): Option
|
||||
{
|
||||
$schema = $this->channel->schemas->firstWhere("name", $name);
|
||||
if (empty($schema)) {
|
||||
return none();
|
||||
}
|
||||
return some($schema);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Channel;
|
||||
|
||||
|
||||
use Lucent\Validator\Validator;
|
||||
|
||||
class PreviewTarget
|
||||
{
|
||||
function __construct(
|
||||
public readonly string $label,
|
||||
public readonly string $url,
|
||||
)
|
||||
{
|
||||
Validator::single("label", $label, "required|min:2|max:50");
|
||||
Validator::single("url", $url, "required|url");
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): PreviewTarget
|
||||
{
|
||||
|
||||
return new PreviewTarget(
|
||||
label: $data["label"],
|
||||
url: $data["url"],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Commands;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CompileConfig extends Command
|
||||
{
|
||||
|
||||
protected $signature = 'lucent:compile:config {path}';
|
||||
|
||||
protected $description = 'Compiles Config';
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$configDir = base_path($this->argument('path'));
|
||||
|
||||
$configJson = file_get_contents($configDir . "lucent.json");
|
||||
$config = json_decode($configJson, true);
|
||||
$schemasDirIterator = new DirectoryIterator($configDir . "Schemas");
|
||||
$schemas = [];
|
||||
foreach ($schemasDirIterator as $file) {
|
||||
if ($file->getExtension() !== "json") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$schemaJson = file_get_contents($configDir . "Schemas/" . $file->getFilename());
|
||||
$schema = json_decode($schemaJson, true);
|
||||
if (empty($schema)) {
|
||||
$this->error("Invalid JSON " . $file->getFilename());
|
||||
return 0;
|
||||
}
|
||||
$schemas[] = $schema;
|
||||
|
||||
}
|
||||
|
||||
$config["schemas"] = collect($schemas)->sortBy("label")->values()->toArray();
|
||||
$configOuputJson = json_encode($config);
|
||||
file_put_contents(storage_path("lucent.config.json"), $configOuputJson);
|
||||
|
||||
$this->info("Lucent JSON update");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Commands;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\Type;
|
||||
|
||||
class RebuildThumbnails extends Command
|
||||
{
|
||||
|
||||
protected $signature = 'lucent:rebuild:thumbnails';
|
||||
|
||||
protected $description = 'Rebuilds thumbnails for path';
|
||||
|
||||
|
||||
public function __construct(public ImageManager $imageManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
public function handle(ChannelService $channelService): int
|
||||
{
|
||||
$channelService->channel->schemas
|
||||
->where("type", Type::FILES)->values()
|
||||
->map([$this, 'rebuildThumbnails']);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function rebuildThumbnails(Schema $schema): void
|
||||
{
|
||||
|
||||
$filesDir = storage_path("app/public/" . $schema->path . "/");
|
||||
$thumbDir = storage_path("app/public/thumbs/" . $schema->path . "/");
|
||||
if (!file_exists($thumbDir)) {
|
||||
mkdir($thumbDir);
|
||||
}
|
||||
|
||||
$filesDirIterator = new DirectoryIterator($filesDir);
|
||||
|
||||
foreach ($filesDirIterator as $file) {
|
||||
|
||||
if ($file->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$image = $this->imageManager->make($filesDir . $file->getFilename());
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
$image->fit(300, 300);
|
||||
try {
|
||||
$image->encode('webp', 75)->save($thumbDir . $file->getFilename());
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->info($file->getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"view" => [
|
||||
"image_server" => env("LUCENT_IMAGE_SERVER")
|
||||
],
|
||||
"read_key" => env("LUCENT_READ_KEY"),
|
||||
"write_key" => env("LUCENT_WRITE_KEY"),
|
||||
"developer_key" => env("LUCENT_DEVELOPER_KEY")
|
||||
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Edge;
|
||||
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Validator\Validator as LucentValidator;
|
||||
|
||||
final class Edge
|
||||
{
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function __construct(
|
||||
|
||||
public string $source,
|
||||
public string $target,
|
||||
public string $sourceSchema,
|
||||
public string $targetSchema,
|
||||
public string $field,
|
||||
public string $rank = "a",
|
||||
public int $depth = 0,
|
||||
)
|
||||
{
|
||||
|
||||
|
||||
LucentValidator::single("source", $source, "required|uuid");
|
||||
LucentValidator::single("target", $target, "required|uuid");
|
||||
}
|
||||
|
||||
public function equal(Edge $edge): bool
|
||||
{
|
||||
return $this->targetSchema === $edge->targetSchema && $this->field === $edge->field && $this->target === $edge->target && $this->source === $edge->source;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return json_decode(json_encode($this), true);
|
||||
}
|
||||
|
||||
public function toDB(): array
|
||||
{
|
||||
$data = $this->toArray();
|
||||
unset($data["depth"]);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): Edge
|
||||
{
|
||||
|
||||
return new Edge(
|
||||
|
||||
source: data_get($data, 'source'),
|
||||
target: data_get($data, 'target'),
|
||||
sourceSchema: data_get($data, 'sourceSchema'),
|
||||
targetSchema: data_get($data, 'targetSchema'),
|
||||
field: data_get($data, 'field'),
|
||||
rank: data_get($data, 'rank'),
|
||||
depth: data_get($data, 'depth', 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php namespace Lucent\Edge;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\LucentException;
|
||||
use PDOException;
|
||||
|
||||
class EdgeRepo
|
||||
{
|
||||
|
||||
|
||||
public static function insert(Edge $edge): void
|
||||
{
|
||||
try {
|
||||
DB::table("edges")->insert($edge->toDB());
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == 23505) {
|
||||
throw new LucentException("Edge already exists");
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function update(string $from, EdgeCollection $edges): void
|
||||
{
|
||||
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
|
||||
DB::table("edges")->where("source", $from)->delete();
|
||||
DB::table("edges")->insert($edgesDB);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php namespace Lucent\Edge;
|
||||
|
||||
use Lucent\LucentException;
|
||||
|
||||
class EdgeService
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public static function create(
|
||||
string $source,
|
||||
string $target,
|
||||
string $sourceSchema,
|
||||
string $targetSchema,
|
||||
string $field,
|
||||
string $rank,
|
||||
): Edge
|
||||
{
|
||||
|
||||
|
||||
$edge = new Edge(
|
||||
|
||||
source: $source,
|
||||
target: $target,
|
||||
sourceSchema: $sourceSchema,
|
||||
targetSchema: $targetSchema,
|
||||
field: $field,
|
||||
rank: $rank,
|
||||
);
|
||||
EdgeRepo::insert($edge);
|
||||
return $edge;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Edge;
|
||||
|
||||
|
||||
final class QueryEdge
|
||||
{
|
||||
public function __construct(
|
||||
public string $fromSchema,
|
||||
public string $from,
|
||||
public string $schema,
|
||||
public string $field,
|
||||
public string $to,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
|
||||
|
||||
use Lucent\LucentException;
|
||||
|
||||
class Field
|
||||
{
|
||||
/**
|
||||
* @param string[] $collections
|
||||
*/
|
||||
function __construct(
|
||||
public FieldName $name,
|
||||
public string $label,
|
||||
public string $ui,
|
||||
public string $help = "",
|
||||
public bool $trashed = false,
|
||||
public bool $locked = false,
|
||||
public string $regex = "",
|
||||
public string $mime = "",
|
||||
public bool $readonly = false,
|
||||
public bool $nullable = false,
|
||||
public bool $required = false,
|
||||
public int $decimals = 0,
|
||||
public array $collections = [],
|
||||
public mixed $min = null,
|
||||
public mixed $max = null,
|
||||
public mixed $default = null,
|
||||
public string $optionsFrom = "",
|
||||
public string $optionsField = "",
|
||||
public bool $optionsSuggest = true,
|
||||
public bool $unique = false, // not used
|
||||
public string $layout = "",
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return \json_decode(\json_encode($this), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public static function fromArray(array $data): Field
|
||||
{
|
||||
return new Field(
|
||||
name: new FieldName($data["name"]),
|
||||
label:$data["label"],
|
||||
ui: $data["ui"],
|
||||
help:$data["help"] ?? "",
|
||||
trashed:$data["trashed"] ?? false,
|
||||
locked:$data["locked"] ?? false,
|
||||
regex:$data["regex"] ?? "" ,
|
||||
mime:$data["mime"] ?? "" ,
|
||||
readonly:$data["readonly"] ?? false,
|
||||
nullable:$data["nullable"] ?? false,
|
||||
required:$data["required"] ??false,
|
||||
decimals:$data["decimals"] ?? 0,
|
||||
collections:$data["collections"] ?? [],
|
||||
min:$data["min"] ?? null,
|
||||
max:$data["max"] ?? null,
|
||||
default:$data["default"] ?? null,
|
||||
optionsFrom:$data["optionsFrom"] ?? "" ,
|
||||
optionsField:$data["optionsField"] ?? "" ,
|
||||
optionsSuggest:$data["optionsSuggest"] ?? true,
|
||||
unique:$data["unique"] ?? false,
|
||||
layout:$data["layout"] ?? "" ,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Support\Collection<int|string, Field>
|
||||
*/
|
||||
final class FieldCollection extends Collection
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
Field ...$array
|
||||
) {
|
||||
parent::__construct($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema[]
|
||||
**/
|
||||
public function toArray(): array
|
||||
{
|
||||
return collect($this)->values()->toArray();
|
||||
}
|
||||
|
||||
|
||||
public function findByName(string $name): ?Field
|
||||
{
|
||||
return $this->firstWhere("name", $name);
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): FieldCollection
|
||||
{
|
||||
$items = array_map([Field::class, 'fromArray'], $data);
|
||||
return new FieldCollection(...$items);
|
||||
}
|
||||
|
||||
public static function fromDB(string $data): FieldCollection
|
||||
{
|
||||
return FieldCollection::fromArray(\json_decode($data,true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Channel\ChannelRepo;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Schema\Type as SchemaType;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
use function Lucent\Svelte\svelte;
|
||||
|
||||
class FieldController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FieldService $fieldService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function new(Request $request)
|
||||
{
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schemaName = $request->route("schemaName");
|
||||
$collections = collect($channel->schemas)->where("type", SchemaType::COLLECTION)->values()->toArray();
|
||||
$filesSchemas = collect($channel->schemas)->where("type", SchemaType::FILES)->values()->toArray();
|
||||
|
||||
$newField = new Field(
|
||||
name: new FieldName(""),
|
||||
label: "",
|
||||
ui: "text"
|
||||
);
|
||||
|
||||
return svelte(
|
||||
layout: "channel",
|
||||
view: "fieldNew",
|
||||
title: "New field",
|
||||
data: [
|
||||
"schemas" => $channel->schemas,
|
||||
"schema" => $channel->schemas->where("name", $schemaName)->first(),
|
||||
"collections" => $collections,
|
||||
"filesSchemas" => $filesSchemas,
|
||||
"field" => $newField->toArray(),
|
||||
"uiList" => UI::values(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->fieldService->create(
|
||||
schemaName: $request->route("schemaName"),
|
||||
data: $request->input("data")
|
||||
);
|
||||
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function edit(Request $request)
|
||||
{
|
||||
$channel = ChannelRepo::current();
|
||||
$schemaName = $request->route("schemaName");
|
||||
$schema = $channel->schemas->where("name.value", $schemaName)->first();
|
||||
$collections = collect($channel->schemas)->where("type", SchemaType::COLLECTION)->values()->toArray();
|
||||
$filesSchemas = collect($channel->schemas)->where("type", SchemaType::FILES)->values()->toArray();
|
||||
$field = collect($schema->fields)->where("name.value", $request->route("fid"))->first();
|
||||
return svelte(
|
||||
layout: "channel",
|
||||
view: "fieldEdit",
|
||||
title: "Edit field",
|
||||
data: [
|
||||
"schemas" => $channel->schemas,
|
||||
"schema" => $schema,
|
||||
"collections" => $collections,
|
||||
"filesSchemas" => $filesSchemas,
|
||||
"field" => $field,
|
||||
"uiList" => UI::values(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function replace(Request $request)
|
||||
{
|
||||
|
||||
$schema = $this->fieldService->replace(
|
||||
schemaName: $request->route("schemaName"),
|
||||
// fields:$request->input("fields")
|
||||
fields: json_decode($request->input("fields"), true)
|
||||
);
|
||||
return ok($schema->toArray());
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->fieldService->update(
|
||||
schemaName: $request->route("schemaName"),
|
||||
data: $request->input("data")
|
||||
);
|
||||
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function move(Request $request)
|
||||
{
|
||||
$newSchema = $this->fieldService->move(
|
||||
schemaName: $request->route("schemaName"),
|
||||
source: $request->input("source"),
|
||||
target: $request->input("target") ?? "",
|
||||
);
|
||||
|
||||
return ok($newSchema->toArray());
|
||||
}
|
||||
|
||||
public function trash(Request $request)
|
||||
{
|
||||
|
||||
try {
|
||||
$newSchema = $this->fieldService->trash(
|
||||
schemaName: $request->route("schemaName"),
|
||||
fieldName: $request->input("field"),
|
||||
);
|
||||
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok($newSchema->toArray());
|
||||
}
|
||||
|
||||
public function restore(Request $request)
|
||||
{
|
||||
try {
|
||||
$newSchema = $this->fieldService->restore(
|
||||
schemaName: $request->route("schemaName"),
|
||||
fieldName: $request->input("field"),
|
||||
);
|
||||
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok($newSchema->toArray());
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
try {
|
||||
$newSchema = $this->fieldService->delete(
|
||||
schemaName: $request->route("schemaName"),
|
||||
fieldName: $request->input("field"),
|
||||
);
|
||||
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok($newSchema->toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
use JsonSerializable;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Validator\Validator;
|
||||
|
||||
|
||||
class FieldName implements JsonSerializable
|
||||
{
|
||||
public string $value;
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
function __construct(string $value)
|
||||
{
|
||||
Validator::single("Name", $value, "min:2|max:50|alpha_dash");
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(FieldName $name): bool
|
||||
{
|
||||
return $this->value === $name->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
use Lucent\Channel\ChannelRepo;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\SchemaRepo;
|
||||
|
||||
readonly class FieldService
|
||||
{
|
||||
public function __construct(
|
||||
private SchemaRepo $schemaRepo
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function create(
|
||||
string $schemaName,
|
||||
array $data
|
||||
): Schema
|
||||
{
|
||||
if (empty($data["name"]) || empty($data["label"])) {
|
||||
throw new LucentException("Name and Label are required");
|
||||
}
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name.value", $schemaName)->first();
|
||||
$field = Field::fromArray($data);
|
||||
$this->validateNameUnique($schema, $field->name);
|
||||
$schema->fields->push($field);
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function update(
|
||||
string $schemaName,
|
||||
array $data
|
||||
): Schema
|
||||
{
|
||||
if (empty($data["name"]) || empty($data["label"])) {
|
||||
throw new LucentException("Name and Label are required");
|
||||
}
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name", $schemaName)->first();
|
||||
$field = Field::fromArray($data);
|
||||
if ($field->locked) {
|
||||
throw new LucentException("Locked fields can't get updated");
|
||||
}
|
||||
$schema->fields = $schema->fields->map(function (Field $aField) use ($field) {
|
||||
if (!$aField->name->equals($field->name)) {
|
||||
return $aField;
|
||||
}
|
||||
return $field;
|
||||
});
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function move(
|
||||
string $schemaName,
|
||||
string $source,
|
||||
string $target,
|
||||
): Schema
|
||||
{
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name", $schemaName)->first();
|
||||
|
||||
if ($source === $target) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$sourceField = $schema->fields->where("name", $source)->first();
|
||||
$fieldsWithoutSource = $schema->fields
|
||||
->where("name", "!=", $source)
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
if (empty($target)) {
|
||||
$schema->fields = new Collection([$sourceField, ...$fieldsWithoutSource]);
|
||||
} else {
|
||||
$newFields = collect($fieldsWithoutSource)->reduce(function ($carry, $afield) use ($sourceField, $target) {
|
||||
$carry[] = $afield;
|
||||
if ($afield->name->value === $target) {
|
||||
$carry[] = $sourceField;
|
||||
}
|
||||
return $carry;
|
||||
}, []);
|
||||
$schema->fields = new Collection($newFields);
|
||||
}
|
||||
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function trash(
|
||||
string $schemaName,
|
||||
string $fieldName,
|
||||
): Schema
|
||||
{
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name.value", $schemaName)->first();
|
||||
$field = $schema->fields->where("name", $fieldName)->first();
|
||||
if ($field->trashed) {
|
||||
throw new LucentException("Field is already in trash");
|
||||
}
|
||||
if ($field->locked) {
|
||||
throw new LucentException("Locked fields can't get trashed");
|
||||
}
|
||||
|
||||
$schema->fields = $schema->fields->map(function (Field $aField) use ($fieldName) {
|
||||
if ($aField->name->value !== $fieldName) {
|
||||
return $aField;
|
||||
}
|
||||
$aField->trashed = true;
|
||||
return $aField;
|
||||
});
|
||||
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function restore(
|
||||
string $schemaName,
|
||||
string $fieldName,
|
||||
): Schema
|
||||
{
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name.value", $schemaName)->first();
|
||||
$field = $schema->fields->where("name", $fieldName)->first();
|
||||
if (!$field->trashed) {
|
||||
throw new LucentException("You can only restore trashed fields");
|
||||
}
|
||||
|
||||
$schema->fields = $schema->fields->map(function (Field $aField) use ($fieldName) {
|
||||
if ($aField->name->value !== $fieldName) {
|
||||
return $aField;
|
||||
}
|
||||
$aField->trashed = false;
|
||||
return $aField;
|
||||
});
|
||||
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function delete(
|
||||
string $schemaName,
|
||||
string $fieldName,
|
||||
): Schema
|
||||
{
|
||||
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name.value", $schemaName)->first();
|
||||
$field = $schema->fields->where("name", $fieldName)->first();
|
||||
if (!$field->trashed) {
|
||||
throw new LucentException("You can only delete trashed fields");
|
||||
}
|
||||
|
||||
$schema->fields = $schema->fields
|
||||
->filter(fn(Field $aField) => $aField->name->value !== $fieldName)
|
||||
->values();
|
||||
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
}
|
||||
|
||||
public function replace(
|
||||
string $schemaName,
|
||||
array $fields
|
||||
): Schema
|
||||
{
|
||||
$channel = ChannelRepo::current();
|
||||
$schema = $channel->schemas->where("name", $schemaName)->first();
|
||||
$schema->fields = new Collection();
|
||||
|
||||
$newFields = collect($fields)->map(function ($fieldData) use ($schema) {
|
||||
|
||||
$field = Field::fromArray($fieldData);
|
||||
$this->validateNameUnique($schema, $field->name);
|
||||
return $field;
|
||||
})->toArray();
|
||||
$schema->fields = new Collection($newFields);
|
||||
$this->schemaRepo->update($schema);
|
||||
return $schema;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
private function validateNameUnique(Schema $schema, string $name): void
|
||||
{
|
||||
$fieldExists = collect($schema->fields)->where("name.value", $name)->first();
|
||||
if (!empty($fieldExists)) {
|
||||
throw new LucentException("Field with name $name exists in schema $schema->label");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
readonly class System
|
||||
{
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public string $ui,
|
||||
public bool $files,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public static function getById(string $id): System
|
||||
{
|
||||
return self::list()[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,System>
|
||||
* */
|
||||
public static function list(): array
|
||||
{
|
||||
return [
|
||||
"_sys.status" => new System(
|
||||
name: "_sys.status",
|
||||
label: "Status",
|
||||
ui: "status",
|
||||
files: false,
|
||||
),
|
||||
"_sys.createdBy" => new System(
|
||||
name: "_sys.createdBy",
|
||||
label: "Created by",
|
||||
ui: "user",
|
||||
files: false,
|
||||
),
|
||||
"_sys.updatedBy" => new System(
|
||||
name: "_sys.updatedBy",
|
||||
label: "Updated by",
|
||||
ui: "user",
|
||||
files: false,
|
||||
),
|
||||
"_sys.createdAt" => new System(
|
||||
name: "_sys.createdAt",
|
||||
label: "Created at",
|
||||
ui: "datetime",
|
||||
files: false,
|
||||
),
|
||||
"_sys.updatedAt" => new System(
|
||||
name: "_sys.updatedAt",
|
||||
label: "Updated At",
|
||||
ui: "datetime",
|
||||
files: false,
|
||||
),
|
||||
"_file.path" => new System(
|
||||
name: "_file.path",
|
||||
label: "Filename",
|
||||
ui: "text",
|
||||
files: true,
|
||||
),
|
||||
"_file.mime" => new System(
|
||||
name: "_file.mime",
|
||||
label: "Mime Type",
|
||||
ui: "text",
|
||||
files: true,
|
||||
),
|
||||
|
||||
"_file.width" => new System(
|
||||
name: "_file.width",
|
||||
label: "Dimensions",
|
||||
ui: "text",
|
||||
files: true,
|
||||
),
|
||||
"_file.size" => new System(
|
||||
name: "_file.size",
|
||||
label: "Size",
|
||||
ui: "text",
|
||||
files: true,
|
||||
),
|
||||
"_file.originalName" => new System(
|
||||
name: "_file.originalName",
|
||||
label: "Original Name",
|
||||
ui: "text",
|
||||
files: true,
|
||||
),
|
||||
"_file.checksum" => new System(
|
||||
name: "_file.checksum",
|
||||
label: "Checksum",
|
||||
ui: "text",
|
||||
files: true,
|
||||
),
|
||||
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Field;
|
||||
|
||||
use Lucent\Schema\FieldType;
|
||||
|
||||
readonly class UI
|
||||
{
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public FieldType $type,
|
||||
public bool $regex,
|
||||
public bool $mime,
|
||||
public bool $required,
|
||||
public bool $nullable,
|
||||
public bool $decimals,
|
||||
public bool $collections,
|
||||
public bool $min,
|
||||
public bool $max,
|
||||
public bool $default,
|
||||
public bool $locked,
|
||||
public bool $readonly,
|
||||
public bool $options,
|
||||
public bool $layout,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getById(string $id): array
|
||||
{
|
||||
return self::buildUis()[$id];
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_values(self::buildUis());
|
||||
}
|
||||
|
||||
public static function buildUis(): array
|
||||
{
|
||||
return [
|
||||
"uuid" => new UI(
|
||||
name: "uuid",
|
||||
label: "UUID",
|
||||
type: FieldType::STRING,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: false,
|
||||
max: false,
|
||||
default: false,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"text" => new UI(
|
||||
name: "text",
|
||||
label: "Text",
|
||||
type: FieldType::STRING,
|
||||
regex: true,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: true,
|
||||
layout: false,
|
||||
),
|
||||
"textarea" => new UI(
|
||||
name: "textarea",
|
||||
label: "Textarea",
|
||||
type: FieldType::STRING,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"color" => new UI(
|
||||
name: "color",
|
||||
label: "Color",
|
||||
type: FieldType::STRING,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: false,
|
||||
max: false,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: true,
|
||||
layout: false,
|
||||
),
|
||||
"rich" => new UI(
|
||||
name: "rich",
|
||||
label: "Rich editor",
|
||||
type: FieldType::STRING,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"block" => new UI(
|
||||
name: "block",
|
||||
label: "Block editor",
|
||||
type: FieldType::JSON,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: true,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"file" => new UI(
|
||||
name: "file",
|
||||
label: "File",
|
||||
type: FieldType::FILE,
|
||||
regex: false,
|
||||
mime: true,
|
||||
required: false,
|
||||
nullable: false,
|
||||
decimals: false,
|
||||
collections: true,
|
||||
min: true,
|
||||
max: true,
|
||||
default: false,
|
||||
locked: false,
|
||||
readonly: false, // feature for later
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"reference" => new UI(
|
||||
name: "reference",
|
||||
label: "Reference",
|
||||
type: FieldType::REFERENCE,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: false,
|
||||
nullable: false,
|
||||
decimals: false,
|
||||
collections: true,
|
||||
min: true,
|
||||
max: true,
|
||||
default: false,
|
||||
locked: false,
|
||||
readonly: false, // feature for later
|
||||
options: false,
|
||||
layout: true,
|
||||
),
|
||||
"checkbox" => new UI(
|
||||
name: "checkbox",
|
||||
label: "Checkbox",
|
||||
type: FieldType::BOOLEAN,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: false,
|
||||
max: false,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"number" => new UI(
|
||||
name: "number",
|
||||
label: "Number",
|
||||
type: FieldType::NUMBER,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: true,
|
||||
collections: false,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: true,
|
||||
layout: false,
|
||||
),
|
||||
"date" => new UI(
|
||||
name: "date",
|
||||
label: "Date",
|
||||
type: FieldType::STRING,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: true,
|
||||
layout: false,
|
||||
),
|
||||
"datetime" => new UI(
|
||||
name: "datetime",
|
||||
label: "Datetime",
|
||||
type: FieldType::STRING,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: true,
|
||||
max: true,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: true,
|
||||
layout: false,
|
||||
),
|
||||
"json" => new UI(
|
||||
name: "json",
|
||||
label: "JSON",
|
||||
type: FieldType::JSON,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: true,
|
||||
nullable: true,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: false,
|
||||
max: false,
|
||||
default: true,
|
||||
locked: true,
|
||||
readonly: true,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
"tab" => new UI(
|
||||
name: "tab",
|
||||
label: "Tab",
|
||||
type: FieldType::TAB,
|
||||
regex: false,
|
||||
mime: false,
|
||||
required: false,
|
||||
nullable: false,
|
||||
decimals: false,
|
||||
collections: false,
|
||||
min: false,
|
||||
max: false,
|
||||
default: false,
|
||||
locked: true,
|
||||
readonly: false,
|
||||
options: false,
|
||||
layout: false,
|
||||
),
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Record\File;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\Type;
|
||||
|
||||
class FileService
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public static function create(Schema $schema, string $uploadFromUrl, array $file): 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) {
|
||||
throw new LucentException("Failed to upload file from url");
|
||||
}
|
||||
$file = '/tmp/' . $pathinfo['basename'];
|
||||
file_put_contents($file, $contents);
|
||||
return new UploadedFile($file, $pathinfo['basename']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Lucent\Record\File;
|
||||
|
||||
class FileUploadResult
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public ?File $recordFile,
|
||||
public string $duplicateId,
|
||||
public bool $isDuplicate,
|
||||
)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Log\Logger;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Record\QueryRecord;
|
||||
|
||||
class ImageService
|
||||
{
|
||||
|
||||
private string $notFoundImage = "/not-found.jpg";
|
||||
|
||||
public function __construct(
|
||||
public ImageManager $imageManager,
|
||||
public ChannelService $channelService,
|
||||
public Logger $logger
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function file(?QueryRecord $record, string $template = ""): string
|
||||
{
|
||||
if (empty($record)) {
|
||||
return $this->notFoundImage;
|
||||
}
|
||||
|
||||
$originalPath = $record->_file->path;
|
||||
$templateUri = $this->findTemplate($originalPath, $template);
|
||||
|
||||
if ($templateUri === false) {
|
||||
$templateUri = $this->createTemplate($originalPath, $template);
|
||||
}
|
||||
return $this->channelService->channel->filesUrl . "/" . $templateUri;
|
||||
}
|
||||
|
||||
private function findTemplate(string $originalPath, string $template): string|false
|
||||
{
|
||||
$templateUri = "templates/" . $template . "/" . $originalPath;
|
||||
$templateFilePath = public_path("storage/" . $templateUri);
|
||||
|
||||
if (file_exists($templateFilePath)) {
|
||||
return $templateUri;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function createTemplate(string $originalPath, string $template): string
|
||||
{
|
||||
$originalFilePath = public_path("storage/" . $originalPath);
|
||||
$templateUri = "/templates/" . $template . "/" . $originalPath;
|
||||
$templateFilePath = public_path("storage/" . $templateUri);
|
||||
if (!file_exists($originalFilePath)) {
|
||||
return $this->notFoundImage;
|
||||
}
|
||||
|
||||
if (!file_exists(pathinfo($templateFilePath, PATHINFO_DIRNAME))) {
|
||||
$this->make_dir(pathinfo($templateFilePath, PATHINFO_DIRNAME));
|
||||
}
|
||||
|
||||
try {
|
||||
$image = $this->imageManager->make($originalFilePath);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
return $this->notFoundImage;
|
||||
}
|
||||
|
||||
$image->filter(new $this->channelService->channel->imageFilters[$template]);
|
||||
try {
|
||||
$image->encode('webp', 75);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
return $this->notFoundImage;
|
||||
}
|
||||
$image->save($templateFilePath);
|
||||
return $templateUri;
|
||||
}
|
||||
|
||||
private function make_dir(string $path): void
|
||||
{
|
||||
is_dir($path) || mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?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(10)->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("_sys->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
|
||||
{
|
||||
try {
|
||||
$image = ImageManagerStatic::make($file);
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image->fit(300, 300);
|
||||
try {
|
||||
$image->encode('webp', 75);
|
||||
|
||||
$disk->put(
|
||||
"thumbs/" . $schemaPath . "/" . $filename,
|
||||
$image,
|
||||
// 'public' // now managed by aws policy
|
||||
);
|
||||
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\Auth;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
use function Lucent\Svelte\svelte;
|
||||
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return svelte(
|
||||
layout: "account",
|
||||
view: "profile",
|
||||
title: "Profile",
|
||||
data: []
|
||||
);
|
||||
}
|
||||
|
||||
public function updateName(Request $request)
|
||||
{
|
||||
|
||||
try {
|
||||
(new Auth)->updateName(session("user.id"), $request->input("name"));
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function updateEmail(Request $request)
|
||||
{
|
||||
|
||||
try {
|
||||
(new Auth)->updateEmail(session("user.id"), $request->input("email"));
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\LucentException;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
use function Lucent\Svelte\svelte;
|
||||
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthService $authService,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function profile(): View
|
||||
{
|
||||
|
||||
|
||||
return svelte(
|
||||
layout: "account",
|
||||
view: "profile",
|
||||
title: "Profile",
|
||||
data: []
|
||||
);
|
||||
}
|
||||
|
||||
public function updateName(Request $request): Response
|
||||
{
|
||||
|
||||
try {
|
||||
$this->authService->updateName(session("user.id"), $request->input("name"));
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
$request->session()->put("user.name", $request->input("name"));
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Channel\ChannelContext;
|
||||
use Lucent\Schema\FieldRepo;
|
||||
use Lucent\Schema\SchemaRepo;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
|
||||
class FieldController extends Controller
|
||||
{
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$schema = SchemaRepo::findByName($request->input("schema"));
|
||||
FieldRepo::create($schema, $request->input("field"));
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
try {
|
||||
$schema = SchemaRepo::findByName($request->input("schema"));
|
||||
FieldRepo::update($schema, $request->input("field"));
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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,126 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Account\UserRepo;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Svelte\Svelte;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthService $authService,
|
||||
private readonly Svelte $svelte,
|
||||
private readonly UserRepo $userRepo,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function register(Request $request): View
|
||||
{
|
||||
$userCount = $this->userRepo->count();
|
||||
$email = $request->input("email");
|
||||
$token = $request->input("token");
|
||||
|
||||
return svelte(
|
||||
layout: "account",
|
||||
view: "register",
|
||||
title: "Create an account",
|
||||
data: [
|
||||
'email' => $email,
|
||||
'token' => $token,
|
||||
'userCount' => $userCount
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function postRegister(Request $request): Response
|
||||
{
|
||||
|
||||
try {
|
||||
if ($request->input("isAdmin")) {
|
||||
$this->authService->registerAdmin(
|
||||
name: $request->input("name"),
|
||||
password: $request->input("password"),
|
||||
email: $request->input("email"),
|
||||
);
|
||||
} else {
|
||||
$this->authService->register(
|
||||
name: $request->input("name"),
|
||||
password: $request->input("password"),
|
||||
email: $request->input("email"),
|
||||
token: $request->input("token") ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function login(): View
|
||||
{
|
||||
return $this->svelte->render(
|
||||
layout: "account",
|
||||
view: "login",
|
||||
title: "Log in"
|
||||
);
|
||||
}
|
||||
|
||||
public function postLogin(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$this->authService->sendLoginEmail($request->input("email"));
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function verify(Request $request): View
|
||||
{
|
||||
return $this->svelte->render(
|
||||
layout: "account",
|
||||
view: "verify",
|
||||
title: "Verify and enter",
|
||||
data: [
|
||||
"email" => $request->input("email"),
|
||||
"token" => $request->input("token"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function postVerify(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$this->authService->login($request->input("email"), $request->input("token"));
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
|
||||
public function logout(): RedirectResponse
|
||||
{
|
||||
session()->flush();
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Lucent\Account\Auth;
|
||||
use Lucent\Channel\ChannelContext;
|
||||
use Lucent\Query\Options;
|
||||
use Lucent\Query\Reference;
|
||||
|
||||
class EdgeController extends Controller
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lucent\Channel\ChannelRepo;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\File\FileUploadResult;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\RecordService;
|
||||
use function Lucent\File\loadDisk;
|
||||
use function Lucent\File\uploadFile;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
|
||||
class FileController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly RecordService $recordService,
|
||||
private readonly Query $query
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function download(Request $request)
|
||||
{
|
||||
$disk = loadDisk();
|
||||
return $disk->download($request->input("path"));
|
||||
}
|
||||
|
||||
public function get(Request $request)
|
||||
{
|
||||
$manager = new ImageManager(['driver' => 'imagick']);
|
||||
$filesystem = loadDisk();
|
||||
$path = $request->route("path");
|
||||
|
||||
if ($filesystem->exists($path)) {
|
||||
$image = $manager->make($filesystem->get($path));
|
||||
return $image->response();
|
||||
}
|
||||
$arr = explode(".", $path);
|
||||
$ext = end($arr);
|
||||
$pathWithoutExtension = str_replace("." . $ext, "", $path);
|
||||
|
||||
$pathArguments = (function ($path) {
|
||||
$pathWithArgumentsAr = explode("-", $path);
|
||||
return collect($pathWithArgumentsAr)
|
||||
->filter(fn($ar) => str_contains($ar, "_"))
|
||||
->reduce(function ($carry, $arg) {
|
||||
[$k, $v] = explode("_", $arg);
|
||||
$carry[$k] = $v;
|
||||
return $carry;
|
||||
});
|
||||
|
||||
})($pathWithoutExtension);
|
||||
|
||||
$originalPath = (function ($path) use ($ext) {
|
||||
$arr = explode("-o-", $path);
|
||||
return $arr[0] . "." . $ext;
|
||||
})($path);
|
||||
$image = $manager->make($filesystem->get($originalPath));
|
||||
if (empty($pathArguments["mode"])) {
|
||||
|
||||
if (empty($pathArguments["w"])) {
|
||||
$image->resize(null, $pathArguments["h"], function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
} elseif (empty($pathArguments["h"])) {
|
||||
$image->resize($pathArguments["w"], null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
} else {
|
||||
$image->resize($pathArguments["w"], $pathArguments["h"]);
|
||||
}
|
||||
} else if ($pathArguments["mode"] === "fit") {
|
||||
$image->fit($pathArguments["w"], $pathArguments["h"]);
|
||||
}
|
||||
|
||||
$disk = loadDisk();
|
||||
// $disk->put("cache/" . $path, $image);
|
||||
$image->save(storage_path("app/public/cache/" . $path));
|
||||
return $image->response();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$validator = Validator::make(request()->all(), [
|
||||
'files.*' => 'required|file|max:100000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return fail($validator->errors()->first());
|
||||
}
|
||||
$schema = $this->channelService->channel->schemas->firstWhere("name", $request->input("schema"));
|
||||
$files = request()->file('files');
|
||||
|
||||
|
||||
$uploadResults = collect($files)->map(fn($file) => uploadFile($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: ""
|
||||
);
|
||||
|
||||
})->toArray();
|
||||
|
||||
|
||||
$queryResult = $this->query
|
||||
->filter([
|
||||
"_sys.schema" => $schema->name
|
||||
])
|
||||
->limit(15)
|
||||
->skip(0)
|
||||
->sort("-_sys.updatedAt")
|
||||
->childrenDepth(0)
|
||||
->parentsDepth(0)
|
||||
->run();
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
return ok($graph->records->toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Schema\FolderRepo;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class FolderController extends Controller
|
||||
{
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$folder = FolderRepo::create($request->input("name"));
|
||||
} catch (\Lucent\LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok((array)$folder);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $cid)
|
||||
{
|
||||
try {
|
||||
$folder = FolderRepo::update(
|
||||
id: $request->input("id"),
|
||||
name: $request->input("name"),
|
||||
);
|
||||
} catch (\Lucent\LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok((array)$folder);
|
||||
}
|
||||
|
||||
public function delete(Request $request, string $cid, string $folderid)
|
||||
{
|
||||
|
||||
try {
|
||||
FolderRepo::delete($folderid);
|
||||
} catch (\Lucent\LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Lucent\Account\UserRepo;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Svelte\Svelte;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly Svelte $svelte,
|
||||
private readonly UserRepo $userRepo,
|
||||
private readonly Query $query,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function home(): View
|
||||
{
|
||||
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "homeIndex",
|
||||
title: "Records",
|
||||
);
|
||||
}
|
||||
|
||||
public function records(Request $request): Response
|
||||
{
|
||||
$urlParams = $request->all();
|
||||
|
||||
$sort = data_get($urlParams, "sort") ?? "-_sys.updatedAt";
|
||||
$filter = data_get($urlParams, "filter") ?? [];
|
||||
$arguments = array_merge([
|
||||
"_sys.status_in" => ["draft", "published"]
|
||||
], $filter);
|
||||
|
||||
$limit = 30;
|
||||
|
||||
$queryResult = $this->query
|
||||
->filter($arguments)
|
||||
->limit($limit)
|
||||
->childrenDepth(1)
|
||||
->parentsDepth(0)
|
||||
->sort($sort)
|
||||
->run();
|
||||
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
|
||||
$users = $this->userRepo->all();
|
||||
|
||||
return ok([
|
||||
"users" => $users,
|
||||
"records" => $graph->getRootRecords()->toArray(),
|
||||
"graph" => $graph->toArray(),
|
||||
"modalUrl" => $request->fullUrl(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\AccountService;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Account\Role;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Svelte\Svelte;
|
||||
use function Lucent\Response\fail;
|
||||
|
||||
class MemberController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthService $authService,
|
||||
private readonly AccountService $accountService,
|
||||
private readonly Svelte $svelte,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "members",
|
||||
title: "Members",
|
||||
data: [
|
||||
"users" => $this->accountService->allProfiles()->toArray(),
|
||||
"roles" => Role::cases()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function invite(Request $request)
|
||||
{
|
||||
if (empty($request->input("role"))) {
|
||||
return fail("Select a role for the user");
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $this->authService->invite($request->input("name"), $request->input("email"), $request->input("role"));
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return [
|
||||
"user" => $user
|
||||
];
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->authService->changeRole($request->input("id"), $request->input("role"));
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return [
|
||||
"users" => $this->accountService->allProfiles()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\AccountService;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Account\UserRepo;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Field\System;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Operator;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\Manager;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\RecordService;
|
||||
use Lucent\Schema\Validator\ValidatorException;
|
||||
use Lucent\Svelte\Svelte;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class RecordController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecordService $recordService,
|
||||
private readonly AccountService $accountService,
|
||||
private readonly AuthService $authService,
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly Svelte $svelte,
|
||||
private readonly UserRepo $userRepo,
|
||||
private readonly Query $query,
|
||||
private readonly Manager $recordManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$schemaName = $request->route("schemaName");
|
||||
$users = $this->accountService->all();
|
||||
$schema = $this->channelService->channel->schemas->where("name", $schemaName)->first();
|
||||
$urlParams = $request->all();
|
||||
$sort = data_get($urlParams, "sort") ?? "-_sys.updatedAt";
|
||||
$filter = data_get($urlParams, "filter") ?? [];
|
||||
$arguments = array_merge([
|
||||
"_sys.schema" => $schema->name,
|
||||
"_sys.status_in" => "draft,published",
|
||||
], $filter);
|
||||
|
||||
|
||||
$skip = data_get($urlParams, "skip") ?? 0;
|
||||
$limit = 15;
|
||||
$records = [];
|
||||
$graphArray = null;
|
||||
$total = 0;
|
||||
|
||||
try {
|
||||
$queryResult = $this->query
|
||||
->filter($arguments)
|
||||
->limit($limit)
|
||||
->status(explode(",", $arguments["_sys.status_in"]))
|
||||
->skip($skip)
|
||||
->sort($sort)
|
||||
->childrenDepth(1)
|
||||
->parentsDepth(0)
|
||||
->runWithCount();
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
$graphArray = $graph->toArray();
|
||||
$total = $queryResult->getTotal();
|
||||
$records = $graph->getRootRecords()->toArray();
|
||||
} catch (SubqueryNoResultException) {
|
||||
}
|
||||
|
||||
$data = [
|
||||
"schemas" => $this->channelService->channel->schemas,
|
||||
"schema" => $schema,
|
||||
"users" => $users,
|
||||
"records" => $records,
|
||||
"graph" => $graphArray,
|
||||
"systemFields" => array_values(System::list()),
|
||||
"operators" => array_values(Operator::list()),
|
||||
"sort" => $sort,
|
||||
"limit" => $limit,
|
||||
"skip" => $skip,
|
||||
"total" => $total,
|
||||
"filter" => $request->input("filter") ?? [],
|
||||
"inModal" => true,
|
||||
];
|
||||
|
||||
if ($request->ajax()) {
|
||||
$data["modalUrl"] = $request->fullUrl();
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data["inModal"] = false;
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "contentIndex",
|
||||
title: "Records",
|
||||
data: $data
|
||||
);
|
||||
}
|
||||
|
||||
public function exportCSV(Request $request)
|
||||
{
|
||||
$schemaName = $request->route("schemaName");
|
||||
$schema = $this->channelService->channel->schemas->where("name", $schemaName)->first();
|
||||
|
||||
$urlParams = $request->all();
|
||||
|
||||
$sort = data_get($urlParams, "sort") ?? "-_sys.updatedAt";
|
||||
$filter = data_get($urlParams, "filter") ?? [];
|
||||
$arguments = array_merge([
|
||||
"_sys.schema" => $schema->name,
|
||||
"_sys.status_in" => "draft,published",
|
||||
], $filter);
|
||||
|
||||
|
||||
$records = [];
|
||||
|
||||
try {
|
||||
$queryResult = $this->query
|
||||
->filter($arguments)
|
||||
// ->limit($limit)
|
||||
->status(explode(",", $arguments["_sys.status_in"]))
|
||||
// ->skip($skip)
|
||||
->sort($sort)
|
||||
->childrenDepth(0)
|
||||
->parentsDepth(0)
|
||||
->run();
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
$records = $graph->getRootRecords()->toArray();
|
||||
} catch (SubqueryNoResultException) {
|
||||
}
|
||||
|
||||
header('Content-Type: application/csv');
|
||||
header('Content-Disposition: attachment; filename="' . $schemaName . '.csv";');
|
||||
$handle = fopen('php://output', 'w');
|
||||
$csvRow = ["id", ...array_keys($records[0]->data->toArray())];
|
||||
fputcsv($handle, $csvRow, ',');
|
||||
foreach ($records as $record) {
|
||||
$csvRow = [$record->id, ...$record->data->toArray()];
|
||||
$csvRow = array_values($csvRow);
|
||||
fputcsv($handle, $csvRow, ',');
|
||||
}
|
||||
fclose($handle);
|
||||
echo $handle;
|
||||
exit;
|
||||
}
|
||||
|
||||
public function new(Request $request)
|
||||
{
|
||||
$schema = $this->channelService->channel->schemas->where("name", $request->input("schema"))->first();
|
||||
$recordHistory = $this->recordManager->fromSession($request->session())->getRecords();
|
||||
$record = $this->recordService->createEmpty($schema, $this->authService->currentUserId());
|
||||
$queryRecord = QueryRecord::fromRecord($record);
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "recordEdit",
|
||||
title: "New Record",
|
||||
data: [
|
||||
"schema" => $schema,
|
||||
"record" => $queryRecord,
|
||||
"recordHistory" => $recordHistory,
|
||||
"isCreateMode" => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
//
|
||||
//
|
||||
// public function newInline(Request $request)
|
||||
// {
|
||||
//
|
||||
// $channel = ChannelRepo::current();
|
||||
// $schema = $channel->schemas->where("name.value", $request->input("schema"))->first();
|
||||
// $record = Record::createEmpty($schema, AuthService::currentUserId($request));
|
||||
// $queryRecord = QueryRecord::fromRecord($record);
|
||||
//
|
||||
// return [
|
||||
// "schemas" => $channel->schemas,
|
||||
// "schema" => $schema,
|
||||
// "record" => $queryRecord,
|
||||
// "isCreateMode" => true,
|
||||
// ];
|
||||
// }
|
||||
|
||||
public function edit(Request $request)
|
||||
{
|
||||
$rid = $request->route("rid");
|
||||
|
||||
|
||||
$queryResult = $this->query
|
||||
->filter(["id" => $rid])
|
||||
->limit(1)
|
||||
->skip(0)
|
||||
->childrenDepth(2)
|
||||
->childrenLimit(100)
|
||||
->parentsDepth(1)
|
||||
->parentsLimit(100)
|
||||
->run();
|
||||
|
||||
|
||||
$graph = $queryResult->getQueryRecords();
|
||||
|
||||
if (empty($graph->records[0])) {
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "recordNotFound",
|
||||
title: "Record Not Found",
|
||||
);
|
||||
}
|
||||
|
||||
$record = $graph->records[0];
|
||||
$schema = $this->channelService->channel->schemas->where("name", $record->_sys->schema)->first();
|
||||
$recordHistory = $this->recordManager->fromSession($request->session())->push($rid)->getRecords($rid);
|
||||
$users = $this->userRepo->all();
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "recordEdit",
|
||||
title: "Edit Record",
|
||||
data: [
|
||||
"schema" => $schema,
|
||||
"graph" => $graph->toArray(),
|
||||
"record" => $record->toArray(),
|
||||
"users" => $users,
|
||||
"recordHistory" => $recordHistory,
|
||||
"isCreateMode" => $record->_sys->status === "empty",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// public function editInline(Request $request)
|
||||
// {
|
||||
// $channel = ChannelRepo::current();
|
||||
// $rid = $request->route("rid");
|
||||
//
|
||||
// $queryResult = $this->query
|
||||
// ->filter(["id" => $rid])
|
||||
// ->limit(1)
|
||||
// ->childrenDepth(2)
|
||||
// ->parentsDepth(1)
|
||||
// ->run();
|
||||
//
|
||||
// $graph = $queryResult->getQueryRecords($channel->schemas);
|
||||
// $record = $graph->records[0];
|
||||
// return ok(
|
||||
// [
|
||||
// "graph" => $graph->toArray(),
|
||||
// "record" => $record->toArray(),
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
//
|
||||
//
|
||||
// public function suggestions(Request $request)
|
||||
// {
|
||||
// $arguments = [
|
||||
// "_sys.schema" => $request->input("schema"),
|
||||
// ];
|
||||
//
|
||||
// if ($request->input("value")) {
|
||||
// if (in_array($request->input("ui"), ["text", "date"])) {
|
||||
// $arguments[$request->input("field") . "_regex"] = $request->input("value");
|
||||
// } elseif ($request->input("ui") == "number") {
|
||||
// $arguments[$request->input("field") . "_eqnum"] = floatval($request->input("value"));
|
||||
// } elseif ($request->input("ui") == "date") {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// $queryResult = $this->query
|
||||
// ->filter($arguments)
|
||||
// ->limit(10)
|
||||
// ->run();
|
||||
//
|
||||
// if (!$queryResult->hasResults()) {
|
||||
// return ok([]);
|
||||
// }
|
||||
// $schemas = $this->schemaRepo->all();
|
||||
// $graph = $queryResult->getQueryRecords($schemas);
|
||||
//
|
||||
// return ok($graph->records->toArray());
|
||||
// }
|
||||
//
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
if ($request->input("isCreateMode")) {
|
||||
$this->recordService->create(
|
||||
schemaName: $request->input("record._sys.schema"),
|
||||
data: $request->input("record.data"),
|
||||
id: $request->input("record.id"),
|
||||
file: $request->input("record._file") ?? [],
|
||||
edges: $request->input("edges"),
|
||||
status: $request->input("record._sys.status"),
|
||||
uploadFromUrl: ""
|
||||
);
|
||||
} else {
|
||||
$this->recordService->update(
|
||||
id: $request->input("record.id"),
|
||||
data: $request->input("record.data"),
|
||||
status: $request->input("record._sys.status"),
|
||||
edges: $request->input("edges"),
|
||||
updateEdges: true,
|
||||
);
|
||||
}
|
||||
|
||||
$queryResult = $this->query
|
||||
->filter(["id" => $request->input("record.id")])
|
||||
->limit(10)
|
||||
->childrenDepth(2)
|
||||
->parentsDepth(1)
|
||||
->run();
|
||||
$newGraph = $queryResult->getQueryRecords();
|
||||
} catch (ValidatorException $th) {
|
||||
return fail($th->getValidatorErrors());
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok($newGraph->toArray());
|
||||
}
|
||||
|
||||
|
||||
public function clone(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
$newRecordId = $this->recordService->clone(
|
||||
recordId: $request->route("rid"),
|
||||
);
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
} catch (ValidatorException $e) {
|
||||
return fail($e);
|
||||
}
|
||||
return ok(["id" => $newRecordId]);
|
||||
}
|
||||
|
||||
public function status(Request $request)
|
||||
{
|
||||
|
||||
$ids = array_map(fn($rec) => $rec["id"], $request->input("records"));
|
||||
|
||||
|
||||
$this->recordService->changeStatusBulk(
|
||||
status: $request->route("status"),
|
||||
recordsIds: $ids,
|
||||
);
|
||||
return ok();
|
||||
}
|
||||
//
|
||||
// public function delete(Request $request)
|
||||
// {
|
||||
// $ids = $request->input("ids");
|
||||
//
|
||||
// try {
|
||||
// $this->recordService->deleteMany($ids);
|
||||
// } catch (Throwable $th) {
|
||||
// return fail($th);
|
||||
// }
|
||||
// return ok();
|
||||
// }
|
||||
//
|
||||
// public function rollback(Request $request)
|
||||
// {
|
||||
// try {
|
||||
// $this->recordService->rollback(
|
||||
// userId: AuthService::currentUserId($request),
|
||||
// recordId: $request->route("rid"),
|
||||
// version: (int)$request->route("version")
|
||||
// );
|
||||
// } catch (ValidatorException $th) {
|
||||
// return fail($th->getFirstValidatorError());
|
||||
// } catch (LucentException|Throwable $th) {
|
||||
// return fail($th);
|
||||
// }
|
||||
// return ok();
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\Auth;
|
||||
use Lucent\Channel\ChannelContext;
|
||||
use Lucent\Record\RecordRepo;
|
||||
use Lucent\Revision\RevisionRepo;
|
||||
use Lucent\Schema\SchemaRepo;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class RevisionController extends Controller
|
||||
{
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
|
||||
$revisions = RevisionRepo::getByRecordId($request->route("rid"));
|
||||
return ok($revisions);
|
||||
}
|
||||
|
||||
public function rollback(Request $request)
|
||||
{
|
||||
$schemas = SchemaRepo::all();
|
||||
$revision = RevisionRepo::getByRecordIdAndVersion($request->route("rid"), (int)$request->route("version"));
|
||||
|
||||
try {
|
||||
RecordRepo::replaceMany($schemas, [$revision->toDB()], Auth::currentUserId());
|
||||
} catch (\Lucent\Schema\Validator\ValidatorException $th) {
|
||||
return fail($th->getFirstValidatorError());
|
||||
} catch (\Lucent\LucentException $th) {
|
||||
return fail($th);
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Lucent\Channel\ChannelRepo;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Schema\SchemaRepo;
|
||||
use Lucent\Schema\SchemaService;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
use function Lucent\Svelte\svelte;
|
||||
|
||||
class SchemaController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SchemaRepo $schemaRepo,
|
||||
private readonly SchemaService $schemaService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LucentException
|
||||
*/
|
||||
public function new(): View
|
||||
{
|
||||
$schemas = $this->schemaRepo->all();
|
||||
|
||||
return svelte(
|
||||
layout: "channel",
|
||||
view: "schemaNew",
|
||||
title: "Create schema",
|
||||
data: [
|
||||
"schemas" => $schemas,
|
||||
"schema" => Schema::fromArray([]),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
|
||||
try {
|
||||
$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: [],
|
||||
path: $request->input("path"),
|
||||
);
|
||||
} catch (LucentException $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
public function edit(string $name): View
|
||||
{
|
||||
$channel = ChannelRepo::current();
|
||||
return svelte(
|
||||
layout: "channel",
|
||||
view: "schemaEdit",
|
||||
title: "Schemas",
|
||||
data: [
|
||||
"schemas" => $channel->schemas,
|
||||
"schema" => $channel->schemas->where("name.value", $name)->first(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(Request $request): View
|
||||
{
|
||||
$channel = ChannelRepo::current();
|
||||
return svelte(
|
||||
layout: "channel",
|
||||
view: "schemaDelete",
|
||||
title: "Schemas",
|
||||
data: [
|
||||
"channel" => $channel,
|
||||
"schemas" => $channel->schemas,
|
||||
"schema" => $channel->schemas->firstWhere("name", $request->route("name")),
|
||||
"nav" => "collections",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$schema = $this->schemaService->update(
|
||||
name: $request->input("name"),
|
||||
label: $request->input("label"),
|
||||
isEntry: $request->input("isEntry"),
|
||||
color: $request->input("color") ?? "",
|
||||
visible: $request->input("visible") ?? [],
|
||||
titleTemplate: $request->input("titleTemplate") ?? "",
|
||||
revisionRetentionDays: $request->input("revisionRetentionDays"),
|
||||
revisionRetentionNumber: $request->input("revisionRetentionNumber"),
|
||||
trashedRetentionDays: $request->input("trashedRetentionDays"),
|
||||
path: $request->input("path"),
|
||||
);
|
||||
} catch (LucentException $e) {
|
||||
return fail($e);
|
||||
}
|
||||
|
||||
return ok($schema->toArray());
|
||||
}
|
||||
|
||||
public function postDelete(Request $request): Response
|
||||
{
|
||||
|
||||
$this->schemaService->delete($request->input("name"));
|
||||
|
||||
return ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Controller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\Auth;
|
||||
use Lucent\Channel\ChannelContext;
|
||||
use Lucent\Schema\SchemaRepo;
|
||||
use Lucent\View\ViewRepo;
|
||||
use function Lucent\Response\fail;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class ViewController extends Controller
|
||||
{
|
||||
|
||||
public function redirect(Request $request, string $cid, string $schemaName, string $viewName)
|
||||
{
|
||||
|
||||
$schema = SchemaRepo::findByName($schemaName);
|
||||
$view = $schema->views->whereName($viewName);
|
||||
parse_str($view->params, $viewParamsArray);
|
||||
return \redirect("/c/{$cid}/content/{$schemaName}/views/{$viewName}?{$view->params}");
|
||||
}
|
||||
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$schema = SchemaRepo::findByName($request->input("schemaName"));
|
||||
ViewRepo::create($schema, $request->input("view"), Auth::currentUserId());
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
try {
|
||||
$schema = SchemaRepo::findByName($request->input("schemaName"));
|
||||
ViewRepo::update($schema, $request->input("view"));
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
$schema = SchemaRepo::findByName($request->input("schemaName"));
|
||||
$view = $schema->views->whereName($request->input("view.name"));
|
||||
|
||||
return ok((array)$view);
|
||||
}
|
||||
|
||||
public function delete(Request $request, string $cid, string $schemaName, string $viewName)
|
||||
{
|
||||
try {
|
||||
$schema = SchemaRepo::findByName($schemaName);
|
||||
ViewRepo::delete($schema, $viewName);
|
||||
} catch (\Throwable $th) {
|
||||
return fail($th);
|
||||
}
|
||||
|
||||
return redirect("c/{$cid}/content/{$schemaName}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Channel\ChannelService;
|
||||
|
||||
readonly class AuthMiddleware
|
||||
{
|
||||
public function __construct(private AuthService $authService, private ChannelService $channelService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return redirect($this->channelService->channel->lucentUrl . "/login");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Lucent\Account\AuthService;
|
||||
|
||||
readonly class GuestMiddleware
|
||||
{
|
||||
public function __construct(private AuthService $authService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($this->authService->isLoggedIn()) {
|
||||
return redirect("/home");
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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:developer')->group(function () {
|
||||
Route::post('/schemas', [SchemaController::class, 'create']);
|
||||
Route::put('/schemas/', [SchemaController::class, 'update']);
|
||||
Route::delete('/schemas/{name}', [SchemaController::class, 'delete']);
|
||||
Route::post('/schemas/fields', [SchemaController::class, 'fields']);
|
||||
});
|
||||
|
||||
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']);
|
||||
});
|
||||
|
||||
// They need testing
|
||||
// Route::middleware('auth.api')->prefix("/fields")->controller(FieldController::class)->group(function () {
|
||||
// Route::post('/', 'create');
|
||||
// Route::put('/', 'update');
|
||||
// Route::delete('/{id}', 'delete');
|
||||
// });
|
||||
|
||||
//Route::middleware(["auth.api"])->group(function () {
|
||||
//
|
||||
//// Route::get('/{rid}', 'findOne');
|
||||
//
|
||||
//// Route::delete('/records/{id}', [RecordController::class, 'delete']);
|
||||
//// Route::post('/bulk', 'bulkCreate');
|
||||
//// Route::put('/bulk', 'bulkUpdate');
|
||||
//// Route::delete('/bulk', 'bulkDelete');
|
||||
//
|
||||
//
|
||||
//});
|
||||
//
|
||||
//
|
||||
//Route::middleware('auth.api')->prefix("/files")->controller(FileController::class)->group(function () {
|
||||
// Route::post('/', 'upload');
|
||||
//});
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Lucent\Http\Controller\Api\AccountController;
|
||||
use Lucent\Http\Controller\AuthController;
|
||||
use Lucent\Http\Controller\FileController;
|
||||
use Lucent\Http\Controller\HomeController;
|
||||
use Lucent\Http\Controller\MemberController;
|
||||
use Lucent\Http\Controller\RecordController;
|
||||
use Lucent\Http\Controller\SchemaController;
|
||||
use Lucent\Revision\RevisionController;
|
||||
|
||||
|
||||
Route::group([
|
||||
'middleware' => ['web'],
|
||||
'prefix' => "lucent"
|
||||
], function () {
|
||||
|
||||
Route::middleware(['lucent.guest'])->group(function () {
|
||||
Route::get('/', [AuthController::class, 'login']);
|
||||
Route::get('/register', [AuthController::class, 'register']);
|
||||
Route::post('/register', [AuthController::class, 'postRegister']);
|
||||
Route::get('/login', [AuthController::class, 'login']);
|
||||
Route::post('/login', [AuthController::class, 'postLogin']);
|
||||
Route::get('/verify', [AuthController::class, 'verify']);
|
||||
Route::post('/verify', [AuthController::class, 'postVerify']);
|
||||
|
||||
});
|
||||
|
||||
Route::middleware('lucent.auth')->group(function () {
|
||||
Route::get('/logout', [AuthController::class, 'logout']);
|
||||
Route::get('/profile', [AccountController::class, 'profile']);
|
||||
Route::post('/account/update-name', [AccountController::class, 'updateName']);
|
||||
});
|
||||
|
||||
|
||||
Route::middleware(["lucent.auth"])->group(function () {
|
||||
Route::get('/members/', [MemberController::class, 'index']);
|
||||
Route::post('/members/invite', [MemberController::class, 'invite']);
|
||||
Route::post('/members/update', [MemberController::class, 'update']);
|
||||
});
|
||||
|
||||
|
||||
Route::middleware(["lucent.auth"])->prefix("/schemas/")->group(function () {
|
||||
Route::get('/new/', [SchemaController::class, 'new']);
|
||||
Route::post('/', [SchemaController::class, 'create']);
|
||||
Route::get('/{name}/edit', [SchemaController::class, 'edit']);
|
||||
Route::put('/', [SchemaController::class, 'update']);
|
||||
Route::get('/{name}/delete', [SchemaController::class, 'delete']);
|
||||
Route::post('/delete', [SchemaController::class, 'postDelete']);
|
||||
});
|
||||
|
||||
|
||||
Route::middleware(["lucent.auth"])->prefix("/records")->group(function () {
|
||||
|
||||
Route::get('/new', [RecordController::class, 'new']);
|
||||
Route::get('/newInline', [RecordController::class, 'newInline']);
|
||||
Route::get('/suggestions', [RecordController::class, 'suggestions']);
|
||||
Route::get('/{rid}', [RecordController::class, 'edit']);
|
||||
Route::post('/clone/{rid}', [RecordController::class, 'clone']);
|
||||
Route::get('/editInline/{rid}', [RecordController::class, 'editInline']);
|
||||
Route::get('/{rid}/parents', [RecordController::class, 'parents']);
|
||||
Route::post('/', [RecordController::class, 'save']);
|
||||
Route::post('/status/{status}', [RecordController::class, 'status']);
|
||||
Route::post('/delete', [RecordController::class, 'delete']);
|
||||
Route::post('/{rid}/rollback/{version}', [RecordController::class, 'rollback']);
|
||||
});
|
||||
|
||||
Route::middleware(["lucent.auth"])->group(function () {
|
||||
Route::get('/records/{rid}/revisions', [RevisionController::class, 'index']);
|
||||
|
||||
});
|
||||
|
||||
Route::middleware(["lucent.auth"])->group(function () {
|
||||
Route::get('/', [HomeController::class, 'home']);
|
||||
Route::get('/home/records', [HomeController::class, 'records']);
|
||||
});
|
||||
|
||||
Route::middleware(["lucent.auth"])->prefix("/content")->group(function () {
|
||||
Route::get('/{schemaName}', [RecordController::class, 'index']);
|
||||
Route::get('/{schemaName}/csv', [RecordController::class, 'exportCSV']);
|
||||
});
|
||||
|
||||
Route::middleware(["lucent.auth"])->group(function () {
|
||||
|
||||
Route::post('/files/upload', [FileController::class, 'upload']);
|
||||
Route::get('/files/download', [FileController::class, 'download']);
|
||||
});
|
||||
|
||||
|
||||
Route::get('/fs/cache/{path}', [FileController::class, 'get'])->where('path', '.*');
|
||||
Route::get('/fs/{path}', [FileController::class, 'get'])->where('path', '.*');
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Id;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Id
|
||||
{
|
||||
public static function new(): string
|
||||
{
|
||||
return (string) Str::uuid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent;
|
||||
|
||||
use Exception;
|
||||
|
||||
|
||||
final class LucentException 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,37 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class LoginMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
public string $email,
|
||||
public string $token,
|
||||
public string $url
|
||||
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->subject('Login to LucentCMS')->text('lucent::emails.login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Primitive;
|
||||
|
||||
use Illuminate\Support\Collection as LaravelCollection;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @extends \Illuminate\Support\Collection<int, T>
|
||||
*/
|
||||
final class Collection extends LaravelCollection
|
||||
{
|
||||
/**
|
||||
* @param array<T> $values
|
||||
*/
|
||||
public function __construct(
|
||||
$values = []
|
||||
) {
|
||||
parent::__construct($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<T>
|
||||
**/
|
||||
public function toArray(): array
|
||||
{
|
||||
|
||||
return collect($this)->values()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<T>
|
||||
**/
|
||||
public function rejectNull(): Collection
|
||||
{
|
||||
return $this->filter(fn($v) => !empty($v))->values();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Primitive;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Support\Collection<int|string, string>
|
||||
*/
|
||||
final class StringCollection extends Collection
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
string ...$array
|
||||
) {
|
||||
parent::__construct($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
**/
|
||||
public function toArray(): array
|
||||
{
|
||||
return collect($this)->values()->toArray();
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): StringCollection
|
||||
{
|
||||
return new StringCollection(...$data);
|
||||
}
|
||||
|
||||
public static function fromDB(string $data): StringCollection
|
||||
{
|
||||
return new StringCollection(...\json_decode($data,true));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class Filter
|
||||
{
|
||||
private array $operatorsList;
|
||||
|
||||
public function __construct(
|
||||
public array $arguments = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function add(array $arguments): Filter
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
private function formatArguments(array $arguments): array
|
||||
{
|
||||
return (array)collect($arguments)->reduce(fn($c, $v, $k) => $this->formatArgument($c, $v, $k), []);
|
||||
}
|
||||
|
||||
private function formatArgument(array $arguments, mixed $value, string $filter): array
|
||||
{
|
||||
|
||||
$operator = $this->detectOperator($filter);
|
||||
$field = $this->detectField($filter, $operator);
|
||||
$formattedValue = match ($operator) {
|
||||
"eq" => $this->formatText($value),
|
||||
"ne" => $this->formatText($value),
|
||||
"eqnum" => $this->formatNumber($value),
|
||||
"nenum" => $this->formatNumber($value),
|
||||
"object" => $this->formatText($value),
|
||||
"in" => $this->formatListString($value),
|
||||
"nin" => $this->formatListString($value),
|
||||
"innum" => $this->formatListNum($value),
|
||||
"ninnum" => $this->formatListNum($value),
|
||||
"eqtrue" => true,
|
||||
"eqfalse" => false,
|
||||
"netrue" => true,
|
||||
"nefalse" => false,
|
||||
"regex" => "%{$value}%",
|
||||
"gt" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"gte" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"lt" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"lte" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"null" => null,
|
||||
"nnull" => null,
|
||||
"exists" => true,
|
||||
"nexists" => false,
|
||||
default => $value,
|
||||
};
|
||||
|
||||
|
||||
$matchedOperator = $this->operatorsList[$operator];
|
||||
$arguments[] = [
|
||||
"field" => str_replace(".", "->", $field),
|
||||
"operator" => $matchedOperator->db,
|
||||
"value" => $formattedValue
|
||||
];
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
private function formatText(string $value): string
|
||||
{
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
private function formatNumber(string $value): float
|
||||
{
|
||||
return \floatval($value);
|
||||
}
|
||||
|
||||
|
||||
private function formatListString(mixed $value): array
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
$value = explode(",", $value);
|
||||
}
|
||||
|
||||
return \array_map(fn($v) => $this->formatText($v), $value);
|
||||
}
|
||||
|
||||
private function formatListNum(mixed $value): array
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
$value = explode(",", $value);
|
||||
}
|
||||
|
||||
return \array_map(fn($v) => $this->formatNumber($v), $value);
|
||||
}
|
||||
|
||||
|
||||
private function detectOperator(string $filter): string
|
||||
{
|
||||
$exploded = \explode("_", $filter);
|
||||
$candidate = end($exploded);
|
||||
$operatorsListNames = collect($this->operatorsList)->map(fn($o) => $o->name)->toArray();
|
||||
|
||||
if (\in_array($candidate, $operatorsListNames)) {
|
||||
return $candidate;
|
||||
}
|
||||
return 'eq';
|
||||
}
|
||||
|
||||
private function detectField(string $filter, string $operator): string
|
||||
{
|
||||
$exploded = \explode("_", $filter);
|
||||
$candidate = array_pop($exploded);
|
||||
|
||||
if ($candidate == $operator) {
|
||||
return \implode("_", $exploded);
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
|
||||
private function formatReferences(Query $query): array
|
||||
{
|
||||
[$arguments, $referenceArguments] = $this->separateMainFromReferenceArguments();
|
||||
if (empty($referenceArguments)) {
|
||||
return [$arguments, []];
|
||||
};
|
||||
|
||||
$subqueries = collect($referenceArguments)->reduce(function ($c, $v, $k) {
|
||||
$keyWithoutRef = str_replace("children.", "", $k);
|
||||
[$field] = explode(".", $keyWithoutRef);
|
||||
$referenceField = str_replace($field . ".", "", $keyWithoutRef);
|
||||
$c[$field][$referenceField] = $v;
|
||||
return $c;
|
||||
}, []);
|
||||
|
||||
|
||||
$sourceIds = collect($subqueries)->reduce(function ($c, $subquery, $k) use ($query) {
|
||||
|
||||
$graph = $query->filter($subquery)->run();
|
||||
|
||||
if (!$graph->hasResults()) {
|
||||
return $c;
|
||||
}
|
||||
|
||||
$targetIds = collect($graph->records)->pluck("id");
|
||||
$sourceIds = DB::table("edges")->whereIn("target", $targetIds)->where("field", $k)->get()->pluck("source");
|
||||
return array_merge($c, $sourceIds->toArray());
|
||||
}, []);
|
||||
|
||||
return [$arguments, [
|
||||
"field" => "id",
|
||||
"operator" => "in",
|
||||
"value" => $sourceIds
|
||||
]];
|
||||
}
|
||||
|
||||
private function separateMainFromReferenceArguments(): array
|
||||
{
|
||||
return collect($this->arguments)->partition(function ($v, $k) {
|
||||
if (!str_starts_with($k, "children.")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
|
||||
public function run(Query $query): array
|
||||
{
|
||||
[$argumentsWithoutReferences, $referencesFilter] = $this->formatReferences($query);
|
||||
|
||||
$this->operatorsList = Operator::list();
|
||||
$formattedArguments = $this->formatArguments($argumentsWithoutReferences);
|
||||
if (!empty($referencesFilter)) {
|
||||
$formattedArguments[] = $referencesFilter;
|
||||
}
|
||||
return $formattedArguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\QueryRecord;
|
||||
|
||||
final class Graph
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Collection<QueryRecord> $records
|
||||
* @param Collection<Edge> $edges
|
||||
* */
|
||||
public function __construct(
|
||||
public Collection $records,
|
||||
public Collection $edges,
|
||||
public ?int $total = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<QueryRecord>
|
||||
* */
|
||||
public function getRootRecords(): Collection
|
||||
{
|
||||
return $this->records->where("isRoot", true)->values();
|
||||
}
|
||||
|
||||
public function hasResults(): bool
|
||||
{
|
||||
return !empty($this->records);
|
||||
}
|
||||
|
||||
// public function getRootRecordsWithChildren(): Collection
|
||||
// {
|
||||
// $rootRecords = $this->records->where("isRoot", true)->values();
|
||||
// return $rootRecords->map([$this, 'findChildren']);
|
||||
// }
|
||||
//
|
||||
// public function findChildren(QueryRecord $record): QueryRecord
|
||||
// {
|
||||
// $recordEdges = $this->edges
|
||||
// ->where("source", $record->id)
|
||||
// ->values()
|
||||
// ->sortBy("rank")
|
||||
// ->groupBy("field")->toArray();
|
||||
//
|
||||
//
|
||||
// foreach ($recordEdges as $field => $edges) {
|
||||
// $recordEdges[$field] = [];
|
||||
// foreach ($edges as $anEdge) {
|
||||
// $aRecord = $this->records->where("id", $anEdge->target)->first();
|
||||
// if (empty($aRecord)) {
|
||||
// continue;
|
||||
// }
|
||||
// $recordEdges[$field][] = $this->findChildren($aRecord);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $record->_children = new Collection($recordEdges);
|
||||
// return $record;
|
||||
// }
|
||||
|
||||
public function tree(): Collection
|
||||
{
|
||||
return $this->records->filter(function (QueryRecord $record) {
|
||||
return $this->edges->filter(fn(Edge $ed) => $ed->target == $record->id)->isEmpty();
|
||||
})->values()
|
||||
->map([$this, 'findChildren']);
|
||||
|
||||
}
|
||||
|
||||
public function findChildren(QueryRecord $record): QueryRecord
|
||||
{
|
||||
$recordEdges = $this->edges->filter(fn(Edge $ed) => $ed->source === $record->id)->values()->sort(fn($a, $b) => $a->rank <=> $b->rank)->values();
|
||||
|
||||
$groupRecordEdges = [];
|
||||
foreach ($recordEdges as $element) {
|
||||
$groupRecordEdges[$element->field][] = $element;
|
||||
}
|
||||
|
||||
$children = [];
|
||||
foreach ($groupRecordEdges as $field => $edges) {
|
||||
|
||||
$children[$field] = [];
|
||||
foreach ($edges as $anEdge) {
|
||||
$aRecord = $this->records->filter(fn(QueryRecord $rec) => $rec->id == $anEdge->target)->values();
|
||||
if (empty($aRecord[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$children[$field][] = $this->findChildren($aRecord[0]);
|
||||
}
|
||||
}
|
||||
$record->_children = $children;
|
||||
return $record;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
|
||||
final class Operator
|
||||
{
|
||||
/**
|
||||
* @psalm-param string[] $uis
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public string $symbol,
|
||||
public string $db,
|
||||
public array $uis,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Operator>
|
||||
*/
|
||||
public static function list(): array
|
||||
{
|
||||
|
||||
return [
|
||||
"regex" => new Operator(
|
||||
name: "regex",
|
||||
label: "Search",
|
||||
symbol: "~",
|
||||
db: 'like',
|
||||
uis: ["id", "text", "textarea", "url", "color", "date", "datetime"],
|
||||
),
|
||||
"eq" => new Operator(
|
||||
name: "eq",
|
||||
label: "Equals",
|
||||
symbol: "is",
|
||||
db: '=',
|
||||
uis: ["id", "text", "textarea", "url", "color", "date", "datetime"],
|
||||
),
|
||||
"ne" => new Operator(
|
||||
name: "ne",
|
||||
label: "Not Equals",
|
||||
symbol: "is not",
|
||||
db: '!=',
|
||||
uis: ["id", "text", "textarea", "url", "color", "date", "datetime"],
|
||||
),
|
||||
"eqnum" => new Operator(
|
||||
name: "eqnum",
|
||||
label: "Equals number",
|
||||
symbol: "is",
|
||||
db: '=',
|
||||
uis: ["number"],
|
||||
),
|
||||
"neqnum" => new Operator(
|
||||
name: "nenum",
|
||||
label: "Not Equals number",
|
||||
symbol: "is not",
|
||||
db: '$ne',
|
||||
uis: ["number"],
|
||||
),
|
||||
"object" => new Operator(
|
||||
name: "object",
|
||||
label: "Equals Object",
|
||||
symbol: "is",
|
||||
db: 'eqobject',
|
||||
uis: [],
|
||||
),
|
||||
"eqtrue" => new Operator(
|
||||
name: "eqtrue",
|
||||
label: "Equals true",
|
||||
symbol: "is",
|
||||
db: '=',
|
||||
uis: ["checkbox"],
|
||||
),
|
||||
"eqfalse" => new Operator(
|
||||
name: "eqfalse",
|
||||
label: "Equals false",
|
||||
symbol: "is not",
|
||||
db: '=',
|
||||
uis: ["checkbox"],
|
||||
),
|
||||
"netrue" => new Operator(
|
||||
name: "netrue",
|
||||
label: "Not equals true",
|
||||
symbol: "!=",
|
||||
db: '$ne',
|
||||
uis: ["checkbox"],
|
||||
),
|
||||
"nefalse" => new Operator(
|
||||
name: "nefalse",
|
||||
label: "Not equals false",
|
||||
symbol: "!=",
|
||||
db: '$ne',
|
||||
uis: ["checkbox"],
|
||||
),
|
||||
"in" => new Operator(
|
||||
name: "in",
|
||||
label: "In list",
|
||||
symbol: "in",
|
||||
db: 'in',
|
||||
uis: ["id", "text", "textarea", "url", "color", "date", "datetime"],
|
||||
),
|
||||
"innum" => new Operator(
|
||||
name: "innum",
|
||||
label: "In list of numbers",
|
||||
symbol: "in",
|
||||
db: '$in',
|
||||
uis: ["number"],
|
||||
),
|
||||
"nin" => new Operator(
|
||||
name: "nin",
|
||||
label: "Not in list",
|
||||
symbol: "not in",
|
||||
db: 'nin',
|
||||
uis: ["id", "text", "textarea", "url", "color", "date", "datetime"],
|
||||
),
|
||||
"ninnum" => new Operator(
|
||||
name: "ninnum",
|
||||
label: "Not In list of numbers",
|
||||
symbol: "not in",
|
||||
db: '$nin',
|
||||
uis: ["number"],
|
||||
),
|
||||
"lt" => new Operator(
|
||||
name: "lt",
|
||||
label: "Less than",
|
||||
symbol: "<",
|
||||
db: '<',
|
||||
uis: ["number", "date", "datetime"],
|
||||
),
|
||||
"lte" => new Operator(
|
||||
name: "lte",
|
||||
label: "Less than equals",
|
||||
symbol: "<=",
|
||||
db: '<=',
|
||||
uis: ["number", "date", "datetime"],
|
||||
),
|
||||
"gt" => new Operator(
|
||||
name: "gt",
|
||||
label: "Greater than",
|
||||
symbol: ">",
|
||||
db: '>',
|
||||
uis: ["number", "date", "datetime"],
|
||||
),
|
||||
"gte" => new Operator(
|
||||
name: "gte",
|
||||
label: "Greater than equals",
|
||||
symbol: ">=",
|
||||
db: '>=',
|
||||
uis: ["number", "date", "datetime"],
|
||||
),
|
||||
"null" => new Operator(
|
||||
name: "null",
|
||||
label: "Is null",
|
||||
symbol: "=",
|
||||
db: '$eq',
|
||||
uis: ["*"],
|
||||
),
|
||||
"nnull" => new Operator(
|
||||
name: "nnull",
|
||||
label: "Not null",
|
||||
symbol: "!=",
|
||||
db: '$ne',
|
||||
uis: ["*"],
|
||||
),
|
||||
"exists" => new Operator(
|
||||
name: "exists",
|
||||
label: "Exists",
|
||||
symbol: "exists",
|
||||
db: '$exists',
|
||||
uis: ["*"],
|
||||
),
|
||||
"nexists" => new Operator(
|
||||
name: "nexists",
|
||||
label: "Not exists",
|
||||
symbol: "not exists",
|
||||
db: '$exists',
|
||||
uis: ["*"],
|
||||
),
|
||||
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\InputFormatter;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\Record;
|
||||
|
||||
final class Query
|
||||
{
|
||||
|
||||
public Filter $filter;
|
||||
public QueryOptions $options;
|
||||
|
||||
public function __construct(
|
||||
public readonly ChannelService $channelService,
|
||||
public readonly InputFormatter $inputFormatter,
|
||||
)
|
||||
{
|
||||
$this->options = new QueryOptions();
|
||||
}
|
||||
|
||||
public function filter(array $filterArguments): Query
|
||||
{
|
||||
$this->filter = new Filter($filterArguments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function run(): Graph
|
||||
{
|
||||
$resultsRecords = $this->mainQuery();
|
||||
|
||||
$ids = array_map(function ($rec) {
|
||||
return $rec->id;
|
||||
}, $resultsRecords);
|
||||
|
||||
$resultChildrenEdgesTargetIds = [];
|
||||
$resultChildrenEdges = [];
|
||||
if ($this->options->childrenDepth > 0 && !empty($ids)) {
|
||||
$resultChildrenEdges = $this->getChildren($ids);
|
||||
$resultChildrenEdgesTargetIds = array_map(fn($e) => $e->target, $resultChildrenEdges);
|
||||
}
|
||||
$resultParentSourceTargetIds = [];
|
||||
$resultParentEdges = [];
|
||||
if ($this->options->parentsDepth > 0 && !empty($ids)) {
|
||||
$resultParentEdges = $this->getParents($ids);
|
||||
$resultParentSourceTargetIds = array_map(fn($e) => $e->source, $resultParentEdges);
|
||||
}
|
||||
|
||||
$edgesIds = collect($resultParentSourceTargetIds)->merge($resultChildrenEdgesTargetIds)->unique()->values()->toArray();
|
||||
$edgeRecords = [];
|
||||
if (!empty($edgesIds)) {
|
||||
$edgeRecords = DB::table('records')
|
||||
->whereIn("id", $edgesIds)
|
||||
->whereIn("_sys->status", $this->options->status)
|
||||
->get()->toArray();
|
||||
}
|
||||
$resultsRecordsUnique = collect(array_merge($resultsRecords, $edgeRecords))->unique("id")->values()->toArray();
|
||||
$resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges))
|
||||
->unique(fn($edge) => $edge->source . $edge->target . $edge->field)
|
||||
->toArray();
|
||||
|
||||
|
||||
return $this->formatRecords($resultsRecordsUnique, $resultEdges);
|
||||
|
||||
}
|
||||
|
||||
private function formatRecords(array $records, array $edges): Graph
|
||||
{
|
||||
$queryRecords = collect($records)->map(function ($recordData) {
|
||||
|
||||
$record = Record::fromDB($recordData);
|
||||
$record->data = $this->inputFormatter->fill($record->_sys->schema, $record->data);
|
||||
$queryRecord = QueryRecord::fromRecord($record);
|
||||
$queryRecord->isRoot = data_get($recordData, "isRoot") === true;
|
||||
return $queryRecord;
|
||||
})->toArray();
|
||||
|
||||
|
||||
$queryEdges = collect($edges)->map(function ($edgeData) {
|
||||
|
||||
return Edge::fromArray((array)$edgeData);
|
||||
|
||||
})->sortBy("rank")->values()->toArray();
|
||||
|
||||
|
||||
return new Graph(
|
||||
new Collection($queryRecords),
|
||||
new Collection($queryEdges),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function tree(): Collection
|
||||
{
|
||||
return $this->run()->tree();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws SubqueryNoResultException
|
||||
*/
|
||||
private function parseFilters(Builder $query): Builder
|
||||
{
|
||||
$filters = $this->filter->run(new Query($this->channelService, $this->inputFormatter));
|
||||
$ignoredFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (in_array($filter["field"], $ignoredFilters)) {
|
||||
continue;
|
||||
} else if ($filter["operator"] == "in") {
|
||||
$query->whereIn($filter["field"], $filter["value"]);
|
||||
} else if ($filter["operator"] == "nin") {
|
||||
$query->whereNotIn($filter["field"], $filter["value"]);
|
||||
} elseif ($filter["operator"] == "eqobject") {
|
||||
|
||||
$object = $filter["value"];
|
||||
// unset related filters used here
|
||||
$addToIgnored = collect($filters)
|
||||
->filter(fn($f) => str_starts_with($f["field"], $object))
|
||||
->values()
|
||||
->map(fn($f) => $f["field"])
|
||||
->toArray();
|
||||
|
||||
$ignoredFilters = array_merge($ignoredFilters, $addToIgnored);
|
||||
|
||||
$objectFilters = collect($filters)
|
||||
->filter(fn($f) => str_starts_with($f["field"], $object))
|
||||
->values()
|
||||
->reduce(function ($c, $f) use ($object) {
|
||||
$field = str_replace($object . "->", "", $f["field"]);
|
||||
$c[$field] = $f["value"];
|
||||
return $c;
|
||||
});
|
||||
|
||||
// target result
|
||||
// filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
|
||||
// $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
|
||||
// $query->whereJsonContains($filter["field"], [$objectFilters]);
|
||||
} else {
|
||||
|
||||
$query->where($filter["field"], $filter["operator"], $filter["value"]);
|
||||
}
|
||||
}
|
||||
|
||||
$query->whereIn("_sys->status", $this->options->status);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SubqueryNoResultException
|
||||
*/
|
||||
private function mainQuery(): array
|
||||
{
|
||||
$query = DB::table("records");
|
||||
$query = $this->parseFilters($query);
|
||||
$query->limit($this->options->limit);
|
||||
$query->offset($this->options->skip);
|
||||
$query = $this->orderByQuery($query);
|
||||
|
||||
return $query->get()->map(function ($r) {
|
||||
$r->isRoot = true;
|
||||
return $r;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
private
|
||||
function getChildren(array $ids): array
|
||||
{
|
||||
$subquery = DB::table('edges AS g')
|
||||
->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 0 as depth '))
|
||||
->whereIn('source', $ids)
|
||||
->limit($this->options->childrenLimit)
|
||||
->unionAll(
|
||||
DB::table(DB::raw("edges AS g, search_graph AS sg "))
|
||||
->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth')
|
||||
->whereRaw("g.source = sg.target")
|
||||
->where("sg.depth", "<=", $this->options->childrenDepth)
|
||||
->orderBy("rank")
|
||||
);
|
||||
|
||||
return DB::table('search_graph')
|
||||
// ->select(DB::raw("*, 1 as depth "))
|
||||
->withRecursiveExpression('search_graph', $subquery)
|
||||
->get()->toArray();
|
||||
}
|
||||
|
||||
private
|
||||
function getParents(array $ids): array
|
||||
{
|
||||
$subquery = DB::table('edges AS g')
|
||||
->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 0 as depth '))
|
||||
->limit($this->options->parentsLimit)
|
||||
->whereIn('g.target', $ids)
|
||||
->unionAll(
|
||||
DB::table(DB::raw("edges AS g, search_graph AS sg "))
|
||||
->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth')
|
||||
->whereRaw("g.target = sg.source")
|
||||
->where("sg.depth", "<=", $this->options->parentsDepth)
|
||||
->orderBy("rank")
|
||||
);
|
||||
|
||||
return DB::table('search_graph')
|
||||
// ->select(DB::raw('sg.source,sg.target,sg.rank,sg."sourceSchema",sg."targetSchema",sg.field,sg.depth'))
|
||||
->withRecursiveExpression('search_graph', $subquery)
|
||||
->get()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SubqueryNoResultException
|
||||
*/
|
||||
public
|
||||
function runWithCount(): Graph
|
||||
{
|
||||
|
||||
$query = DB::table("records");
|
||||
$query = $this->parseFilters($query);
|
||||
$graph = $this->run();
|
||||
$graph->total = $query->count();
|
||||
return $graph;
|
||||
}
|
||||
|
||||
|
||||
public
|
||||
function limit(int $limit): Query
|
||||
{
|
||||
$this->options->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public
|
||||
function skip(int $skip): Query
|
||||
{
|
||||
$this->options->skip = $skip;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function childrenDepth(int $depth): Query
|
||||
{
|
||||
$this->options->childrenDepth = $depth;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function childrenLimit(int $limit): Query
|
||||
{
|
||||
$this->options->childrenLimit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function parentsDepth(int $depth): Query
|
||||
{
|
||||
$this->options->parentsDepth = $depth;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function parentsLimit(int $limit): Query
|
||||
{
|
||||
$this->options->parentsLimit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function sort(string $sort): Query
|
||||
{
|
||||
$this->options->sort[] = $sort;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function status(array $status): Query
|
||||
{
|
||||
$this->options->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public
|
||||
function orderByQuery(Builder $query): Builder
|
||||
{
|
||||
foreach ($this->options->sort as $item) {
|
||||
$field = str_replace(".", "->", ltrim($item, '-'));
|
||||
$dir = str_starts_with($item, '-') ? "desc" : "asc";
|
||||
if ($field) {
|
||||
$query->orderBy($field, $dir);
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
final class QueryOptions
|
||||
{
|
||||
|
||||
|
||||
public function __construct(
|
||||
public int $limit = 20,
|
||||
public int $skip = 0,
|
||||
public int $childrenDepth = -1,
|
||||
public int $parentsDepth = -1,
|
||||
public int $childrenLimit = -1,
|
||||
public int $parentsLimit = -1,
|
||||
public array $sort = [],
|
||||
public array $status = ["published", "draft"]
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\QueryRecord;
|
||||
|
||||
final class QueryResult
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* @param Collection<QueryRecord> $records
|
||||
* @param Collection<Edge> $edges
|
||||
* @param int|null $total
|
||||
*/
|
||||
public function __construct(
|
||||
public Collection $records,
|
||||
public Collection $edges,
|
||||
public ?int $total = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getTotal(): ?int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
public function hasResults(): bool
|
||||
{
|
||||
return !empty($this->records);
|
||||
}
|
||||
|
||||
|
||||
public function graph(): Graph
|
||||
{
|
||||
return new Graph($this->records, $this->edges);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Exception;
|
||||
|
||||
|
||||
final class SubqueryNoResultException 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,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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Response;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lucent\LucentException;
|
||||
use Throwable;
|
||||
|
||||
function fail(Throwable|string|array $error, int $code = 400): Response
|
||||
{
|
||||
$message = "Something went wrong";
|
||||
|
||||
if (is_string($error) || is_array($error)) {
|
||||
$message = $error;
|
||||
} elseif ($error instanceof LucentException) {
|
||||
$message = $error->getMessage();
|
||||
} elseif ($error instanceof Throwable) {
|
||||
Log::error($error->getMessage());
|
||||
}
|
||||
|
||||
return response([
|
||||
"error" => $message
|
||||
], $code);
|
||||
}
|
||||
|
||||
|
||||
function ok(array $data = [], int $code = 200): Response
|
||||
{
|
||||
return response($data, $code);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Revision;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Lucent\Record\File;
|
||||
use Lucent\Record\Record;
|
||||
use Lucent\Record\RecordData;
|
||||
use Lucent\Record\System;
|
||||
use stdClass;
|
||||
|
||||
readonly class Revision
|
||||
{
|
||||
|
||||
function __construct(
|
||||
public string $id,
|
||||
public string $recordId,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public ?File $_file = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return json_decode(json_encode($this), true);
|
||||
}
|
||||
|
||||
|
||||
public static function fromDB(stdClass $data): Revision
|
||||
{
|
||||
|
||||
$file = json_decode($data->_file, true);
|
||||
if (!empty($file)) {
|
||||
|
||||
$file = new File(...$file);
|
||||
} else {
|
||||
$file = null;
|
||||
}
|
||||
|
||||
return new Revision(
|
||||
id: $data->id,
|
||||
recordId: $data->recordId,
|
||||
_sys: System::fromArray(json_decode($data->_sys, true)),
|
||||
data: new RecordData(json_decode($data->data, true)),
|
||||
_file: $file,
|
||||
);
|
||||
}
|
||||
|
||||
public function toDB(): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"recordId" => $this->recordId,
|
||||
"_sys" => json_encode($this->_sys),
|
||||
"_file" => json_encode($this->_file),
|
||||
"data" => json_encode($this->data),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public static function fromRecord(Record $record): Revision
|
||||
{
|
||||
return new Revision(
|
||||
id: (string)Str::uuid(),
|
||||
recordId: $record->id,
|
||||
_sys: $record->_sys,
|
||||
data: $record->data,
|
||||
_file: $record->_file,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Revision;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class RevisionController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly RevisionRepo $revisionRepo,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$revisions = $this->revisionRepo->getByRecordId($request->route("rid"));
|
||||
return ok($revisions->toArray());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Revision;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Primitive\Collection;
|
||||
use PhpOption\Option;
|
||||
|
||||
class RevisionRepo
|
||||
{
|
||||
|
||||
private string $table = "revisions";
|
||||
|
||||
public function create(Revision $revision): string
|
||||
{
|
||||
$revisionDB = $revision->toDB();
|
||||
DB::table($this->table)->insert($revisionDB);
|
||||
return $revision->id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Collection<Revision>
|
||||
**/
|
||||
public function getByRecordId(string $rid): Collection
|
||||
{
|
||||
$revisions = DB::table($this->table)
|
||||
->where("recordId", $rid)
|
||||
->get()
|
||||
->map([Revision::class, 'fromDB'])
|
||||
->sortByDesc("_sys.version")
|
||||
->toArray();
|
||||
|
||||
return new Collection($revisions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Option<Revision>
|
||||
*/
|
||||
public function getByRecordIdAndVersion(string $rid, int $version): Option
|
||||
{
|
||||
|
||||
$res = DB::table($this->table)
|
||||
->where("recordId", $rid)
|
||||
->where('_sys->version', $version)->first();
|
||||
|
||||
if (empty($res)) {
|
||||
return none();
|
||||
}
|
||||
|
||||
return some(Revision::fromDB($res));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Revision;
|
||||
|
||||
use Lucent\Record\Record;
|
||||
use PhpOption\Option;
|
||||
|
||||
readonly class RevisionService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private RevisionRepo $revisionRepo,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Option<Revision>
|
||||
*/
|
||||
public function getByRecordIdAndVersion(string $recordId, int $version): Option
|
||||
{
|
||||
|
||||
return $this->revisionRepo->getByRecordIdAndVersion($recordId, $version);
|
||||
}
|
||||
|
||||
public function create(Record $record): string
|
||||
{
|
||||
$revision = Revision::fromRecord($record);
|
||||
return $this->revisionRepo->create($revision);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
|
||||
class CollectionSchema implements Schema
|
||||
{
|
||||
public Type $type = Type::COLLECTION;
|
||||
|
||||
/**
|
||||
* @param Collection<FieldInterface> $fields
|
||||
* @param array<string> $visible
|
||||
*/
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public array $visible,
|
||||
public Collection $fields,
|
||||
public bool $isEntry = false,
|
||||
public string $color = "",
|
||||
public string $titleTemplate = "",
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
|
||||
class FieldInfo
|
||||
{
|
||||
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public FieldType $type,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
|
||||
interface FieldInterface
|
||||
{
|
||||
public function format(array $input, array $output): array;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
|
||||
enum FieldType: string
|
||||
{
|
||||
case STRING = 'string';
|
||||
case NUMBER = 'number';
|
||||
case BOOLEAN = 'boolean';
|
||||
case FILE = 'file';
|
||||
case JSON = 'json';
|
||||
case REFERENCE = 'reference';
|
||||
case TAB = 'tab';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
|
||||
class FilesSchema implements Schema
|
||||
{
|
||||
|
||||
public Type $type = Type::FILES;
|
||||
|
||||
/**
|
||||
* @param Collection<FieldInterface> $fields
|
||||
*/
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public Collection $fields,
|
||||
public string $path,
|
||||
public bool $isEntry = false,
|
||||
public string $color = "",
|
||||
public string $titleTemplate = "",
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
use Lucent\Field\Field;
|
||||
use Lucent\View\View;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
**/
|
||||
function visibleFields(Schema $schema, ?View $view = null): array
|
||||
{
|
||||
|
||||
$visibleFieldNames = $schema->visible;
|
||||
if (!empty($view)) {
|
||||
$visibleFieldNames = $view->visible;
|
||||
}
|
||||
|
||||
return $schema->fields
|
||||
->filter(function (Field $f) use ($visibleFieldNames) {
|
||||
|
||||
if ($f->trashed || $f->ui == "tab") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $visibleFieldNames->contains($f->name->value);
|
||||
})
|
||||
->values()
|
||||
->map(fn(Field $f) => $f->name->value)
|
||||
->toArray();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
final class Nullable
|
||||
{
|
||||
public function __construct(
|
||||
public bool $nullable,
|
||||
public mixed $value,
|
||||
public mixed $default,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function value(): mixed
|
||||
{
|
||||
|
||||
if (!empty($this->value)) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
if ($this->nullable) {
|
||||
return null;
|
||||
}
|
||||
return $this->default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
interface Schema
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
use Lucent\Primitive\Collection;
|
||||
|
||||
class SchemaService
|
||||
{
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function fromArray(array $schemaArr): Schema
|
||||
{
|
||||
|
||||
return match ($schemaArr["type"]) {
|
||||
"collection" => new CollectionSchema(
|
||||
name: $schemaArr["name"],
|
||||
label: $schemaArr["label"],
|
||||
visible: $schemaArr["visible"] ?? [],
|
||||
fields: (new Collection($schemaArr["fields"]))->map([$this, 'mapFields']),
|
||||
isEntry: $schemaArr["isEntry"],
|
||||
color: $schemaArr["color"],
|
||||
titleTemplate: $schemaArr["titleTemplate"],
|
||||
),
|
||||
"files" => new FilesSchema(
|
||||
name: $schemaArr["name"],
|
||||
label: $schemaArr["label"],
|
||||
fields: (new Collection($schemaArr["fields"]))->map([$this, 'mapFields']),
|
||||
path: $schemaArr["path"],
|
||||
isEntry: $schemaArr["isEntry"],
|
||||
color: $schemaArr["color"],
|
||||
titleTemplate: $schemaArr["titleTemplate"]
|
||||
)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public function mapFields(array $field): FieldInterface
|
||||
{
|
||||
$className = "\\Lucent\Schema\Ui\\" . ucfirst($field["ui"]);
|
||||
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);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema;
|
||||
|
||||
|
||||
enum Type: string
|
||||
{
|
||||
case COLLECTION = 'collection';
|
||||
case FILES = 'files';
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema\Ui;
|
||||
|
||||
use Lucent\Schema\FieldInfo;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\FieldType;
|
||||
use Lucent\Schema\Nullable;
|
||||
use Lucent\Schema\Validator\RequiredInterface;
|
||||
|
||||
class Block implements FieldInterface, RequiredInterface
|
||||
{
|
||||
public FieldInfo $info;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public bool $nullable = false,
|
||||
public bool $required = false,
|
||||
public string $default = "",
|
||||
public bool $readonly = false,
|
||||
public string $group = "",
|
||||
)
|
||||
{
|
||||
$this->info = new FieldInfo("block", "Block editor", FieldType::JSON);
|
||||
}
|
||||
|
||||
public function format(array $input, array $output): array
|
||||
{
|
||||
$value = $input[$this->name] ?? null;
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
}
|
||||
|
||||
$output[$this->name] = (new Nullable($this->nullable, $value, []))->value();
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function failRequired(mixed $value): bool
|
||||
{
|
||||
return empty($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema\Ui;
|
||||
|
||||
use Lucent\Schema\FieldInfo;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\FieldType;
|
||||
use Lucent\Schema\Nullable;
|
||||
use Lucent\Schema\Validator\RequiredInterface;
|
||||
use function is_bool;
|
||||
|
||||
class Checkbox implements FieldInterface, RequiredInterface
|
||||
{
|
||||
|
||||
public FieldInfo $info;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public bool $required = false,
|
||||
public bool $nullable = false,
|
||||
public string $default = "",
|
||||
public bool $readonly = false,
|
||||
public string $group = "",
|
||||
)
|
||||
{
|
||||
$this->info = new FieldInfo("checkbox", "Block Checkbox", FieldType::BOOLEAN);
|
||||
}
|
||||
|
||||
public function format(array $input, array $output): array
|
||||
{
|
||||
$value = $input[$this->name] ?? null;
|
||||
|
||||
if (is_bool($value)) {
|
||||
$newValue = $value;
|
||||
} else {
|
||||
$newValue = (new Nullable($this->nullable, $value, false))->value();
|
||||
}
|
||||
$output[$this->name] = $newValue;
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function failRequired(mixed $value): bool
|
||||
{
|
||||
return (bool)$value !== true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema\Ui;
|
||||
|
||||
use Lucent\Schema\FieldInfo;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\FieldType;
|
||||
use Lucent\Schema\Nullable;
|
||||
use Lucent\Schema\Validator\RequiredInterface;
|
||||
|
||||
class Color implements FieldInterface, RequiredInterface
|
||||
{
|
||||
public FieldInfo $info;
|
||||
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public bool $required = false,
|
||||
public bool $nullable = false,
|
||||
public string $default = "",
|
||||
public bool $readonly = false,
|
||||
public string $optionsFrom = "",
|
||||
public string $optionsField = "",
|
||||
public bool $optionsSuggest = false,
|
||||
public string $group = "",
|
||||
)
|
||||
{
|
||||
$this->info = new FieldInfo("color", "Color", FieldType::STRING);
|
||||
}
|
||||
|
||||
public function format(array $input, array $output): array
|
||||
{
|
||||
$value = $input[$this->name] ?? null;
|
||||
$output[$this->name] = (new Nullable($this->nullable, $value, ""))->value();
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function failRequired(mixed $value): bool
|
||||
{
|
||||
return empty(trim($value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Schema\Ui;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Lucent\Schema\FieldInfo;
|
||||
use Lucent\Schema\FieldInterface;
|
||||
use Lucent\Schema\FieldType;
|
||||
use Lucent\Schema\Nullable;
|
||||
use Lucent\Schema\Validator\MinMaxInterface;
|
||||
use Lucent\Schema\Validator\RequiredInterface;
|
||||
|
||||
class Date implements FieldInterface, RequiredInterface, MinMaxInterface
|
||||
{
|
||||
public FieldInfo $info;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $label,
|
||||
public bool $required = false,
|
||||
public bool $nullable = false,
|
||||
public ?Carbon $min = null,
|
||||
public ?Carbon $max = null,
|
||||
public string $default = "",
|
||||
public bool $readonly = false,
|
||||
public string $optionsFrom = "",
|
||||
public string $optionsField = "",
|
||||
public bool $optionsSuggest = false,
|
||||
public string $group = "",
|
||||
)
|
||||
{
|
||||
$this->info = new FieldInfo("date", "Date", FieldType::STRING);
|
||||
}
|
||||
|
||||
public function format(array $input, array $output): array
|
||||
{
|
||||
$value = $input[$this->name] ?? null;
|
||||
if (empty($value)) {
|
||||
$newValue = (new Nullable($this->nullable, null, ""))->value();
|
||||
} else {
|
||||
$date = Carbon::parse($value);
|
||||
$dateFormatted = $date->format("Y-m-d");
|
||||
$newValue = (new Nullable($this->nullable, $dateFormatted, ""))->value();
|
||||
}
|
||||
|
||||
$output[$this->name] = $newValue;
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function failRequired(mixed $value): bool
|
||||
{
|
||||
return empty(trim($value));
|
||||
}
|
||||
|
||||
public function failMin(mixed $value): bool
|
||||
{
|
||||
if (empty($this->value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $value->lessThanOrEqualTo($this->min);
|
||||
}
|
||||
|
||||
public function failMax(mixed $value): bool
|
||||
{
|
||||
if (empty($this->value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $value->greaterThan($this->max);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user