Last active
September 10, 2023 14:57
-
-
Save YPetremann/efa0b449cf1400f37a6c34e29b3618c0 to your computer and use it in GitHub Desktop.
Prisma Auto Exclude
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { Prisma } = require("@prisma/client"); | |
/** | |
* this plugin is used to automatically exclude fields from prisma queries | |
* this works with almost all prisma queries that use select and include | |
* if a custom query is used, it will not work by default, | |
* but it can by adding a $autoExclude:true to the args | |
* next to select and include, there is now exclude | |
* if a field ends with an underscore, it will be excluded unless explicitly in select or include | |
* truthy field in exclude will be excluded, even if present in select or include | |
* work on deep relational tree | |
* as a result, it will merge include in select if they are both present | |
* | |
* @example | |
* const { PrismaClient } = require("@prisma/client"); | |
* const excluder = require("./prisma-auto-excluder.js"); | |
* const prisma = new PrismaClient({ errorFormat: "pretty" }).$extends(excluder); | |
* prisma.user.findMany({ | |
* select: { id: true, email_: true } | |
* exclude: { password_: true } | |
* }).then(console.log); | |
* @todo type safe version of this | |
* @todo some tests for this | |
* @todo some docs for this | |
*/ | |
const excluder = { | |
name: "prisma-auto-excluder", | |
query: { | |
$allOperations({ model, operation, query, args }) { | |
excludeForOperation(model, operation, (args = structuredClone(args))); | |
return query(args); | |
}, | |
}, | |
}; | |
const models = parseModels(); | |
function parseModels() { | |
const isRelation = ({ relationName }) => relationName; | |
const isNotRelation = ({ relationName }) => !relationName; | |
const isPrivate = ({ name }) => name.endsWith("_"); | |
const isNotPrivate = ({ name }) => !name.endsWith("_"); | |
const entryRelation = ({ name, type }) => [name, type]; | |
const entryBool = ({ name }) => [name, true]; | |
return Object.fromEntries( | |
Prisma.dmmf.datamodel.models.map(({ name, fields }) => [ | |
name, | |
{ | |
select: Object.fromEntries( | |
fields.filter(isNotRelation).filter(isNotPrivate).map(entryBool), | |
), | |
exclude: fields.filter(isNotRelation).filter(isPrivate).length > 0, | |
include: Object.fromEntries(fields.filter(isRelation).map(entryRelation)), | |
}, | |
]), | |
); | |
} | |
function excludeToSelect(query, model) { | |
const dSel = structuredClone(models[model].select); | |
const hdExc = models[model].exclude; | |
const hSel = "select" in query; | |
const hInc = "include" in query && Object.keys(query.include).length > 0; | |
const hExc = "exclude" in query && Object.keys(query.exclude).length > 0; | |
// Prisma-auto-excluder performs the following: | |
// 1. if exclude is true from the model, | |
// and select is not specified in the query, | |
// then select is set to the default select | |
if (hdExc || hExc) query.select ??= dSel; | |
// 2. if include and (select or exclude) is specified in the query, | |
// then include is merged into select then removed from the query | |
if (hInc && (hdExc || hExc || hSel)) { | |
query.select ??= dSel; | |
Object.assign(query.select, query.include); | |
delete query.include; | |
} | |
// 3. if exclude is specified in the query, | |
// then exclude is merged into select then removed from the query | |
if (hExc) { | |
for (const key in query.exclude) if (key in query.select) delete query.select[key]; | |
delete query.exclude; | |
} | |
// 4. for each of include and select objects, if a key is a relation, | |
// (if the value is a boolean, then it is replaced with an empty object) | |
// and excludeToSelect is called on the value | |
const sel = query.select ?? query.include; | |
for (const key in sel) { | |
const child = models[model].include[key]; | |
if (!child) continue; | |
sel[key] ??= {}; | |
if (typeof sel[key] === "boolean" && models[child].exclude) sel[key] = {}; | |
if (typeof sel[key] === "object") excludeToSelect(sel[key], child); | |
} | |
} | |
function excludeForOperation(model, operation, args) { | |
switch (operation) { | |
case "findUnique": | |
case "findUniqueOrThrow": | |
case "findFirst": | |
case "findFirstOrThrow": | |
case "findMany": | |
case "create": | |
case "update": | |
case "upsert": | |
case "delete": | |
excludeToSelect(args, model); | |
break; | |
default: | |
if (args.$autoExclude) { | |
delete args.$autoExclude; | |
excludeToSelect(args, model); | |
} | |
} | |
} | |
module.exports = excluder; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment