Skip to content

Commit

Permalink
feat: improved version of mergeSelectOrIncludeClauses() + start writi…
Browse files Browse the repository at this point in the history
…ng tests for it
  • Loading branch information
LilaRest committed May 4, 2023
1 parent c395845 commit 40fcfd4
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 159 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"scripts": {
"build": "tsc && bash ./scripts/add-templates-to-dist.bash",
"test": "jest",
"semantic-release": "semantic-release"
"semantic-release": "semantic-release",
"docs:dev": "typedoc src/"
},
"dependencies": {
"@casl/ability": "^6.4.0",
Expand Down
251 changes: 98 additions & 153 deletions src/utils/merge-select-or-include-clauses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,209 +55,154 @@
* Represents a {@link queryBody} object that only supports `include` and `select` clauses.
* Is also used to represnent sub-body nested in `include` and `select clauses.
*/
interface QueryBody {
select?: SelectOrIncludeBody;
include?: SelectOrIncludeBody;
[key: string | "select" | "include"]: SelectOrIncludeBody | string | undefined;
interface SelectQueryBody {
select: SelectOrIncludeBody;
include?: never;
[key: string]: SelectOrIncludeBody | string | undefined;
}

interface IncludeQueryBody {
select?: never;
include: SelectOrIncludeBody;
[key: string]: SelectOrIncludeBody | string | undefined;
}

type QueryBody = SelectQueryBody | IncludeQueryBody;

/**
* Represents the body of a `select` or `include` clause.
*/
interface SelectOrIncludeBody {
[key: string]: true | QueryBody;
}

/**
* Represents the clause format awaited by the below functions
*/
interface SelectOrIncludeClause extends QueryBody {
type: "include" | "select";
};

/**
* Represents the partial query body object passed between nested calls of
* `_mergeSelectOrIncludeClauses()` function.
*/
type Receiver = Partial<QueryBody>;

/**
* This function merges two given `include` or `select` clauses.
* Useful for bundling many read requests in a single one.
* @param clause1
* @param clause2
* @param body1
* @param body2
* @returns
*/
export function mergeSelectOrIncludeClauses (clause1: SelectOrIncludeClause, clause2: SelectOrIncludeClause) {
const startTime = performance.now();
const mergedClauseBody: Receiver = {};
_mergeSelectOrIncludeClauses(clause1, clause2, mergedClauseBody);
console.log("In: ", performance.now() - startTime, "ms");
return mergedClauseBody;
export function mergeSelectOrIncludeClauses (body1: QueryBody, body2: QueryBody): QueryBody {
// const startTime = performance.now();
const mergedBody = _mergeSelectOrIncludeClauses(body1, body2);
// console.log("In: ", performance.now() - startTime, "ms");
return mergedBody;
}

function _mergeSelectOrIncludeClauses (clause1: SelectOrIncludeClause, clause2: SelectOrIncludeClause, receiver: Receiver) {

// Retrieve the main type of the merged clause
const mergedClauseType = [clause1.type, clause2.type].includes("include") ? "include" : "select";
receiver[mergedClauseType] = {};
function _mergeSelectOrIncludeClauses (body1: QueryBody, body2: QueryBody): QueryBody {

const mergedBody: Partial<QueryBody> = {};

// Retrieve given clauses types
const type1 = body1.include ? "include" : "select";
const type2 = body2.include ? "include" : "select";

// Retrieve the type of the merged clause and provides an empty body
const typeMerged = body1.include || body2.include ? "include" : "select";
mergedBody[typeMerged] = {};

// If there is one `select` and one `include` clause
if (clause1.type !== clause2.type) {
const selectBody: SelectOrIncludeBody = clause1.type === "select" ? clause1.select! : clause2.select!;
const includeBody: SelectOrIncludeBody = clause1.type === "include" ? clause1.include! : clause2.include!;
// If there is one `select` and one `include` clause to be merged
if (type1 !== type2) {

for (const key in selectBody) {
// Identify the `select` and `include` clause bodies
const [selectBody, includeBody] = type1 === "select"
? [body1.select!, body2.include!]
: [body2.select!, body1.include!];

// Ignore top-level scalar fields of `select` body, they are already all included by the include clause
if (selectBody[key] !== true) {
// Iterate over entries of `select` clause body
for (const [key, value] of Object.entries(selectBody)) {

// Ignore top-level scalar fields of `select` body, as they are already implicitely included by the include clause
if (value !== true) {

// If key is also included in the `include` clause body
if (key in includeBody) {

// And if it nest some sub-body
if (includeBody[key] !== true) {
// And if it also contains a sub-body
const includedBodyValue = includeBody[key];
if (includedBodyValue !== true) {

// Merge the two sub-bodies together
receiver[mergedClauseType]![key] = {};
_mergeSelectOrIncludeClauses(
{
type: "select",
select: selectBody[key] as SelectOrIncludeBody
},
{
type: "include",
include: includeBody[key] as SelectOrIncludeBody
},
receiver[mergedClauseType]![key] as Receiver);
mergedBody[typeMerged]![key] = _mergeSelectOrIncludeClauses(value, includedBodyValue);
}

// Else, if the `select` clause key holds detailed relationships selectors reflect those in the
// new `include`
else {
// Retrieve all non-scalar fiels of the `select` body
const nonScalarFields: any = {};
for (const [k, v] of Object.entries(selectBody[key] as SelectOrIncludeBody)) {
if (v !== true) nonScalarFields[k] = v;
if (value.include) {
mergedBody[typeMerged]![key] = {
include: value.include
};
}
// If some non-scalar fields have been found, append the sub-select body
if (Object.keys(nonScalarFields).length) receiver[mergedClauseType]![key] = nonScalarFields;
else if (value.select) {

// Retrieve all non-scalar fiels of the sub `select` body
const nonScalarFields: any = {};
for (const [k, v] of Object.entries(value.select)) {
if (v !== true) nonScalarFields[k] = v;
}

// If some non-scalar fields have been found, append the sub-select body
if (Object.keys(nonScalarFields).length) mergedBody[typeMerged]![key] = {
select: nonScalarFields
};

// Else simply set key to true to mean "include all"
else receiver[mergedClauseType]![key] = true;
// Else simply set key to true to mean "include all"
else mergedBody[typeMerged]![key] = true;
}
}
delete includeBody[key];
}

// Else
}
}
receiver[mergedClauseType] = { ...receiver[mergedClauseType], ...includeBody };
mergedBody[typeMerged] = { ...mergedBody[typeMerged], ...includeBody };
}

// Else if both clauses have same type
// Or if both clauses have the same type
else {
const type = clause1.type;
for (const key in clause1[type]) {
if (key in clause2[type]!) {
if (clause1[type]![key] === true) {
receiver[mergedClauseType]![key] = true;
delete clause2[type]![key];
}
else {
receiver[mergedClauseType]![key] = {};
_mergeSelectOrIncludeClauses(
{
type: type,
[type]: clause1[type]![key] as SelectOrIncludeBody
},
{
type: type,
[type]: clause2[type]![key] as SelectOrIncludeBody
},
receiver[mergedClauseType]![key] as Receiver);
}
}
else receiver[mergedClauseType]![key] = clause1[type]![key];
}
receiver[mergedClauseType] = { ...receiver[mergedClauseType], ...clause2[type] };
}
}

// Retrieve common type and iterate over body1 keys
const type = type1;
for (const key in body1[type]) {

// If key is also in body2
if (key in body2[type]!) {

// If key value is true in body1
if (body1[type]![key] === true) {
// And also true in body 2, set it to true
if (body2[type]![key] === true) mergedBody[typeMerged]![key] = true;
// Else, use the body2 value
else mergedBody[typeMerged]![key] = body2[type]![key];
}

// Or if key value is true in body2 but not in body1
else if (body2[type]![key] === true) {

console.log(JSON.stringify(mergeSelectOrIncludeClauses(
{
type: "include",
include: {
posts: true
}
},
{
type: "select",
select: {
posts: {
select: {
title: true,
comments: {
select: {
content: true
}
},
likes: true
// Use body1 value
mergedBody[typeMerged]![key] = body2[type]![key];
}
}
}
}
), null, 2));

console.log(JSON.stringify(mergeSelectOrIncludeClauses(
{
type: "select",
select: {
email: true
}
},
{
type: "select",
select: {
name: true
}
}
), null, 2));
// Else if they are both sub-bodies
else {

console.log(JSON.stringify(mergeSelectOrIncludeClauses(
{
type: "include",
include: {
posts: true
}
},
{
type: "select",
select: {
posts: {
select: {
title: true,
comments: {
select: {
content: true
}
},
likes: true
// Merge the sub-bodies together
mergedBody[typeMerged]![key] = _mergeSelectOrIncludeClauses(
body1[type]![key] as QueryBody,
body2[type]![key] as QueryBody
);
}
delete body2[type]![key];
}
// Else simply append it to mergedBody;
else mergedBody[typeMerged]![key] = body1[type]![key];
}
mergedBody[typeMerged] = { ...mergedBody[typeMerged], ...body2[type] };
}
), null, 2));
/**
* Should give:
{
include: {
posts: {
}
}


return mergedBody as QueryBody;
}
*
*/
6 changes: 3 additions & 3 deletions tests/clauses.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, test, it } from '@jest/globals';
import { describe, expect, it } from '@jest/globals';
import { clauses, parentClauses, extendClauses, ignoredClauses, notSupportedClauses } from "../src/clauses";

describe("Clauses", () => {
describe("clauses.ts file", () => {
const implementedClauses = new Set([...parentClauses, ...extendClauses, ...ignoredClauses, ...notSupportedClauses]);
it("should be all implemented", () => {
it("should implement all clauses", () => {
expect(implementedClauses).toBe(clauses);
});
});
Loading

0 comments on commit 40fcfd4

Please sign in to comment.