Skip to content

Commit

Permalink
feat: release first version of mergeSelectOrIncludeClauses function
Browse files Browse the repository at this point in the history
  • Loading branch information
LilaRest committed May 4, 2023
1 parent 2358e83 commit c395845
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile

- name: Release new versions
env:
Expand Down
10 changes: 5 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ generator client {
provider = "prisma-client-js"
}

generator prismary {
provider = "prismary"
}
// generator prismary {
// provider = "prismary"
// }

datasource db {
provider = "postgresql"
Expand All @@ -15,10 +15,10 @@ model User {
id Int @id @default(autoincrement())
email String @unique
name String?
bases Base[]
bases Baseiooo[]
}

model Base {
model Baseiooo {
user User @relation(fields: [userId], references: [id])
userId Int @id
}
7 changes: 5 additions & 2 deletions src/clauses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type SetToUnion } from "./utils";
import { type SetToUnion } from "./utils/other";
import { type Action } from "./types";

/**
Expand Down Expand Up @@ -142,7 +142,10 @@ export const notSupportedClauses = new Set([
"findUniqueOrThrow", "findFirstOrThrow", "aggregate", "groupBy",

// Other
"rejectOnNotFound"
"rejectOnNotFound",

// Raw database access
// "queryRaw", "executeRaw", "findRaw", "runCommandRaw"
] as const);
export type NotSupportedClause = SetToUnion<typeof notSupportedClauses>;

Expand Down
2 changes: 1 addition & 1 deletion src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getPrismaryConfig } from "./config";
import { promises as fs } from "fs";
import { Variants } from "./variants";
import { Project, VariableDeclarationKind } from "ts-morph";
import { writeArray, getTemplate, buildVariationName, getZodSchemaFromField } from "./utils";
import { writeArray, getTemplate, buildVariationName, getZodSchemaFromField } from "./utils/other";


generatorHandler({
Expand Down
25 changes: 25 additions & 0 deletions src/utils/deep-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Taken from https://gist.github.com/LukeChannings/15c92cef5a016a8b21a0

// ensure the keys being passed is an array of key paths
// example: 'a.b' becomes ['a', 'b'] unless it was already ['a', 'b']
const keys = (ks: string | string[]) => (Array.isArray(ks) ? ks : ks.split('.'));

// traverse the set of keys left to right,
// returning the current value in each iteration.
// if at any point the value for the current key does not exist,
// return the default value
export const deepGet = (o: any, kp: string[]) => keys(kp).reduce((o, k) => o && o[k], o);

// traverse the set of keys right to left,
// returning a new object containing both properties from the object
// we were originally passed and our new property.
//
// Example:
// If o = { a: { b: { c: 1 } } }
//
// deepSet(o, ['a', 'b', 'c'], 2) will progress thus:
// 1. c = Object.assign({}, {c: 1}, { c: 2 })
// 2. b = Object.assign({}, { b: { c: 1 } }, { b: c })
// 3. returned = Object.assign({}, { a: { b: { c: 1 } } }, { a: b })
export const deepSet = (o: any, kp: string | string[], v: any) =>
keys(kp).reduceRight((v, k, i, ks) => Object.assign({}, deepGet(o, ks.slice(0, i)), { [k]: v }), v);
263 changes: 263 additions & 0 deletions src/utils/merge-select-or-include-clauses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**
* @packageDocumentation
*
* This utils file handle merging Prisma Client `include` and `select` clauses.
*
* ## Standard
*
* The below code aims to strictly respect the following specifications of the Prisma Client API reference:
* - `include` clause: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#include
* - `select` clause: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#select
*
* Additional documentation on the usage of those clauses can be found here:
* - https://www.prisma.io/docs/concepts/components/prisma-client/select-fields
*
* Based on those resources, here is the valid format of a "select or include" clause:
*
* ```text
* ["select"|"include"]: {
* data: true | {
* include?: {...}
* select?: {...}
* }
* }
* ```
*
* Where {...} is just another nested object of this type.
*
*
* ## Performances & validation
*
* Note that to keep those pieces of code as performant as possible, only the minimum necessary validations
* are performed at runtime on the given values.
* TS is enforcing type safety at write and compile time only.
*
*
* ## Terminology
*
* - **query body**: object given as argument of Prisma Client model method
* @example
* ```ts
* prisma.user.findMany(x)
* ```
* where x is a "query body" object
*
* - **sub-body**: object given as value of a scalar field keys in `include` or `select` clause
* @example
* ```ts
* prisma.user.findMany({ select: { posts: x } })
* ```
* where x is a "sub body"
*/


/**
* 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;
}

/**
* 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
* @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;
}

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] = {};

// 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!;

for (const key in selectBody) {

// Ignore top-level scalar fields of `select` body, they are already all included by the include clause
if (selectBody[key] !== 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) {

// 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);
}

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 some non-scalar fields have been found, append the sub-select body
if (Object.keys(nonScalarFields).length) receiver[mergedClauseType]![key] = nonScalarFields;

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

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

// Else if both clauses have 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] };
}
}





console.log(JSON.stringify(mergeSelectOrIncludeClauses(
{
type: "include",
include: {
posts: true
}
},
{
type: "select",
select: {
posts: {
select: {
title: true,
comments: {
select: {
content: true
}
},
likes: true
}
}
}
}
), null, 2));

console.log(JSON.stringify(mergeSelectOrIncludeClauses(
{
type: "select",
select: {
email: true
}
},
{
type: "select",
select: {
name: true
}
}
), null, 2));

console.log(JSON.stringify(mergeSelectOrIncludeClauses(
{
type: "include",
include: {
posts: true
}
},
{
type: "select",
select: {
posts: {
select: {
title: true,
comments: {
select: {
content: true
}
},
likes: true
}
}
}
}
), null, 2));
/**
* Should give:
{
include: {
posts: {
}
}
}
*
*/
Loading

0 comments on commit c395845

Please sign in to comment.