Skip to content

feat: options for logging queries sent to prisma #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/runtime/src/enhancements/omit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ import { DefaultPrismaProxyHandler, makeProxy } from './proxy';
import { ModelMeta } from './types';
import { enumerate, getModelFields } from './utils';

/**
* Options for @see withOmit
*/
export type WithOmitOptions = {
/**
* Model metatadata
*/
modelMeta?: ModelMeta;
};

/**
* Gets an enhanced Prisma client that supports @omit attribute.
*/
export function withOmit<DbClient extends object>(prisma: DbClient, modelMeta?: ModelMeta): DbClient {
const _modelMeta = modelMeta ?? getDefaultModelMeta();
export function withOmit<DbClient extends object>(prisma: DbClient, options?: WithOmitOptions): DbClient {
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta();
return makeProxy(
prisma,
_modelMeta,
Expand Down
14 changes: 12 additions & 2 deletions packages/runtime/src/enhancements/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ import { NestedWriteVisitor } from './nested-write-vistor';
import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy';
import { ModelMeta } from './types';

/**
* Options for @see withPassword
*/
export type WithPasswordOptions = {
/**
* Model metatadata
*/
modelMeta?: ModelMeta;
};

/**
* Gets an enhanced Prisma client that supports @password attribute.
*/
export function withPassword<DbClient extends object = any>(prisma: DbClient, modelMeta?: ModelMeta): DbClient {
const _modelMeta = modelMeta ?? getDefaultModelMeta();
export function withPassword<DbClient extends object = any>(prisma: DbClient, options?: WithPasswordOptions): DbClient {
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta();
return makeProxy(
prisma,
_modelMeta,
Expand Down
71 changes: 52 additions & 19 deletions packages/runtime/src/enhancements/policy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CrudFailureReason } from '../../constants';
import { AuthUser, DbClientContract, PolicyOperationKind } from '../../types';
import { BatchResult, PrismaProxyHandler } from '../proxy';
import { ModelMeta, PolicyDef } from '../types';
import { prismaClientValidationError } from '../utils';
import { formatObject, prismaClientValidationError } from '../utils';
import { Logger } from './logger';
import { PolicyUtil } from './policy-utils';

Expand All @@ -20,10 +20,11 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
private readonly policy: PolicyDef,
private readonly modelMeta: ModelMeta,
private readonly model: string,
private readonly user?: AuthUser
private readonly user?: AuthUser,
private readonly logPrismaQuery?: boolean
) {
this.logger = new Logger(prisma);
this.utils = new PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user);
this.utils = new PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user, this.logPrismaQuery);
}

private get modelClient() {
Expand Down Expand Up @@ -107,9 +108,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// use a transaction to wrap the write so it can be reverted if the created
// entity fails access policies
const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) =>
dbOps.create(writeArgs)
);
const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => {
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`create\`: ${formatObject(writeArgs)}`);
}
return dbOps.create(writeArgs);
});

const ids = this.utils.getEntityIds(this.model, result);
if (Object.keys(ids).length === 0) {
Expand All @@ -133,9 +137,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// use a transaction to wrap the write so it can be reverted if any created
// entity fails access policies
const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) =>
dbOps.createMany(writeArgs, skipDuplicates)
);
const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => {
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`createMany\`: ${formatObject(writeArgs)}`);
}
return dbOps.createMany(writeArgs, skipDuplicates);
});

return result as BatchResult;
}
Expand All @@ -158,9 +165,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// use a transaction to wrap the write so it can be reverted if any nested
// create fails access policies
const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) =>
dbOps.update(writeArgs)
);
const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => {
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`update\`: ${formatObject(writeArgs)}`);
}
return dbOps.update(writeArgs);
});

const ids = this.utils.getEntityIds(this.model, result);
if (Object.keys(ids).length === 0) {
Expand All @@ -183,9 +193,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// use a transaction to wrap the write so it can be reverted if any nested
// create fails access policies
const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) =>
dbOps.updateMany(writeArgs)
);
const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => {
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`updateMany\`: ${formatObject(writeArgs)}`);
}
return dbOps.updateMany(writeArgs);
});

return result as BatchResult;
}
Expand All @@ -212,9 +225,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// use a transaction to wrap the write so it can be reverted if any nested
// create fails access policies
const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) =>
dbOps.upsert(writeArgs)
);
const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => {
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`upsert\`: ${formatObject(writeArgs)}`);
}
return dbOps.upsert(writeArgs);
});

