Skip to content

Instantly share code, notes, and snippets.

@YPetremann
Last active September 10, 2023 14:57
Show Gist options
  • Save YPetremann/efa0b449cf1400f37a6c34e29b3618c0 to your computer and use it in GitHub Desktop.
Save YPetremann/efa0b449cf1400f37a6c34e29b3618c0 to your computer and use it in GitHub Desktop.
Prisma Auto Exclude
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