options = new QueryOptions(); } public function filter(array $filterArguments): Query { $this->filter = new Filter($filterArguments); return $this; } public function run(): Graph { $resultsRecords = $this->mainQuery(); $ids = array_map(function ($rec) { return $rec->id; }, $resultsRecords); $resultChildrenEdgesTargetIds = []; $resultChildrenEdges = []; if ($this->options->childrenDepth > 0 && !empty($ids)) { $resultChildrenEdges = $this->getChildren($ids); $resultChildrenEdgesTargetIds = array_map(fn($e) => $e->target, $resultChildrenEdges); } $resultParentSourceTargetIds = []; $resultParentEdges = []; if ($this->options->parentsDepth > 0 && !empty($ids)) { $resultParentEdges = $this->getParents($ids); $resultParentSourceTargetIds = array_map(fn($e) => $e->source, $resultParentEdges); } $edgesIds = collect($resultParentSourceTargetIds)->merge($resultChildrenEdgesTargetIds)->unique()->values()->toArray(); $edgeRecords = []; if (!empty($edgesIds)) { $edgeRecords = DB::table('records') ->whereIn("id", $edgesIds) ->whereIn("_sys->status", $this->options->status) ->get()->toArray(); } $resultsRecordsUnique = collect(array_merge($resultsRecords, $edgeRecords))->unique("id")->values()->toArray(); $resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges)) ->unique(fn($edge) => $edge->source . $edge->target . $edge->field) ->toArray(); return $this->formatRecords($resultsRecordsUnique, $resultEdges); } private function formatRecords(array $records, array $edges): Graph { $queryRecords = collect($records)->map(function ($recordData) { $record = Record::fromDB($recordData); $record->data = $this->inputFormatter->fill($record->_sys->schema, $record->data); $queryRecord = QueryRecord::fromRecord($record); $queryRecord->isRoot = data_get($recordData, "isRoot") === true; return $queryRecord; })->toArray(); $queryEdges = collect($edges)->map(function ($edgeData) { return Edge::fromArray((array)$edgeData); })->sortBy("rank")->values()->toArray(); return new Graph( new Collection($queryRecords), new Collection($queryEdges), ); } public function tree(): Collection { return $this->run()->tree(); } /** * @throws SubqueryNoResultException */ private function parseFilters(Builder $query): Builder { $filters = $this->filter->run(new Query($this->channelService, $this->inputFormatter)); $ignoredFilters = []; foreach ($filters as $filter) { if (in_array($filter["field"], $ignoredFilters)) { continue; } else if ($filter["operator"] == "in") { $query->whereIn($filter["field"], $filter["value"]); } else if ($filter["operator"] == "nin") { $query->whereNotIn($filter["field"], $filter["value"]); } elseif ($filter["operator"] == "eqobject") { $object = $filter["value"]; // unset related filters used here $addToIgnored = collect($filters) ->filter(fn($f) => str_starts_with($f["field"], $object)) ->values() ->map(fn($f) => $f["field"]) ->toArray(); $ignoredFilters = array_merge($ignoredFilters, $addToIgnored); $objectFilters = collect($filters) ->filter(fn($f) => str_starts_with($f["field"], $object)) ->values() ->reduce(function ($c, $f) use ($object) { $field = str_replace($object . "->", "", $f["field"]); $c[$field] = $f["value"]; return $c; }); // target result // filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24 // $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]); // $query->whereJsonContains($filter["field"], [$objectFilters]); } else { $query->where($filter["field"], $filter["operator"], $filter["value"]); } } $query->whereIn("_sys->status", $this->options->status); return $query; } /** * @throws SubqueryNoResultException */ private function mainQuery(): array { $query = DB::table("records"); $query = $this->parseFilters($query); $query->limit($this->options->limit); $query->offset($this->options->skip); $query = $this->orderByQuery($query); return $query->get()->map(function ($r) { $r->isRoot = true; return $r; })->toArray(); } private function getChildren(array $ids): array { $subquery = DB::table('edges AS g') ->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 0 as depth ')) ->whereIn('source', $ids) ->limit($this->options->childrenLimit) ->unionAll( DB::table(DB::raw("edges AS g, search_graph AS sg ")) ->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth') ->whereRaw("g.source = sg.target") ->where("sg.depth", "<=", $this->options->childrenDepth) ->orderBy("rank") ); return DB::table('search_graph') // ->select(DB::raw("*, 1 as depth ")) ->withRecursiveExpression('search_graph', $subquery) ->get()->toArray(); } private function getParents(array $ids): array { $subquery = DB::table('edges AS g') ->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 0 as depth ')) ->limit($this->options->parentsLimit) ->whereIn('g.target', $ids) ->unionAll( DB::table(DB::raw("edges AS g, search_graph AS sg ")) ->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth') ->whereRaw("g.target = sg.source") ->where("sg.depth", "<=", $this->options->parentsDepth) ->orderBy("rank") ); return DB::table('search_graph') // ->select(DB::raw('sg.source,sg.target,sg.rank,sg."sourceSchema",sg."targetSchema",sg.field,sg.depth')) ->withRecursiveExpression('search_graph', $subquery) ->get()->toArray(); } /** * @throws SubqueryNoResultException */ public function runWithCount(): Graph { $query = DB::table("records"); $query = $this->parseFilters($query); $graph = $this->run(); $graph->total = $query->count(); return $graph; } public function limit(int $limit): Query { $this->options->limit = $limit; return $this; } public function skip(int $skip): Query { $this->options->skip = $skip; return $this; } public function childrenDepth(int $depth): Query { $this->options->childrenDepth = $depth; return $this; } public function childrenLimit(int $limit): Query { $this->options->childrenLimit = $limit; return $this; } public function parentsDepth(int $depth): Query { $this->options->parentsDepth = $depth; return $this; } public function parentsLimit(int $limit): Query { $this->options->parentsLimit = $limit; return $this; } public function sort(string $sort): Query { $this->options->sort[] = $sort; return $this; } public function status(array $status): Query { $this->options->status = $status; return $this; } public function orderByQuery(Builder $query): Builder { foreach ($this->options->sort as $item) { $field = str_replace(".", "->", ltrim($item, '-')); $dir = str_starts_with($item, '-') ? "desc" : "asc"; if ($field) { $query->orderBy($field, $dir); } } return $query; } }