first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<?php
namespace Common\Database;
use Illuminate\Pagination\LengthAwarePaginator;
class CustomLengthAwarePaginator extends LengthAwarePaginator
{
public function toArray(): array
{
return [
'current_page' => $this->currentPage(),
'data' => $this->items->toArray(),
'from' => $this->firstItem(),
'last_page' => $this->lastPage(),
'next_page' => $this->hasMorePages()
? $this->currentPage() + 1
: null,
'per_page' => $this->perPage(),
'prev_page' =>
$this->currentPage() > 1 ? $this->currentPage() - 1 : null,
'to' => $this->lastItem(),
'total' => $this->total(),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Common\Database;
class CustomSimplePaginator extends \Illuminate\Pagination\Paginator
{
public function toArray(): array
{
return [
'current_page' => $this->currentPage(),
'data' => $this->items->toArray(),
'from' => $this->firstItem(),
'next_page' => $this->hasMorePages()
? $this->currentPage() + 1
: null,
'per_page' => $this->perPage(),
'prev_page' =>
$this->currentPage() > 1 ? $this->currentPage() - 1 : null,
'to' => $this->lastItem(),
];
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Common\Database\Datasource;
use Common\Database\Datasource\Filters\AlgoliaFilterer;
use Common\Database\Datasource\Filters\ElasticFilterer;
use Common\Database\Datasource\Filters\MeilisearchFilterer;
use Common\Database\Datasource\Filters\MysqlFilterer;
use Common\Database\Datasource\Filters\TntFilterer;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\Expression;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Scout\Builder as ScoutBuilder;
use Laravel\Scout\Searchable;
use Matchish\ScoutElasticSearch\Engines\ElasticSearchEngine;
use const App\Providers\WORKSPACED_RESOURCES;
class Datasource
{
protected EloquentBuilder|Relation $builder;
protected Model $model;
protected array $params;
protected bool $queryBuilt = false;
public DatasourceFilters $filters;
public array|null|false $order = null;
// sometimes we might need to order by ID or something else, if primary column
// is not guaranteed to be unique, to avoid duplicated items in pagination
public string|null $secondaryOrderCol = null;
protected ?ScoutBuilder $scoutBuilder;
public function __construct(
$model,
array $params,
DatasourceFilters $filters = null,
protected string $filtererName = 'mysql',
) {
$this->model = $model->getModel();
$this->params = $this->toCamelCase($params);
$this->builder = $model instanceof Model ? $model->newQuery() : $model;
$this->filters =
$filters ?? new DatasourceFilters($this->params['filters'] ?? null);
}
public function paginate(): AbstractPaginator
{
$this->buildQuery();
$perPage = $this->limit();
$page = (int) $this->param('page', 1);
$columns = empty($this->builder->getQuery()->columns)
? ['*']
: $this->builder->getQuery()->columns;
if ($this->param('paginate') === 'lengthAware') {
return $this->scoutBuilder instanceof ScoutBuilder
? $this->scoutBuilder->paginate($perPage, 'page', $page)
: $this->builder->paginate($perPage, $columns, 'page', $page);
} else {
return $this->scoutBuilder instanceof ScoutBuilder
? $this->scoutBuilder->simplePaginate($perPage, 'page', $page)
: $this->builder->simplePaginate(
$perPage,
$columns,
'page',
$page,
);
}
}
public function get(): Collection
{
$this->buildQuery();
return $this->builder->limit($this->limit())->get();
}
public function param(string $name, $default = null)
{
return Arr::get($this->params, Str::camel($name)) ?: $default;
}
public function buildQuery(): self
{
if ($this->queryBuilt) {
return $this;
}
$with = array_filter(explode(',', $this->param('with', '')));
$withCount = array_filter(explode(',', $this->param('withCount', '')));
$searchTerm = $this->param('query');
// load specified relations and counts
if (!empty($with)) {
$this->builder->with($with);
}
if (!empty($withCount)) {
$this->builder->withCount($withCount);
}
$this->applyWorkspaceFilter();
$filterer = $this->resolveFilterer($searchTerm);
$this->scoutBuilder = (new $filterer(
$this->builder,
$this->filters,
$searchTerm,
))->apply();
// allow caller class to override order or
// prevent it completely by setting "false"
if ($this->order !== false) {
$order = $this->getOrder();
if (isset($order['col'])) {
$orderCol = str_replace(
$this->builder->getModel()->getTable() . '.',
'',
$order['col'],
);
$methodName = Str::camel('orderBy' . ucfirst($orderCol));
$scopeMethodName = 'scope' . ucfirst($methodName);
if (
method_exists($this->builder->getModel(), $methodName) ||
method_exists($this->builder->getModel(), $scopeMethodName)
) {
$this->builder->$methodName($order['dir']);
} else {
$this->builder->orderBy(
Str::snake($order['col']),
$order['dir'] ?? 'desc',
);
}
if ($this->secondaryOrderCol) {
$this->builder->orderBy(
$this->secondaryOrderCol,
$order['dir'] ?? 'desc',
);
}
}
}
$this->queryBuilt = true;
return $this;
}
private function resolveFilterer(string $searchTerm = null): string
{
$filtererName = $this->filtererName;
if (
!$searchTerm ||
!in_array(Searchable::class, class_uses_recursive($this->model)) ||
$filtererName === 'mysql'
) {
return MysqlFilterer::class;
} elseif ($filtererName === 'meilisearch') {
return MeilisearchFilterer::class;
} elseif ($filtererName === 'tntsearch') {
return TntFilterer::class;
} elseif ($filtererName === 'algolia') {
return AlgoliaFilterer::class;
} elseif ($filtererName === ElasticSearchEngine::class) {
return ElasticFilterer::class;
}
return MysqlFilterer::class;
}
private function applyWorkspaceFilter(): void
{
if (
!config('common.site.workspaces_integrated') ||
!in_array(get_class($this->model), WORKSPACED_RESOURCES)
) {
return;
}
if ($workspaceId = $this->param('workspaceId')) {
$this->filters->where('workspace_id', '=', $workspaceId);
} elseif ($userId = $this->param('userId')) {
$this->filters
->where('user_id', '=', $userId)
->where('workspace_id', '=', 0);
}
}
public function getOrder(
string $defaultOrderCol = 'updated_at',
string $defaultOrderDir = 'desc',
): array {
if (isset($this->order['col'])) {
$orderCol = $this->order['col'];
$orderDir = $this->order['dir'];
// order might be a single string: "column|direction"
} elseif ($specifiedOrder = $this->param('order')) {
$parts = preg_split('(\||:)', $specifiedOrder);
$orderCol = Arr::get($parts, 0, $defaultOrderCol);
$orderDir = Arr::get($parts, 1, $defaultOrderDir);
// order might be as separate params
} elseif ($this->param('orderBy') || $this->param('orderDir')) {
$orderCol = $this->param('orderBy');
$orderDir = $this->param('orderDir');
// try ordering be relevance, if it's a search query and
// using mysql fulltext, finally default to "updated_at" column
} elseif ($this->hasRelevanceColumn()) {
$orderCol = 'relevance';
$orderDir = 'desc';
} else {
$orderCol = $defaultOrderCol;
$orderDir = $defaultOrderDir;
}
if ($orderCol !== 'relevance' && !Str::endsWith($orderCol, '_count')) {
$orderCol = $this->builder->qualifyColumn($orderCol);
}
return [
'col' => $orderCol,
'dir' => $orderDir,
];
}
private function toCamelCase(array $params): array
{
return collect($params)
->keyBy(function ($value, $key) {
return Str::camel($key);
})
->toArray();
}
private function hasRelevanceColumn(): bool
{
return !!Arr::first($this->getQueryBuilder() ?? [], function ($col) {
return $col instanceof Expression &&
Str::endsWith($col->getValue(), 'AS relevance');
});
}
private function limit(): int
{
if ($this->param('perPage')) {
return (int) $this->param('perPage');
} else {
return $this->getQueryBuilder()->limit ?? 15;
}
}
private function getQueryBuilder(): QueryBuilder
{
$query = $this->builder->getQuery();
if ($query instanceof EloquentBuilder) {
$query = $query->getQuery();
}
return $query;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Common\Database\Datasource;
use Auth;
use Illuminate\Support\Collection;
class DatasourceFilters
{
private array $filters;
public function __construct(?string $encodedFilters = '')
{
$this->filters = $this->decodeFilters($encodedFilters);
}
public function getAll(): array
{
return $this->filters;
}
public function empty(): bool
{
return empty($this->filters);
}
public function has(string $key): bool {
foreach ($this->filters as $filter) {
if ($filter['key'] === $key) {
return true;
}
}
return false;
}
public function where(string $key, string $operator, $value): self
{
if ($value instanceof Collection) {
$value = $value->toArray();
}
$this->filters[] = [
'key' => $key,
'operator' => $operator,
'value' => $value,
];
return $this;
}
public function getAndRemove(
string $key,
string $operator = null,
$value = null,
): ?array {
// use func_get_args as "null" is a valid value, need
// to check whether if it was actually passed by user
$args = func_get_args();
foreach ($this->filters as $key => $filter) {
if (
$filter['key'] === $args[0] &&
(!isset($args[1]) || $filter['operator'] === $args[1]) &&
(!isset($args[2]) || $filter['value'] === $args[2])
) {
unset($this->filters[$key]);
return $filter;
}
}
return null;
}
private function decodeFilters(?string $filterString): array
{
if ($filterString) {
$filters = json_decode(
base64_decode(urldecode($filterString)),
true,
);
return collect($filters)
->flatMap(fn($filter) => $this->normalizeFilter($filter))
->filter()
->toArray();
} else {
return [];
}
}
private function normalizeFilter(array $filter): ?array
{
$value = $filter['value'] ?? null;
$operator = $filter['operator'] ?? '=';
$value = $this->replaceValuePlaceholders($value);
if (is_array($value)) {
// filtering by normalized model
if (isset($value['id'])) {
$value = $this->replaceValuePlaceholders($value['id']);
// "value" contains both value and operator
} elseif (array_key_exists('value', $value)) {
$operator = $value['operator'] ?? $operator;
$value = $this->replaceValuePlaceholders($value['value']);
}
}
$filters = [
// preserve any extra keys that might be present
array_merge($filter, [
'key' => $filter['key'],
'operator' => $operator,
'value' => $value,
]),
];
// filter might have some extra static filters, for example to restrict by model type
if (isset($filter['extraFilters'])) {
$filters[] = $filter['extraFilters'];
}
return $filters;
}
private function replaceValuePlaceholders($value)
{
if ($value === '{authId}') {
return Auth::id();
}
return $value;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Common\Database\Datasource\Filters;
use Algolia\AlgoliaSearch\SearchIndex;
use Common\Database\Datasource\Filters\Traits\NormalizesFiltersForFulltextEngines;
use Illuminate\Support\Str;
use Laravel\Scout\Builder;
class AlgoliaFilterer extends BaseFilterer
{
use NormalizesFiltersForFulltextEngines;
public function apply(): ?Builder
{
return $this->query
->getModel()
->search($this->searchTerm, function (
SearchIndex $algolia,
string $query,
array $options
) {
$filters = $this->prepareFiltersForAlgolia();
$filters = implode(' AND ', $filters);
if ($filters) {
$options['filters'] = $filters;
}
return $algolia->search($query, $options);
});
}
private function prepareFiltersForAlgolia(): array
{
$filters = $this->normalizeFilters($this->filters->getAll());
return array_map(function ($filter) {
$prefix = '';
if (Str::contains($filter['operator'], '!')) {
$filter['operator'] = str_replace('!', '', $filter['operator']);
$prefix = 'NOT ';
}
if (!is_numeric($filter['value']) && $filter['operator'] === '=') {
$filter['operator'] = ':';
}
if (is_array($filter['value'])) {
$filter['value'] = implode(',', $filter['value']);
}
return $prefix . implode('', $filter);
}, $filters);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Common\Database\Datasource\Filters;
use Common\Database\Datasource\DatasourceFilters;
use Laravel\Scout\Builder as ScoutBuilder;
abstract class BaseFilterer
{
public function __construct(
protected $query,
protected DatasourceFilters $filters,
protected string|null $searchTerm = null
) {
}
abstract public function apply(): ?ScoutBuilder;
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Common\Database\Datasource\Filters;
use Common\Database\Datasource\Filters\Traits\NormalizesFiltersForFulltextEngines;
use Elasticsearch\Client;
use Laravel\Scout\Builder;
use Matchish\ScoutElasticSearch\ElasticSearch\Params\Search as SearchParams;
use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery;
use ONGR\ElasticsearchDSL\Query\TermLevel\TermsQuery;
use ONGR\ElasticsearchDSL\Search;
class ElasticFilterer extends BaseFilterer
{
use NormalizesFiltersForFulltextEngines;
public function apply(): ?Builder
{
return $this->query
->getModel()
->search($this->searchTerm, function (
Client $client,
Search $body
) {
$filters = $this->normalizeFilters($this->filters->getAll());
foreach ($filters as $filter) {
if (is_array($filter['value'])) {
$filter = new TermsQuery(
$filter['key'],
$filter['value'],
);
} else {
$filter = new TermQuery(
$filter['key'],
$filter['value'],
);
}
$body->addPostFilter($filter);
}
$params = new SearchParams(
$this->query->getModel()->searchableAs(),
$body->toArray(),
);
return $client->search($params->toArray());
});
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Common\Database\Datasource\Filters;
use Common\Database\Datasource\Filters\Traits\NormalizesFiltersForFulltextEngines;
use Laravel\Scout\Builder;
class MeilisearchFilterer extends BaseFilterer
{
use NormalizesFiltersForFulltextEngines;
public function apply(): Builder
{
return $this->query
->getModel()
->search($this->searchTerm, function (
$driver,
?string $query,
array $options
) {
$filters = $this->prepareFiltersForMeilisearch();
$filters = implode(' AND ', $filters);
if ($filters) {
$options['filter'] = $filters;
}
return $driver->search($query, $options);
});
}
private function prepareFiltersForMeilisearch(): array
{
$filters = $this->normalizeFilters($this->filters->getAll());
return array_map(function ($filter) {
if (is_array($filter['value'])) {
$values = array_map(
fn($v) => $this->createFilterString(
$filter['key'],
$filter['operator'],
$v,
),
$filter['value'],
);
return '(' . implode(' OR ', $values) . ')';
} else {
return $this->createFilterString(
$filter['key'],
$filter['operator'],
$filter['value'],
);
}
}, $filters);
}
private function createFilterString(
string $key,
string $operator,
$value
): string {
return "$key $operator $value";
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Common\Database\Datasource\Filters;
use Common\Database\Datasource\Filters\Traits\SupportsMysqlFilters;
use Laravel\Scout\Builder;
class MysqlFilterer extends BaseFilterer
{
use SupportsMysqlFilters;
public function apply(): ?Builder
{
$this->applyMysqlFilters($this->filters, $this->query);
if ($this->searchTerm) {
$this->query->mysqlSearch($this->searchTerm);
}
return null;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Common\Database\Datasource\Filters;
use Common\Database\Datasource\Filters\Traits\SupportsMysqlFilters;
use Laravel\Scout\Builder;
class TntFilterer extends BaseFilterer
{
use SupportsMysqlFilters;
public function apply(): ?Builder
{
$constrains = $this->applyMysqlFilters(
$this->filters,
$this->query->getModel()->newInstance(),
);
return $this->query
->getModel()
->search($this->searchTerm)
->constrain($constrains);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Common\Database\Datasource\Filters\Traits;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
trait NormalizesFiltersForFulltextEngines
{
protected function normalizeFilters(array $filters): array
{
$normalizedFilters = [];
foreach ($filters as $index => $filter) {
// flatten "between" filter
if ($filter['operator'] === 'between') {
if ($start = Arr::get($filter, 'value.start')) {
$normalizedFilters[] = [
'key' => $filter['key'],
'operator' => '>=',
'value' => Carbon::parse($start)->timestamp,
];
}
if ($end = Arr::get($filter, 'value.end')) {
$normalizedFilters[] = [
'key' => $filter['key'],
'operator' => '<=',
'value' => Carbon::parse($end)->timestamp,
];
}
} else {
// normalize value and operator, so it's accepted by meilisearch, elastic and algolia
$normalizedFilters[] = [
'key' => $filter['key'],
'operator' => $this->normalizeFilterOperator($filter),
'value' => $this->normalizeFilterValue($filter),
];
}
}
return $normalizedFilters;
}
protected function normalizeFilterValue(array $filter): array|string
{
$value = $filter['value'];
if (is_string($value) && Str::contains($value, ' ')) {
return "'$value'";
} elseif ($value === null) {
return '_null';
} elseif ($value === false) {
return 'false';
} elseif ($value === true) {
return 'true';
} elseif (
in_array($filter['key'], $this->query->getModel()->getDates())
) {
return Carbon::parse($value)->timestamp;
} else {
return $value;
}
}
protected function normalizeFilterOperator(array $filter): string
{
if ($filter['operator'] === 'has') {
return '=';
} elseif ($filter['operator'] === 'doesntHave') {
return '!=';
} else {
return $filter['operator'];
}
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Common\Database\Datasource\Filters\Traits;
use Common\Database\Datasource\DatasourceFilters;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
trait SupportsMysqlFilters
{
public function applyMysqlFilters(DatasourceFilters $filters, $query)
{
foreach ($filters->getAll() as $filter) {
if ($filter['value'] === 'null') {
$filter['value'] = null;
} elseif ($filter['value'] === 'false') {
$filter['value'] = false;
}
if ($filter['value'] === 'true') {
$filter['value'] = true;
}
if ($filter['operator'] === 'between') {
$query->whereBetween(
$query->getModel()->qualifyColumn($filter['key']),
[$filter['value']['start'], $filter['value']['end']],
);
} elseif (
$filter['operator'] === 'has' ||
$filter['operator'] === 'doesntHave'
) {
$relName = $filter['key'];
$relation = $query->getModel()->$relName();
if (
$relation instanceof HasMany ||
$relation instanceof HasOne
) {
$query = $this->filterByHasManyRelation(
$query,
$relation,
$filter,
);
} elseif ($relation instanceof BelongsToMany) {
$query = $this->filterByManyToManyRelation(
$query,
$relation,
$filter,
);
}
} elseif ($filter['operator'] === 'hasAll') {
$genreIds = $filter['value'];
$relName = $filter['key'];
$relation = $query->getModel()->$relName();
$query->whereIn(
$query->getModel()->qualifyColumn('id'),
fn(Builder $query) => $query
->select($relation->getQualifiedForeignPivotKeyName())
->from($relation->getTable())
->whereIn(
$relation->getQualifiedRelatedPivotKeyName(),
$genreIds,
)
->when(
count($genreIds) > 1,
fn(Builder $query) => $query
->groupBy(
$relation->getQualifiedForeignPivotKeyName(),
)
->having(
DB::raw('COUNT(*)'),
'=',
count($genreIds),
),
),
);
} elseif (
$query->hasNamedScope('where' . ucfirst($filter['key']))
) {
$query->{'where' . ucfirst($filter['key'])}(
$filter['value'],
$filter['operator'],
$filter['key'],
);
} else {
$query = $query->where(
$query->qualifyColumn($filter['key']),
$filter['operator'],
$filter['value'],
);
}
}
return $query;
}
private function filterByHasManyRelation($query, $relation, array $filter)
{
$related = $relation->getRelated()->getTable();
$foreignKey = $relation->getQualifiedForeignKeyName();
$parentKey = $relation->getQualifiedParentKeyName();
// use left join to check if model has any of specified relations
if ($filter['value'] === '*') {
$query
// prevent null values from being returned when using left join
->when(empty($query->getQuery()->getColumns()), function ($q) {
$q->select($q->getModel()->getTable() . '.*');
})
->leftJoin($related, $foreignKey, '=', $parentKey)
->where(
$foreignKey,
$filter['operator'] === 'doesntHave' ? '=' : '!=',
null,
);
// use left join to check if model has relation with specified ID
} else {
$query
->leftJoin($related, $foreignKey, '=', $parentKey)
->where(
"$related.id",
$filter['operator'] === 'has' ? '=' : '!=',
$filter['value'],
);
if ($filter['operator'] === 'doesntHave') {
$this->query->orWhere("$related.id", null);
}
}
return $query;
}
private function filterByManyToManyRelation(
$query,
$relation,
array $filter,
) {
if ($filter['operator'] === 'has') {
$values = is_array($filter['value'])
? $filter['value']
: [$filter['value']];
$query->join(
$relation->getTable(),
$relation->getQualifiedParentKeyName(),
'=',
$relation->getQualifiedForeignPivotKeyName(),
);
$query->where(function ($q) use ($values, $relation) {
foreach ($values as $value) {
$q->orWhere(
$relation->getQualifiedRelatedPivotKeyName(),
'=',
$value,
);
}
});
} elseif ($filter['operator'] === 'doesntHave') {
$table = $query->getModel()->getTable();
$query->whereNotIn("$table.id", function (Builder $builder) use (
$filter,
$query,
) {
$relName = $filter['key'];
$relation = $query->getModel()->$relName();
$builder
->select($relation->getQualifiedForeignPivotKeyName())
->from($relation->getTable())
->where(
$relation->getQualifiedRelatedPivotKeyName(),
$filter['value'],
);
});
}
return $query;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Common\Database\Metrics;
use Common\Database\Metrics\Traits\RoundingPrecision;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;
abstract class BaseMetric
{
use RoundingPrecision;
protected Builder $query;
public function __construct(
public Builder|string|Model $model,
public MetricDateRange $dateRange,
public string|Expression|null $column = null,
public string|null $dateColumn = null,
protected int $limit = 100,
) {
$this->query =
$this->model instanceof Builder
? $this->model->clone()
: (new $this->model())->newQuery();
if (!$this->dateColumn) {
$this->dateColumn = $this->query
->getModel()
->getQualifiedCreatedAtColumn();
}
}
public function count(): array
{
return $this->aggregate('count');
}
public function average(): array
{
return $this->aggregate('avg');
}
public function sum(): array
{
return $this->aggregate('sum');
}
public function max(): array
{
return $this->aggregate('max');
}
public function min(): array
{
return $this->aggregate('min');
}
abstract protected function aggregate(string $function);
protected function getWrappedColumn(): string
{
$column =
$this->column ?: $this->query->getModel()->getQualifiedKeyName();
return $column instanceof Expression
? (string) $column
: $this->query
->getQuery()
->getGrammar()
->wrap($column);
}
protected function round(int|float $value): float
{
return round($value, $this->roundingPrecision, $this->roundingMode);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Common\Database\Metrics;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterval;
use Carbon\CarbonPeriod;
class MetricDateRange
{
const YEAR = 'year';
const MONTH = 'month';
const WEEK = 'week';
const DAY = 'day';
const HOUR = 'hour';
const MINUTE = 'minute';
public CarbonImmutable $start;
public CarbonImmutable $end;
public string $timezone;
public string $granularity;
public CarbonPeriod $period;
public function __construct(
string $start = null,
string $end = null,
string $timezone = null,
string $granularity = null,
) {
$this->start = $start
? CarbonImmutable::parse($start)->timezone($timezone)
: CarbonImmutable::today()->startOfWeek();
$this->end = $end
? CarbonImmutable::parse($end)->timezone($timezone)
: CarbonImmutable::today()->endOfWeek();
$this->timezone = $timezone ?: config('app.timezone');
$this->setGranularity($granularity);
$this->period = CarbonPeriod::create(
$this->start,
$this->end,
);
$this->period->setDateInterval(
CarbonInterval::make(1, $this->granularity),
);
}
public function getPreviousPeriod(): self
{
return new self(
$this->start->sub($this->end->diffAsCarbonInterval($this->start)),
$this->start->subSecond(),
);
}
public function getCacheKey(): string
{
return sprintf(
'%s-%s-%s',
$this->start->timestamp,
$this->end->timestamp,
$this->timezone,
);
}
protected function setGranularity(string $granularity = null): void {
// set unit specified by user
if ($granularity) {
$this->granularity = $granularity;
// set the smallest supported unit based on start and end date
} else {
if ($this->start->diffInYears($this->end) >= 3) {
$this->granularity = self::YEAR;
} elseif ($this->start->diffInMonths($this->end) >= 4) {
$this->granularity = self::MONTH;
} elseif ($this->start->diffInDays($this->end) > 14) {
$this->granularity = self::WEEK;
} elseif ($this->start->diffInDays($this->end) > 1) {
$this->granularity = self::DAY;
} elseif ($this->start->diffInHours($this->end) > 1) {
$this->granularity = self::HOUR;
} else {
$this->granularity = self::MINUTE;
}
}
}
/**
* Return format by which metric values should be grouped based on granularity.
*/
public function getGroupingFormat(): string {
return match ($this->granularity) {
$this::YEAR => 'Y',
$this::MONTH => 'Y-m',
$this::WEEK => 'o-W',
$this::DAY => 'Y-m-d',
$this::HOUR => 'Y-m-d H:00',
$this::MINUTE => 'Y-m-d H:i:00',
};
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Common\Database\Metrics;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
class Partition extends BaseMetric
{
public function __construct(
Model|Builder|string $model,
public string $groupBy,
MetricDateRange $dateRange,
string|Expression|null $column = null,
?string $dateColumn = null,
int $limit = 50,
protected array $additionalColumns = [],
) {
parent::__construct($model, $dateRange, $column, $dateColumn, $limit);
}
protected function aggregate(string $function): array
{
$select = [
$this->groupBy,
DB::raw("{$function}({$this->getWrappedColumn()}) as aggregate"),
...$this->additionalColumns,
];
$results = $this->query
->select($select)
->groupBy($this->groupBy, ...$this->additionalColumns)
->when(
$this->dateRange,
fn($query) => $query->whereBetween($this->dateColumn, [
$this->dateRange->start,
$this->dateRange->end,
]),
)
->limit($this->limit)
->get();
$data = $results->map(function ($result) {
$finalResult = [
'label' => $this->getLabel($result),
'value' => $this->round($result->aggregate),
];
foreach ($this->additionalColumns as $column) {
$finalResult[$column] = $result->{$column};
}
return $finalResult;
});
$total = $data->sum('value');
$data = $data
->map(function ($item) use ($total) {
$item['percentage'] = round((100 * $item['value']) / $total, 1);
return $item;
})
->sortByDesc('value')
->values();
return $data->all();
}
protected function getLabel(Model $result): string
{
$label = with(
$result->{last(explode('.', $this->groupBy))},
fn($key) => $key,
);
return __(ucfirst($label));
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Common\Database\Metrics\Traits;
use Carbon\CarbonInterface;
use Common\Database\Metrics\MetricDateRange;
trait GeneratesTrendResults
{
protected function getAllPossibleDateResults(
MetricDateRange $dateRange,
): array {
$format = $dateRange->getGroupingFormat();
// dates in range will already be in correct timezone
$possibleDateResults = [];
foreach ($dateRange->period as $date) {
$possibleDateResults[
(string) $date->format($format)
] = $this->formatTrendResult($dateRange->granularity, $date);
}
return $possibleDateResults;
}
protected function formatTrendResult(
string $granularity,
CarbonInterface $date,
int $value = 0,
): array {
if ($granularity === $this->dateRange::WEEK) {
return [
'date' => $date->startOfWeek()->toISOString(),
'endDate' => $date->endOfWeek()->toISOString(),
'value' => $value,
];
} else {
return [
'date' => $date->toISOString(),
'value' => $value,
];
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Common\Database\Metrics\Traits;
trait RoundingPrecision
{
public int $roundingPrecision = 0;
public int $roundingMode = PHP_ROUND_HALF_UP;
/**
* Set the precision level used when rounding the value.
*/
public function precision(
int $precision = 0,
int $mode = PHP_ROUND_HALF_UP,
): static {
$this->roundingPrecision = $precision;
if (
in_array($mode, [
PHP_ROUND_HALF_UP,
PHP_ROUND_HALF_DOWN,
PHP_ROUND_HALF_EVEN,
PHP_ROUND_HALF_ODD,
])
) {
$this->roundingMode = $mode;
}
return $this;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Common\Database\Metrics;
use Common\Database\Metrics\Traits\GeneratesTrendResults;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class Trend extends BaseMetric
{
use GeneratesTrendResults;
protected function aggregate(string $function): array
{
$expression = new TrendDateExpression(
$this->query,
$this->dateColumn,
$this->dateRange->granularity,
$this->dateRange->timezone,
);
$results = $this->query
->select(
DB::raw(
"{$expression->getValue(
DB::connection()->getQueryGrammar(),
)} as date_result, {$function}({$this->getWrappedColumn()}) as aggregate",
),
)
->whereBetween($this->dateColumn, [
$this->dateRange->start,
$this->dateRange->end,
])
->groupBy(DB::raw($expression))
->orderBy('date_result')
->limit($this->limit)
->get();
$mergedResults = array_replace(
$this->getAllPossibleDateResults($this->dateRange),
$results
->mapWithKeys(function ($result) {
return [
(string) $result->date_result => $this->formatTrendResult(
$this->dateRange->granularity,
$this->parseMysqlDate($result->date_result),
$result->aggregate,
),
];
})
->all(),
);
return array_values($mergedResults);
}
protected function parseMysqlDate(string $mysqlDate): Carbon
{
// dates coming from mysql will be in user's timezone (due to + Interval x HOUR),
// set user's timezone on carbon as well, so that "toIsoString" will return correct date.
if ($this->dateRange->granularity === $this->dateRange::WEEK) {
[$year, $week] = explode('-', $mysqlDate);
return Carbon::today($this->dateRange->timezone)->setISODate(
(int) $year,
(int) $week,
);
} else {
return Carbon::parse($mysqlDate, $this->dateRange->timezone);
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Common\Database\Metrics;
use Carbon\CarbonImmutable;
use DateTime;
use DateTimeZone;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Grammar;
use Illuminate\Database\Query\Expression;
class TrendDateExpression extends Expression
{
public function __construct(
protected Builder $query,
protected string $column,
protected string $unit,
protected string $timezone,
) {
}
public function getValue(Grammar $grammar): string
{
// dates in database are stored in UTC, and date_format will not return hour in many cases, so we need
// to convert date_format result to specified timezone in mysql because it can use the full timestamp
$offset = $this->offset();
if ($offset > 0) {
$interval = '+ INTERVAL ' . $offset . ' HOUR';
} elseif ($offset === 0) {
$interval = '';
} else {
$interval = '- INTERVAL ' . $offset * -1 . ' HOUR';
}
switch ($this->unit) {
case 'year':
return "date_format({$this->wrap(
$this->column,
)} {$interval}, '%Y')";
case 'month':
return "date_format({$this->wrap(
$this->column,
)} {$interval}, '%Y-%m')";
case 'week':
return "date_format({$this->wrap(
$this->column,
)} {$interval}, '%x-%v')";
case 'day':
return "date_format({$this->wrap(
$this->column,
)} {$interval}, '%Y-%m-%d')";
case 'hour':
return "date_format({$this->wrap(
$this->column,
)} {$interval}, '%Y-%m-%d %H:00')";
case 'minute':
return "date_format({$this->wrap(
$this->column,
)} {$interval}, '%Y-%m-%d %H:%i:00')";
}
}
protected function wrap(string $value): string
{
return $this->query
->getQuery()
->getGrammar()
->wrap($value);
}
protected function offset(): int
{
$timezoneOffset = function ($timezone) {
return (new DateTime(
CarbonImmutable::now()->format('Y-m-d H:i:s'),
new DateTimeZone($timezone),
))->getOffset() /
60 /
60;
};
$appOffset = $timezoneOffset(config('app.timezone'));
$userOffset = $timezoneOffset($this->timezone);
return $userOffset - $appOffset;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Common\Database\Metrics;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;
class ValueMetric extends BaseMetric
{
public function __construct(
Model|Builder|string $model,
MetricDateRange $dateRange,
string|Expression|null $column = null,
?string $dateColumn = null,
) {
parent::__construct($model, $dateRange, $column, $dateColumn);
}
protected function aggregate(string $function): array
{
$column =
$this->column ?: $this->query->getModel()->getQualifiedKeyName();
$previousValue = round(
(clone $this->query)
->when($this->dateRange, function ($query) {
$previous = $this->dateRange->getPreviousPeriod();
$query->whereBetween($this->dateColumn, [
$previous->start,
$previous->end,
]);
})
->{$function}($column) ?? 0,
$this->roundingPrecision,
$this->roundingMode,
);
$currentValue = round(
(clone $this->query)
->when(
$this->dateRange,
fn($query) => $query->whereBetween($this->dateColumn, [
$this->dateRange->start,
$this->dateRange->end,
]),
)
->{$function}($column) ?? 0,
$this->roundingPrecision,
$this->roundingMode,
);
if (!$currentValue && !$previousValue) {
$percentageChange = 0; // no change
} elseif (!$previousValue) {
$percentageChange = 100; // 100% increase
} else {
$percentageChange =
(($currentValue - $previousValue) / $previousValue) * 100;
}
return [
'previousValue' => $previousValue,
'currentValue' => $currentValue,
'percentageChange' => min(300, round($percentageChange, 2)),
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Common\Database;
use Common\Admin\Appearance\GenerateFavicon;
use Common\Core\Manifest\BuildManifestFile;
use Database\Seeders\DatabaseSeeder;
use File;
use Illuminate\Database\Eloquent\Model;
class MigrateAndSeed
{
public function execute(callable $afterMigrateCallback = null): void
{
// Migrate
if (!app('migrator')->repositoryExists()) {
app('migration.repository')->createRepository();
}
$migrator = app('migrator');
$paths = $migrator->paths();
$paths[] = app('path.database') . DIRECTORY_SEPARATOR . 'migrations';
$migrator->run($paths);
$afterMigrateCallback && $afterMigrateCallback();
$this->runCommonSeeders();
// Seed
$seeder = class_exists(\DatabaseSeeder::class)
? app(\DatabaseSeeder::class)
: app(DatabaseSeeder::class);
$seeder->setContainer(app());
Model::unguarded(function () use ($seeder) {
$seeder->__invoke();
});
// Manifest
app(BuildManifestFile::class)->execute();
$defaultFaviconPath = public_path('images/favicon-original.png');
if (!env('INSTALLED') && file_exists($defaultFaviconPath)) {
app(GenerateFavicon::class)->execute($defaultFaviconPath);
}
}
public function runCommonSeeders(): void
{
$paths = File::files(app('path.common') . '/Database/Seeds');
foreach ($paths as $fileInfo) {
Model::unguarded(function () use ($fileInfo) {
$namespace =
'Common\Database\Seeds\\' . $fileInfo->getBaseName('.php');
$seeder = app($namespace)->setContainer(app());
$seeder->__invoke();
});
}
}
}

324
common/Database/Paginator.php Executable file
View File

@@ -0,0 +1,324 @@
<?php namespace Common\Database;
use Cache;
use Carbon\Carbon;
use Closure;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class Paginator
{
/**
* @var Builder
*/
private $query;
/**
* @var Model
*/
private $model;
/**
* @var string
*/
private $defaultOrderColumn = 'updated_at';
/**
* @var string
*/
private $defaultOrderDirection = 'desc';
/**
* @var Closure
*/
public $secondaryOrderCallback;
/**
* @var int
*/
public $defaultPerPage = 15;
/**
* @var string
*/
public $searchColumn = 'name';
/**
* @var array
*/
public $filterColumns = [];
/**
* @var Closure
*/
public $searchCallback;
/**
* @var array
*/
private $params;
/**
* @var bool
*/
public $dontSort;
public function __construct($model, array $params)
{
$this->model = $model;
$this->params = $this->toCamelCase($params);
$this->query = $model->newQuery();
}
public function paginate(): LengthAwarePaginator
{
$with = array_filter(explode(',', $this->param('with', '')));
$withCount = array_filter(explode(',', $this->param('withCount', '')));
$searchTerm = $this->param('query');
$order = $this->getOrder();
$perPage = $this->param('perPage', $this->defaultPerPage);
$page = (int) $this->param('page', 1);
// load specified relations and counts
if (!empty($with)) {
$this->query->with($with);
}
if (!empty($withCount)) {
$this->query->withCount($withCount);
}
// search
if ($searchTerm) {
if ($this->searchCallback) {
call_user_func(
$this->searchCallback,
$this->query,
$searchTerm,
);
} else {
$this->query->where(
$this->searchColumn,
'like',
"$searchTerm%",
);
}
}
$this->applyFilters();
// order
if (!$this->dontSort) {
if ($this->isRawOrder($order['col'])) {
$this->query->orderByRaw($order['col']);
} else {
$this->query->orderBy($order['col'], $order['dir']);
}
if ($this->secondaryOrderCallback) {
call_user_func(
$this->secondaryOrderCallback,
$this->query,
$order['col'],
$order['dir'],
);
}
}
$countCacheKeyQuery = $this->query
->toBase()
->cloneWithout(['columns', 'orders', 'limit', 'offset']);
$countCacheKye = base64_encode(
Str::replaceArray(
'?',
$countCacheKeyQuery->getBindings(),
$countCacheKeyQuery->toSql(),
),
);
$total = null;
if ($countCacheKye) {
$total = Cache::get($countCacheKye);
}
if (is_null($total)) {
$total = $this->query->toBase()->getCountForPagination();
if ($total > 500000) {
Cache::put($countCacheKye, $total, Carbon::now()->addDay());
}
}
$items = $total
? $this->query->forPage($page, $perPage)->get()
: new Collection();
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $items,
'total' => $total,
'perPage' => $perPage,
'page' => $page,
]);
}
public function param(string $name, $default = null)
{
return Arr::get($this->params, Str::camel($name)) ?: $default;
}
public function setParam(string $name, $value): self
{
$this->params[$name] = $value;
return $this;
}
/**
* @return Builder
*/
public function query()
{
return $this->query;
}
/**
* Load specified relation counts with paginator items.
*
* @param mixed $relations
* @return $this
*/
public function withCount($relations)
{
$this->query->withCount($relations);
return $this;
}
/**
* Load specified relations of paginated items.
*
* @param mixed $relations
* @return $this
*/
public function with($relations)
{
$this->query->with($relations);
return $this;
}
/**
* @param $column
* @param null $operator
* @param null $value
* @param string $boolean
* @return $this
*/
public function where(
$column,
$operator = null,
$value = null,
$boolean = 'and'
) {
$this->query->where($column, $operator, $value, $boolean);
return $this;
}
public function setDefaultOrderColumns($column, $direction = 'desc'): self
{
$this->defaultOrderColumn = $column;
$this->defaultOrderDirection = $direction;
return $this;
}
/**
* @return array
*/
public function getOrder()
{
// order provided as single string: "column|direction"
if ($specifiedOrder = $this->param('order')) {
$parts = preg_split('(\||:)', $specifiedOrder);
$orderCol = Arr::get($parts, 0, $this->defaultOrderColumn);
$orderDir = Arr::get($parts, 1, $this->defaultOrderDirection);
} else {
$orderCol = $this->param('orderBy', $this->defaultOrderColumn);
$orderDir = $this->param('orderDir', $this->defaultOrderDirection);
}
return [
'dir' => Str::snake($orderDir),
'col' => !$this->isRawOrder($orderCol)
? Str::snake($orderCol)
: $orderCol,
];
}
private function toCamelCase($params)
{
return collect($params)
->keyBy(function ($value, $key) {
return Str::camel($key);
})
->toArray();
}
private function applyFilters()
{
foreach ($this->filterColumns as $column => $callback) {
$column = is_int($column) ? $callback : $column;
$column = Str::camel($column);
if (isset($this->params[$column])) {
$value = $this->params[$column];
$column = Str::snake($column);
// user specified callback
if (is_callable($callback)) {
$callback($this->query, $value);
// boolean filter
} elseif ($value === 'false' || $value === 'true') {
$this->applyBooleanFilter($column, $value);
// filter by between date
} elseif (
\Str::contains($column, '_at') &&
\Str::contains($value, ':')
) {
$this->query()->whereBetween($column, explode(':', $value));
// filter by specified column value
} else {
$this->query()->where($column, $value);
}
}
}
}
/**
* @param string $column
* @param string $value
*/
private function applyBooleanFilter($column, $value)
{
// cast "true" or "false" to boolean
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
$casts = $this->model->getCasts();
// column is a simple boolean type
if (Arr::get($casts, $column) === 'boolean') {
$this->query()->where($column, $value);
// column has actual value, test whether it's null or not by default
} else {
if ($value) {
$this->query()->whereNotNull($column);
} else {
$this->query()->whereNull($column);
}
}
}
private function isRawOrder(string $order): bool
{
return Str::contains($order, ['(', ')']);
}
}

View File

@@ -0,0 +1,50 @@
<?php namespace Common\Database\Seeds;
use App\Models\User;
use Common\Admin\Appearance\Themes\CssTheme;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Seeder;
class CssThemesTableSeeder extends Seeder
{
public function run(): void
{
$dark = config('common.themes.dark');
$light = config('common.themes.light');
$admin = User::whereHas('permissions', function (Builder $builder) {
$builder->where('name', 'admin');
})->first();
$darkTheme = CssTheme::where('default_dark', true)
->orWhere('name', 'Dark')
->first();
if (!$darkTheme || !$darkTheme->getRawOriginal('values')) {
if ($darkTheme) {
$darkTheme->delete();
}
CssTheme::create([
'name' => 'Dark',
'is_dark' => true,
'default_dark' => true,
'values' => $dark,
'user_id' => $admin ? $admin->id : 1,
]);
}
$lightTheme = CssTheme::where('default_light', true)
->orWhere('name', 'Light')
->first();
if (!$lightTheme || !$lightTheme->getRawOriginal('values')) {
if ($lightTheme) {
$lightTheme->delete();
}
CssTheme::create([
'name' => 'Light',
'default_light' => true,
'user_id' => $admin ? $admin->id : 1,
'values' => $light,
]);
}
}
}

View File

@@ -0,0 +1,79 @@
<?php namespace Common\Database\Seeds;
use Common\Pages\CustomPage;
use Illuminate\Database\Seeder;
class DefaultPagesSeeder extends Seeder
{
public function run(): void
{
CustomPage::firstOrCreate(
[
'slug' => 'privacy-policy',
],
[
'title' => 'Example Privacy Policy',
'slug' => 'privacy-policy',
'body' => $this->replacePlaceholders(
file_get_contents(
base_path(
'common/resources/defaults/privacy-policy.html',
),
),
),
'type' => 'default',
],
);
CustomPage::firstOrCreate(
[
'slug' => 'terms-of-service',
],
[
'title' => 'Example Terms of Service',
'slug' => 'terms-of-service',
'body' => $this->replacePlaceholders(
file_get_contents(
base_path(
'common/resources/defaults/terms-of-service.html',
),
),
),
'type' => 'default',
],
);
CustomPage::firstOrCreate(
[
'slug' => 'about-us',
],
[
'title' => 'Example About Us',
'slug' => 'about-us',
'body' => file_get_contents(
base_path('common/resources/lorem.html'),
),
'type' => 'default',
],
);
}
protected function replacePlaceholders(string $text): string
{
return str_replace(
[
'[Website Name]',
'[Website URL]',
'[Contact Email]',
'[Your Country/State]',
],
[
config('app.name'),
url('/'),
settings('mail.contact_page_address'),
'United States',
],
$text,
);
}
}

View File

@@ -0,0 +1,45 @@
<?php namespace Common\Database\Seeds;
use Common\Localizations\Localization;
use Common\Localizations\LocalizationsRepository;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Seeder;
class LocalizationsTableSeeder extends Seeder
{
public function __construct(protected LocalizationsRepository $repository)
{
}
public function run()
{
$localizations = Localization::all();
if ($localizations->isNotEmpty()) {
$this->mergeExistingTranslationLines($localizations);
} else {
$this->repository->create([
'name' => 'English',
'language' => 'en',
]);
}
}
/**
* Merge existing localization translation lines with default ones.
*/
private function mergeExistingTranslationLines(Collection $localizations)
{
$defaultLines = $this->repository->getDefaultTranslationLines();
$localizations->each(function ($localization) use ($defaultLines) {
$this->repository->storeLocalizationLines(
$localization,
array_merge(
$defaultLines,
$this->repository->getLocalizationLines($localization),
),
);
});
}
}

View File

@@ -0,0 +1,14 @@
<?php namespace Common\Database\Seeds;
use Illuminate\Database\Seeder;
class MailTemplatesSeeder extends Seeder
{
/**
* @return void
*/
public function run()
{
//
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace Common\Database\Seeds;
use Common\Auth\Permissions\Permission;
use Common\Core\Values\GetStaticPermissions;
use Illuminate\Database\Seeder;
class PermissionTableSeeder extends Seeder
{
public function run(): void
{
$allPermissions = app(GetStaticPermissions::class)->execute();
$allPermissions['admin'][] = [
'name' => 'admin',
'display_name' => 'Super Admin',
'description' => 'Give all permissions to user.',
'group' => 'admin',
];
foreach ($allPermissions as $groupName => $group) {
foreach ($group as $permission) {
$permission['group'] = $groupName;
app(Permission::class)->updateOrCreate(['name' => $permission['name']], $permission);
}
}
// delete legacy permissions
app(Permission::class)->whereNull('group')->delete();
}
}

View File

@@ -0,0 +1,128 @@
<?php namespace Common\Database\Seeds;
use App\Models\User;
use Common\Auth\Permissions\Permission;
use Common\Auth\Permissions\Traits\SyncsPermissions;
use Common\Auth\Roles\Role;
use Illuminate\Database\Seeder;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class RolesTableSeeder extends Seeder
{
use SyncsPermissions;
private array $commonConfig = [];
private array $appConfig = [];
public function __construct(
protected Role $role,
protected User $user,
protected Permission $permission,
protected Filesystem $fs,
) {
}
public function run(): void
{
$this->commonConfig = File::getRequire(
app('path.common') . '/resources/defaults/permissions.php',
);
$this->appConfig = File::getRequire(
resource_path('defaults/permissions.php'),
);
foreach ($this->appConfig['roles'] as $appRole) {
if ($commonRoleName = Arr::get($appRole, 'extends')) {
$commonRole = $this->findRoleConfig($commonRoleName);
$appRole = array_merge($commonRole, $appRole);
$appRole['permissions'] = array_merge(
$commonRole['permissions'],
$appRole['permissions'],
);
}
// skip billing permissions if billing is not integrated
$appRole['permissions'] = array_filter(
$appRole['permissions'],
function ($permission) {
if (is_array($permission)) {
$permission = $permission['name'];
}
return config('common.site.billing_integrated') ||
!Str::contains($permission, ['invoice.', 'plans.']);
},
);
$this->createOrUpdateRole($appRole);
}
}
private function findRoleConfig(string $roleName): array
{
$roleConfig = Arr::first($this->commonConfig['roles'], function (
$role,
) use ($roleName) {
return $role['name'] === $roleName;
});
if (!$roleConfig) {
$roleConfig = Arr::first($this->appConfig['roles'], function (
$role,
) use ($roleName) {
return $role['name'] === $roleName;
});
}
return $roleConfig;
}
private function createOrUpdateRole(array $appRole): Role
{
$defaultPermissions = collect($appRole['permissions'])->map(
fn($permission) => is_string($permission)
? ['name' => $permission]
: $permission,
);
$dbPermissions = Permission::whereIn(
'name',
$defaultPermissions->pluck('name'),
)->get();
$dbPermissions->map(function (Permission $permission) use (
$defaultPermissions,
) {
$defaultPermission = $defaultPermissions
->where('name', $permission['name'])
->first();
$permission['restrictions'] =
Arr::get($defaultPermission, 'restrictions') ?: [];
return $permission;
});
if (Arr::get($appRole, 'default')) {
$attributes = ['default' => true];
Role::where('name', $appRole['name'])->update(['default' => true]);
} elseif (Arr::get($appRole, 'guests')) {
$attributes = ['guests' => true];
Role::where('name', $appRole['name'])->update(['guests' => true]);
} else {
$attributes = ['name' => $appRole['name']];
}
if ($role = Role::where($attributes)->first()) {
return $role;
} else {
$role = $this->role->create(
Arr::except($appRole, ['permissions', 'extends']),
);
$this->syncPermissions(
$role,
$role->permissions->concat($dbPermissions),
);
$role->save();
return $role;
}
}
}

View File

@@ -0,0 +1,109 @@
<?php namespace Common\Database\Seeds;
use Carbon\Carbon;
use Common\Settings\Setting;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class SettingsTableSeeder extends Seeder
{
public function __construct(protected Setting $setting)
{
}
public function run()
{
$defaultSettings = config('common.default-settings');
$names = [];
$defaultSettings = array_map(function ($setting) use (&$names) {
$names[] = $setting['name'];
$setting['created_at'] = Carbon::now();
$setting['updated_at'] = Carbon::now();
//make sure all settings have "private" field to
//avoid db errors due to different column count
if (!array_key_exists('private', $setting)) {
$setting['private'] = 0;
}
// cast booleans to string as "insert"
// method will not use Setting model setters
if ($setting['value'] === true) {
$setting['value'] = 'true';
} elseif ($setting['value'] === false) {
$setting['value'] = 'false';
}
$setting['value'] = (string) $setting['value'];
// add ids to menus and menu items, if don't have one already
if ($setting['name'] === 'menus') {
$value = json_decode($setting['value'], true);
foreach ($value as &$menu) {
if (!isset($menu['id'])) {
$menu['id'] = Str::random(6);
}
foreach ($menu['items'] as &$item) {
if (!isset($item['id'])) {
$item['id'] = Str::random(6);
}
}
}
$setting['value'] = json_encode($value);
}
return $setting;
}, $defaultSettings);
$existing = $this->setting->whereIn('name', $names)->pluck('name');
//only insert settings that don't already exist in database
$new = array_filter($defaultSettings, function ($setting) use (
$existing,
) {
return !$existing->contains($setting['name']);
});
$this->setting->insert($new);
$this->mergeMenusSetting($defaultSettings);
}
/**
* Merge existing menus setting json with new one.
*/
private function mergeMenusSetting(array $defaultSettings): void
{
$existing =
$this->setting->where('name', 'menus')->first()->value ?? [];
$new = json_decode(
Arr::first(
$defaultSettings,
fn($value) => $value['name'] === 'menus',
)['value'],
true,
);
foreach ($new as $newMenu) {
$alreadyHas = Arr::first(
$existing,
fn($value) => $value['name'] === $newMenu['name'],
);
foreach ($newMenu['items'] as $index => $item) {
$newMenu['items'][$index]['order'] = $index;
}
if (!$alreadyHas) {
$existing[] = $newMenu;
}
}
$this->setting
->where('name', 'menus')
->update(['value' => json_encode($existing)]);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Common\Database\Traits;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
trait AddsIndexToExistingTable
{
protected function addIndexIfDoesNotExist(Blueprint $table, string $column) {
$prefix = Schema::getConnection()->getTablePrefix();
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$tableName = "{$prefix}{$table->getTable()}";
$indexesFound = $sm->listTableIndexes($tableName);
if (!array_key_exists("{$tableName}_{$column}_index", $indexesFound)) {
$table->index($column);
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('users')) {
return;
}
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('username', 100)->nullable();
$table->string('first_name', 100)->nullable();
$table->string('last_name', 100)->nullable();
$table->string('avatar_url')->nullable();
$table->string('gender', 20)->nullable();
$table->text('permissions')->nullable();
$table->string('email')->unique();
$table->string('password', 60);
$table->string('card_brand', 30)->nullable();
$table->string('card_last_four', 4)->nullable();
$table->rememberToken();
$table
->timestamp('created_at')
->index()
->nullable();
$table
->timestamp('updated_at')
->index()
->nullable();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePasswordResetsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('password_resets')) return;
Schema::create('password_resets', function (Blueprint $table) {
$table->increments('id');
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('password_resets');
}
}

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSocialProfilesTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('social_profiles')) return;
Schema::create('social_profiles', function(Blueprint $table)
{
$table->increments('id');
$table->integer('user_id');
$table->string('service_name', 20);
$table->string('user_service_id');
$table->string('username')->nullable();
$table->timestamps();
$table->index('user_id');
$table->unique(['user_id', 'service_name']);
$table->unique(['service_name', 'user_service_id']);
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('social_profiles');
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSettingsTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('settings')) return;
Schema::create('settings', function(Blueprint $table)
{
$table->increments('id');
$table->string('name')->unique();
$table->text('value');
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('settings');
}
}

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateFollowsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('follows')) {
return;
}
Schema::create('follows', function (Blueprint $table) {
$table->increments('id');
$table->integer('follower_id');
$table->integer('followed_id');
$table->timestamps();
$table->unique(['follower_id', 'followed_id']);
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('follows');
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('tags')) return;
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->index()->unique();
$table->string('display_name')->nullable();
$table->string('type', 30)->index()->default('custom');
$table->timestamp('created_at')->index()->nullable();
$table->timestamp('updated_at')->index()->nullable();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('tags');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateTaggablesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('taggables', function (Blueprint $table) {
$table->increments('id');
$table->integer('tag_id')->unsigned()->index();
$table->integer('taggable_id')->unsigned()->index();
$table->string('taggable_type', 80)->index();
$table->integer('user_id')->unsigned()->index()->nullable();
$table->unique(['tag_id', 'taggable_id', 'user_id', 'taggable_type'], 'taggable_unique');
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('taggables');
}
}

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateUploadsTable extends Migration
{
public function up()
{
Schema::create('uploads', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->index();
$table->string('file_name', 36)->unique();
$table->string('file_size');
$table->string('mime');
$table->string('extension');
$table->string('user_id')->index();
$table->string('url')->nullable();
$table->string('thumbnail_url')->nullable();
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
public function down()
{
Schema::drop('uploads');
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateUploadablesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('uploadables', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('upload_id');
$table->unsignedInteger('uploadable_id');
$table->string('uploadable_type', 60);
$table->unique(['upload_id', 'uploadable_id', 'uploadable_type'], 'uploadable_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('uploadables');
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateGroupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('groups')) return;
Schema::create('groups', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
$table->text('permissions')->nullable();
$table->boolean('default')->default(0)->unsigned()->index();
$table->boolean('guests')->default(0)->unsigned()->index();
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('groups');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUserGroupTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_group', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id');
$table->integer('group_id');
$table->timestamp('created_at')->nullable();
$table->unique(['user_id', 'group_id']);
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('user_group');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('pages')) return;
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->longText('body');
$table->string('slug')->unique()->index();
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('pages');
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateLocalizationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('localizations', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->index();
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('localizations');
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPrivateFieldToSettingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('settings', 'private')) return;
Schema::table('settings', function (Blueprint $table) {
$table->boolean('private')->default(0)->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('private');
});
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddColumnsToUsersTable extends Migration
{
public function up()
{
if (Schema::hasColumn('users', 'language')) return;
Schema::table('users', function (Blueprint $table) {
$table->string('language', 6)->nullable();
$table->string('country', 40)->nullable();
$table->string('timezone', 30)->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('language');
$table->dropColumn('country');
$table->dropColumn('timezone');
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MakePasswordColumnNullable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('password', 60)->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('password', 60)->change();
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MakeSettingsValueColumnNullable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('settings', function (Blueprint $table) {
$table->text('value')->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('settings', function (Blueprint $table) {
$table->text('value')->change();
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPublicColumnToUploadsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('uploads', function (Blueprint $table) {
$table->boolean('public')->default(0)->index();
// set collation to latin1_bin for maximum encoded string length
$pathColumn = $table->string('path')->nullable();
$pathColumn->collation = 'latin1_bin';
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('uploads', function (Blueprint $table) {
$table->dropColumn('public');
$table->dropColumn('path');
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddAvatarColumnToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('users', 'avatar')) return;
Schema::table('users', function (Blueprint $table) {
$table->string('avatar')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->removeColumn('avatar');
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
class CreateSubscriptionsTable extends Migration
{
public function up()
{
Schema::create('subscriptions', function ($table) {
$table->increments('id');
$table->integer('user_id')->index();
$table->string('plan_id')->index();
$table
->string('gateway')
->default('none')
->index();
$table
->string('gateway_id')
->nullable()
->unique()
->index();
$table->integer('quantity')->default(1);
$table->text('description')->nullable();
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamp('renews_at')->nullable();
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
public function down()
{
Schema::drop('subscriptions');
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBillingToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('users', 'stripe_id')) return;
Schema::table('users', function (Blueprint $table) {
$table->string('stripe_id')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('stripe_id');
});
}
}

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBillingPlansTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('billing_plans', function(Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('amount')->nullable();
$table->string('currency');
$table->string('currency_symbol')->default('$');
$table->string('interval')->default('month');
$table->integer('interval_count')->default(1);
$table->integer('parent_id')->nullable();
$table->text('permissions')->nullable();
$table->uuid('uuid');
$table->boolean('recommended')->default(0);
$table->boolean('free')->default(0);
$table->boolean('show_permissions')->default(0);
$table->text('features')->nullable();
$table->integer('position')->default(0);
$table->timestamps();
$table->collation = config('database.connections.mysql.collation');
$table->charset = config('database.connections.mysql.charset');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('billing_plans');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddAvailableSpaceToBillingPlansTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->bigInteger('available_space')->nullable()->unsigned();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->dropColumn('available_space');
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddAvailableSpaceToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('users', 'available_space')) return;
Schema::table('users', function (Blueprint $table) {
$table->bigInteger('available_space')->nullable()->unsigned();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if ( ! Schema::hasColumn('users', 'available_space')) return;
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('available_space');
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenameGroupsToRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('roles')) {
Schema::table('groups', function (Blueprint $table) {
$table->rename('roles');
});
}
if ( ! Schema::hasTable('user_role')) {
Schema::table('user_group', function (Blueprint $table) {
$table->rename('user_role');
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('roles', function (Blueprint $table) {
$table->rename('groups');
});
Schema::table('user_role', function (Blueprint $table) {
$table->rename('user_group');
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenameUserRoleTableColumnsToRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('user_role', function (Blueprint $table) {
$table->renameColumn('group_id', 'role_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_role', function (Blueprint $table) {
$table->renameColumn('role_id', 'group_id');
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenameUploadsToFileEntries extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('file_entries')) return;
Schema::table('uploads', function (Blueprint $table) {
$table->rename('file_entries');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->rename('uploads');
});
}
}

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RefactorFileEntriesColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// refactoring was already done via another (app specific) migration
if (Schema::hasColumn('file_entries', 'public_path')) return;
Schema::table('file_entries', function (Blueprint $table) {
$table->bigInteger('file_size')->unsigned()->nullable()->change();
$table->integer('parent_id')->nullable()->index();
$table->string('description', 150)->nullable();
$table->string('mime', 100)->nullable()->change();
$table->string('extension', 10)->nullable()->change();
$table->string('password', 50)->nullable();
$table->string('type', 20)->nullable()->index();
$table->timestamp('deleted_at')->nullable()->index();
$table->dropColumn('url');
$table->dropColumn('thumbnail_url');
$table->renameColumn('path', 'public_path');
$table->string('user_id')->index()->nullable()->change();
$table->index('updated_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->dropColumn('parent_id');
$table->dropColumn('description');
$table->dropColumn('password');
$table->dropColumn('deleted_at');
});
}
}

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddFolderPathColumnToFileEntriesTable extends Migration
{
public function up()
{
if (Schema::hasColumn('file_entries', 'path')) return;
Schema::table('file_entries', function (Blueprint $table) {
$table->string('path')->nullable()->index();
});
}
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->dropColumn('path');
});
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Eloquent\Collection;
use Common\Files\FileEntry;
use Illuminate\Database\Migrations\Migration;
class MigrateFileEntryUsersToManyToMany extends Migration
{
/**
* migrate file entries => user from "one to one" to "many to many"
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('user_file_entry')) {
return;
}
FileEntry::select('id', 'user_id')->orderBy('id')->chunk(50, function(Collection $entries) {
$records = $entries->map(function(FileEntry $entry) {
return ['file_entry_id' => $entry->id, 'user_id' => $entry->user_id, 'owner' => 1];
});
DB::table('user_file_entry')->insert($records->toArray());
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MoveUploadsIntoSubfolders extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$drive = Storage::drive(config('common.site.uploads_disk'));
foreach ($drive->files() as $fileName) {
$pathinfo = pathinfo($fileName);
if ( ! isset($pathinfo['extension']) && ! \Str::contains($fileName, '.')) {
$drive->createDir("$fileName-temp");
$drive->move($fileName, "$fileName-temp/$fileName");
$drive->rename("$fileName-temp", $fileName);
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenameUploadablesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::rename('uploadables', 'file_entry_models');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::rename('file_entry_models', 'uploadables');
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenameFileEntryModelsTableColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entry_models', function (Blueprint $table) {
$table->renameColumn('upload_id', 'file_entry_id');
$table->renameColumn('uploadable_id', 'model_id');
$table->renameColumn('uploadable_type', 'model_type');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entry_models', function (Blueprint $table) {
$table->renameColumn('file_entry_id', 'upload_id');
$table->renameColumn('model_id', 'uploadable_id');
$table->renameColumn('model_type', 'uploadable_type');
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTypeAndTitleColumnsToPagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function (Blueprint $table) {
if ( ! Schema::hasColumn('pages', 'type')) {
$table->string('type', 20)->index()->default('default')->after('slug');
}
if ( ! Schema::hasColumn('pages', 'title')) {
$table->string('title')->nullable()->after('id');
} else {
$table->string('title')->nullable()->change();
}
$table->text('meta')->nullable()->after('slug');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('type');
$table->dropColumn('title');
$table->dropColumn('meta');
});
}
}

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeUniqueIndexOnTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tags', function (Blueprint $table) {
$table->string('type', 30)->default('custom')->change();
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $sm->listTableIndexes('tags');
if (array_key_exists('tags_name_unique', $indexesFound)) {
$table->dropUnique('tags_name_unique');
}
if (array_key_exists('tags_name_type_unique', $indexesFound)) {
$table->dropUnique('tags_name_type_unique');
}
$table->unique(['name', 'type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class DeleteOldSeoSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('settings')->where('name', 'LIKE', 'seo.%')->delete();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue');
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
$table->index(['queue', 'reserved_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('jobs');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPreviewTokenToFileEntriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->string('preview_token', 15)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->dropColumn('preview_token');
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddThumbnailColumnToFileEntriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->boolean('thumbnail')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->dropColumn('thumbnail');
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPaypalIdColumnToBillingPlansTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->string('paypal_id', 50)->nullable()->after('uuid');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->dropColumn('paypal_id');
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class IndexDescriptionColumnInFileEntriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->index('description');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
//
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCustomDomainsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('custom_domains')) return;
Schema::create('custom_domains', function (Blueprint $table) {
$table->increments('id');
$table->string('host', 100)->index()->unique();
$table->integer('user_id')->index();
$table->timestamp('created_at')->index()->nullable();
$table->timestamp('updated_at')->index()->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('custom_domains');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddUserIdColumnToPagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function (Blueprint $table) {
$table->integer('user_id')->nullable()->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenamePagesTableToCustomPages extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('custom_pages')) {
Schema::table('pages', function (Blueprint $table) {
$table->rename('custom_pages');
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if ( ! Schema::hasTable('pages')) {
Schema::table('custom_pages', function (Blueprint $table) {
$table->rename('pages');
});
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePermissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('permissions')) return;
Schema::create('permissions', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 30)->unique();
$table->string('display_name');
$table->text('description')->nullable();
$table->string('group', 30);
$table->text('restrictions')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('permissions');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePermissionablesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('permissionables')) return;
Schema::create('permissionables', function (Blueprint $table) {
$table->increments('id');
$table->integer('permission_id')->index();
$table->integer('permissionable_id')->index();
$table->string('permissionable_type', 40)->index();
$table->text('restrictions')->nullable();
$table->unique(['permission_id', 'permissionable_id', 'permissionable_type'], 'permissionable_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('permissionables');
}
}

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenamePermissionsColumns extends Migration
{
public function up()
{
$tables = ['users', 'roles', 'billing_plans'];
foreach ($tables as $tableName) {
// rename permissions column
if (Schema::hasColumn($tableName, 'permissions')) {
Schema::table($tableName, function (Blueprint $table) {
$table->renameColumn('permissions', 'legacy_permissions');
});
}
// drop permissions index, if exists
Schema::table($tableName, function (Blueprint $table) use($tableName) {
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $sm->listTableIndexes($tableName);
if (array_key_exists('legacy_permissions', $indexesFound)) {
$table->dropIndex('legacy_permissions');
}
if (array_key_exists('permissions', $indexesFound)) {
$table->dropIndex('permissions');
}
});
}
}
public function down()
{
//
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCssThemesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('css_themes')) return;
Schema::create('css_themes', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 100)->unique();
$table->boolean('is_dark')->default(0);
$table->boolean('default_light')->index()->default(0);
$table->boolean('default_dark')->index()->default(0);
$table->integer('user_id')->index();
$table->text('colors');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('css_themes');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateInvoicesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('invoices')) return;
Schema::create('invoices', function (Blueprint $table) {
$table->increments('id');
$table->integer('subscription_id')->index();
$table->boolean('paid');
$table->string('uuid', 10)->index();
$table->text('notes')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('invoices');
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddGlobalColumnToCustomDomainsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('custom_domains', 'global')) return;
Schema::table('custom_domains', function (Blueprint $table) {
$table->boolean('global')->index()->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('custom_domains', function (Blueprint $table) {
$table->dropColumn('global');
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangePlanAmountToFloat extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('billing_plans', function(Blueprint $table) {
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}billing_plans CHANGE amount amount DECIMAL(13,2) NULL");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->integer('amount')->nullable()->change();
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddIndexToUsernameColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->index('username');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropIndex('username');
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('comments')) return;
Schema::create('comments', function (Blueprint $table) {
$table->increments('id');
$table->text('content');
$table->integer('parent_id')->unsigned()->nullable()->index();
$table->string('path')->index();
$table->integer('user_id')->unsigned()->index();
$table->integer('commentable_id')->unsigned()->index();
$table->string('commentable_type', 30)->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('notifications')) return;
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('notifications');
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddResourceIdAndTypeToCustomDomainsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('custom_domains', function (Blueprint $table) {
$table->integer('resource_id')->unsigned()->index()->nullable();
$table->string('resource_type', 20)->index()->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('custom_domains', function (Blueprint $table) {
$table->dropColumn('resource_id');
$table->dropColumn('resource_type');
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('personal_access_tokens')) return;
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenamePublicPathColumnToDiskPrefix extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entries', function (Blueprint $table) {
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}file_entries CHANGE public_path disk_prefix VARCHAR(191) NULL");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
//
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeFileSizeColumnDefaultValueTo0 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entries', function (Blueprint $table) {
$table->bigInteger('file_size')->unsigned()->default(0)->notNull()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_entries', function (Blueprint $table) {
//
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateFileEntryModelsTableToV2 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('file_entry_models', function (Blueprint $table) {
$table->timestamps();
$table->boolean('owner')->default(0)->index();
$table->text('permissions')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,47 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
class MoveUserFileEntryTableRecordsToFileEntryModels extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('user_file_entry')) {
return;
}
DB::table('user_file_entry')
->chunkById(100, function(Collection $records) {
$records = $records->map(function($record) {
$record = (array) $record;
$record['model_type'] = User::class;
$record['model_id'] = $record['user_id'];
unset($record['user_id']);
return $record;
});
try {
DB::table('file_entry_models')->insert($records->toArray());
} catch (Exception $e) {
//
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateNotificationSubscriptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('notification_subscriptions')) return;
Schema::create('notification_subscriptions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('notif_id', 5)->index();
$table->integer('user_id')->index();
$table->string('channels');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('notification_subscriptions');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddLanguageColToLocalizationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('localizations', function (Blueprint $table) {
$table->string('language', 5)->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('localizations', function (Blueprint $table) {
$table->dropColumn('language');
});
}
}

View File

@@ -0,0 +1,49 @@
<?php
use Common\Core\Values\ValueLists;
use Common\Localizations\Localization;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Arr;
class AddLangCodeToExistingLocalizations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$languages = app(ValueLists::class)->languages();
app(Localization::class)->get()->each(function(Localization $localization) use($languages) {
if ( ! $localization->language) {
$lang = Arr::first($languages, function($lang) use($localization) {
return slugify($lang['name']) === slugify($localization->name);
});
if ( ! $lang) {
$lang = Arr::random($languages);
}
$localization->language = $lang['code'];
$localization->save();
}
$slugName = slugify($localization->name);
$oldPath = resource_path("lang/$slugName.json");
if (file_exists($oldPath)) {
rename($oldPath, resource_path("lang/$localization->language.json"));
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddHiddenColumnToPlansTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->boolean('hidden')->default(false)->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('billing_plans', function (Blueprint $table) {
$table->dropColumn('hidden');
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddVerifiedAtColumnToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( !Schema::hasColumn('users', 'email_verified_at')) {
Schema::table('users', function (Blueprint $table) {
$table->timestamp('email_verified_at')->nullable();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('email_verified_at');
});
}
}

View File

@@ -0,0 +1,31 @@
<?php
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
class MoveConfirmedColumnToEmailVerifiedAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('users', 'confirmed')) {
User::where('confirmed', true)
->update(['email_verified_at' => Carbon::now()]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
class FixIssuesWithMigrationToLaravel7 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
try {
collect(File::allFiles(resource_path('views/vendor')))
->filter(function(SplFileInfo $file) {
return Str::endsWith($file->getPathname(), 'blade.php') &&
!Str::endsWith($file->getPathname(), 'html/message.blade.php') &&
!Str::endsWith($file->getPathname(), 'email.blade.php');
})->each(function(SplFileInfo $file) {
File::delete($file->getPathname());
});
} catch (Exception $e) {
//
}
try {
File::delete(base_path('vendor/symfony/translation/TranslatorInterface.php'));
} catch (Exception $e) {
//
}
try {
$setting = DB::table('settings')->where('name', 'player.enable_landing')->first();
if ($setting && (bool) $setting->value) {
DB::table('settings')->where('name', 'homepage.type')->orWhere('name', 'homepage.value')->delete();
DB::table('settings')->insert([
['name' => 'homepage.type', 'value' => 'component'],
['name' => 'homepage.value', 'value' => 'Landing Page'],
]);
}
} catch (Exception $e) {
//
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWorkspacesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('workspaces')) {
Schema::create('workspaces', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('owner_id')->unsigned()->index();
$table->timestamp('created_at')->index()->nullable();
$table->timestamp('updated_at')->index()->nullable();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('workspaces');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWorkspaceInvitesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('workspace_invites')) {
Schema::create('workspace_invites', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('avatar', 80)->nullable();
$table->integer('workspace_id')->unsigned()->index();
$table->integer('user_id')->unsigned()->index()->nullable();
$table->string('email', 80)->index();
$table->integer('role_id')->unsigned()->index();
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('workspace_invites');
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWorkspaceUserTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( ! Schema::hasTable('workspace_user')) {
Schema::create('workspace_user', function (Blueprint $table) {
$table->id();
$table->integer('user_id')->unsigned()->index();
$table->integer('workspace_id')->unsigned()->index();
$table->integer('role_id')->unsigned()->index()->nullable();
$table->boolean('is_owner')->index()->default(false);
$table->timestamps();
$table->unique(['workspace_id', 'user_id']);
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('workspace_user');
}
}

Some files were not shown because too many files have changed in this diff Show More