const ids = this.utils.getEntityIds(this.model, result);
if (Object.keys(ids).length === 0) {
Expand Down Expand Up @@ -248,6 +264,9 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
}

// conduct the deletion
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`delete\`:\n${formatObject(args)}`);
}
await this.modelClient.delete(args);

if (!readResult) {
Expand All @@ -270,6 +289,9 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
await this.utils.injectAuthGuard(args, this.model, 'delete');

// conduct the deletion
if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`deleteMany\`:\n${formatObject(args)}`);
}
return this.modelClient.deleteMany(args);
}

Expand All @@ -282,6 +304,10 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// inject policy conditions
await this.utils.injectAuthGuard(args, this.model, 'read');

if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`aggregate\`:\n${formatObject(args)}`);
}
return this.modelClient.aggregate(args);
}

Expand All @@ -295,6 +321,9 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
// inject policy conditions
await this.utils.injectAuthGuard(args, this.model, 'read');

if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`groupBy\`:\n${formatObject(args)}`);
}
return this.modelClient.groupBy(args);
}

Expand All @@ -304,6 +333,10 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
// inject policy conditions
args = args ?? {};
await this.utils.injectAuthGuard(args, this.model, 'read');

if (this.logPrismaQuery && this.logger.enabled('info')) {
this.logger.info(`[withPolicy] \`count\`:\n${formatObject(args)}`);
}
return this.modelClient.count(args);
}

Expand All @@ -323,7 +356,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
const readArgs = { select: origArgs.select, include: origArgs.include, where: ids };
const result = await this.utils.readWithCheck(this.model, readArgs);
if (result.length === 0) {
this.logger.warn(`${action} result cannot be read back`);
this.logger.info(`${action} result cannot be read back`);
throw this.utils.deniedByPolicy(
this.model,
operation,
Expand Down
36 changes: 31 additions & 5 deletions packages/runtime/src/enhancements/policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ export type WithPolicyContext = {
user?: AuthUser;
};

/**
* Options for @see withPolicy
*/
export type WithPolicyOptions = {
/**
* Policy definition
*/
policy?: PolicyDef;

/**
* Model metatadata
*/
modelMeta?: ModelMeta;

/**
* Whether to log Prisma query
*/
logPrismaQuery?: boolean;
};

/**
* Gets an enhanced Prisma client with access policy check.
*
Expand All @@ -24,16 +44,22 @@ export type WithPolicyContext = {
export function withPolicy<DbClient extends object>(
prisma: DbClient,
context?: WithPolicyContext,
policy?: PolicyDef,
modelMeta?: ModelMeta
options?: WithPolicyOptions
): DbClient {
const _policy = policy ?? getDefaultPolicy();
const _modelMeta = modelMeta ?? getDefaultModelMeta();
const _policy = options?.policy ?? getDefaultPolicy();
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta();
return makeProxy(
prisma,
_modelMeta,
(_prisma, model) =>
new PolicyProxyHandler(_prisma as DbClientContract, _policy, _modelMeta, model, context?.user),
new PolicyProxyHandler(
_prisma as DbClientContract,
_policy,
_modelMeta,
model,
context?.user,
options?.logPrismaQuery
),
'policy'
);
}
Expand Down
20 changes: 17 additions & 3 deletions packages/runtime/src/enhancements/policy/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ import { EventEmitter } from 'stream';
* A logger that uses an existing Prisma client to emit.
*/
export class Logger {
constructor(private readonly prisma: any) {}
private emitter: EventEmitter | undefined;
private eventNames: Array<string | symbol> = [];

private get emitter() {
constructor(private readonly prisma: any) {
const engine = (this.prisma as any).getEngine();
return engine ? (engine.logEmitter as EventEmitter) : undefined;
this.emitter = engine ? (engine.logEmitter as EventEmitter) : undefined;
if (this.emitter) {
this.eventNames = this.emitter.eventNames();
}
}

/**
* Checks if a log level is enabled.
*/
public enabled(level: 'info' | 'warn' | 'error') {
return !!this.eventNames.includes(level);
}

/**
* Generates a message with the given level.
*/
public log(level: 'info' | 'warn' | 'error', message: string) {
this.emitter?.emit(level, {
timestamp: new Date(),
Expand Down
Loading