@godspeedsystems/plugins-prisma-as-datastore
Version:
Prisma as a datasource plugin for Godspeed Framework.
303 lines • 14.2 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_CONFIG = exports.CONFIG_FILE_NAME = exports.Type = exports.SourceType = exports.DataSource = void 0;
const core_1 = require("@godspeedsystems/core");
const prisma_deterministic_search_field_encryption_1 = require("@godspeedsystems/prisma-deterministic-search-field-encryption");
const buffer_1 = require("buffer");
const crypto_1 = __importDefault(require("crypto"));
const os_1 = __importDefault(require("os"));
// CHANGE 0: In Prisma Client v6.8.2, PrismaClient is not directly exported from the main @prisma/client package.
// Instead, it's generated and available after running npx prisma generate.
const iv = buffer_1.Buffer.alloc(16);
const platform = os_1.default.platform();
const response_codes = {
find: 200,
findFirst: 200,
findUnique: 200,
findMany: 200,
create: 201,
createMany: 201,
update: 204,
updateMany: 204,
upsert: 201,
delete: 202,
deleteMany: 202,
count: 200,
aggregate: 200,
groupBy: 200,
};
class DataSource extends core_1.GSDataSource {
constructor() {
super(...arguments);
this.secret = this.config.prisma_secret || 'prismaEncryptionSecret';
this.password_hash = crypto_1.default
.createHash('md5')
.update(this.secret, 'utf-8')
.digest('hex')
.toUpperCase();
}
initClient() {
return __awaiter(this, void 0, void 0, function* () {
try {
// TODO: until we figure out, how to share path between prisma file and our module loader
// we are supporting only one prisma db
// const module = await import(`../../../node_modules/.prisma/${this.config.name}`);
// const prisma = new module.PrismaClient();
const client = yield this.loadPrismaClient();
return client;
}
catch (error) {
console.error('Could not load prisma client', this.config.name);
console.error(error);
process.exit(1);
}
});
}
loadPrismaClient() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e;
const pathString = platform === 'win32' ? `${process.cwd()}\\dist\\datasources\\prisma-clients\\${this.config.name}` : `${process.cwd()}/dist/datasources/prisma-clients/${this.config.name}`;
const { Prisma, PrismaClient } = require(pathString);
const prisma = new PrismaClient();
try {
yield prisma.$connect();
// Try to connect by performing an operation that requires a connection
let result;
// CHANGE 1: Updated provider check to use new property name
if (((_a = prisma._engineConfig) === null || _a === void 0 ? void 0 : _a.activeProvider) !== "mongodb") {
result = yield prisma.$queryRaw `SELECT 1`;
}
else {
result = yield prisma.$runCommandRaw({ ping: 1 });
}
}
catch (error) {
throw error;
}
// CHANGE 2: Updated middleware usage for Prisma v6
prisma.$use((0, prisma_deterministic_search_field_encryption_1.fieldEncryptionMiddleware)({
encryptFn: (decrypted) => this.cipher(decrypted),
decryptFn: (encrypted) => this.decipher(encrypted),
// CHANGE 3: Access DMMF through the new structure
dmmf: Prisma.dmmf || prisma._dmmf,
}));
// CHANGE 4: Updated models access for Prisma v6
prisma.models = ((_c = (_b = Prisma.dmmf) === null || _b === void 0 ? void 0 : _b.datamodel) === null || _c === void 0 ? void 0 : _c.models) || ((_e = (_d = prisma._dmmf) === null || _d === void 0 ? void 0 : _d.datamodel) === null || _e === void 0 ? void 0 : _e.models);
return prisma;
});
}
cipher(decrypted) {
const cipher = crypto_1.default.createCipheriv('aes-256-gcm', this.password_hash, iv);
return cipher.update(decrypted, 'utf-8', 'hex');
}
decipher(encrypted) {
const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', this.password_hash, iv);
return decipher.update(encrypted, 'hex', 'utf-8');
}
execute(ctx, args) {
return __awaiter(this, void 0, void 0, function* () {
const { logger } = ctx;
const _a = args, { meta: { entityType, method, fnNameInWorkflow, authzPerms } } = _a, rest = __rest(_a, ["meta"]);
if (authzPerms) {
const authzFailRes = modifyForAuthz(this.client, rest, authzPerms, entityType, method);
if (authzFailRes) {
return authzFailRes;
}
}
// Now authz checks are set in select fields and passed in where clause
let prismaMethod;
try {
const client = this.client;
// @ts-ignore
if (entityType && !client[entityType]) {
logger.error('Invalid entityType %s in %s', entityType, fnNameInWorkflow);
return new core_1.GSStatus(false, 400, undefined, { error: `Invalid entityType ${entityType} in ${fnNameInWorkflow}` });
}
// @ts-ignore
prismaMethod = client[entityType][method];
if (method && !prismaMethod) {
logger.error('Invalid CRUD method %s in %s', method, fnNameInWorkflow);
return new core_1.GSStatus(false, 500, undefined, { error: 'Internal Server Error' });
}
// @ts-ignore
let prismaResponse = yield prismaMethod.bind(client)(rest);
if (Object.keys(prismaResponse).length > 0 && typeof prismaResponse === 'object' && !Array.isArray(prismaResponse)) {
let finalResult = {};
for (const [key, value] of Object.entries(prismaResponse)) {
if (typeof value === 'bigint') {
finalResult[key] = Number(value);
}
else {
finalResult[key] = value;
}
prismaResponse = Object.assign({}, finalResult);
}
}
return new core_1.GSStatus(true, responseCode(method), undefined, prismaResponse);
}
catch (error) {
logger.error('Error in executing Prisma query for args %o \n Error: %o', args, error);
return new core_1.GSStatus(false, 400, error.message, JSON.stringify(error.message));
}
});
}
}
exports.DataSource = DataSource;
function modifyForAuthz(client, args, authzPerms, entityType, method) {
// Find the model for this entityType
const model = client === null || client === void 0 ? void 0 : client.models.find((m) => m.name === entityType);
//Find the fields of this model
const fields = model.fields.map((f) => f.name);
//Fix select fields based on the authzPerms and model fields
const isSelectFine = fixSelect(args, authzPerms, fields);
if (!isSelectFine.success) {
return new core_1.GSStatus(true, responseCode(method), undefined, defaultResponse(method));
}
if (args.where) {
//Make sure where clause in the args does not query any fields which are not allowed
const isWhereFine = assertWhereIsFine(args.where, authzPerms, method);
if (!isWhereFine.success) {
isWhereFine.success = true;
//Just silently return empty response to the user
return isWhereFine;
}
if (authzPerms === null || authzPerms === void 0 ? void 0 : authzPerms.where) {
// Merge where clause from authzPerms to query's where clause
// Intentionally doing this after checking args.where first for allowed access,
// which limits the API caller based on authz rules.
// But allow authzPerms.where clause to not be limited by authz.can_access/no_access limits
args.where = Object.assign({}, args === null || args === void 0 ? void 0 : args.where, authzPerms === null || authzPerms === void 0 ? void 0 : authzPerms.where);
if (Object.keys(args.where).length === 0) {
args.where = undefined;
}
}
}
else if (authzPerms === null || authzPerms === void 0 ? void 0 : authzPerms.where) {
// The args.where is empty. So just assign that to authzPerms.where
args.where = authzPerms.where;
}
}
/**
* Remove any not allowed columns from the select clause
* @param query Prisma query args
* @param authzPerms authz.data.select
*/
function fixSelect(query, authzPerms, allFields) {
let querySelect = query.select;
let finalSelect;
if (!querySelect) { //Input query does not have select clause. Prisma by default returns all fields
if (authzPerms.can_access) {
finalSelect = authzPerms.can_access;
}
else if (authzPerms.no_access) {
finalSelect = allFields.filter((f) => { var _a; return !((_a = authzPerms.no_access) === null || _a === void 0 ? void 0 : _a.includes(f)); });
}
else {
//No need to set query.select Let it be undefined
return new core_1.GSStatus(true);
}
}
else {
if (authzPerms.can_access) {
const accessFields = authzPerms.can_access;
finalSelect = Object.keys(query.select).filter((f) => query.select[f] && accessFields.includes(f));
}
else if (authzPerms.no_access) {
const noAccessFields = authzPerms.no_access;
finalSelect = Object.keys(query.select).filter((f) => query.select[f] && !noAccessFields.includes(f));
}
else {
//No need to touch query.select. Let it be as it is
return new core_1.GSStatus(true);
}
}
// Atleast one of can_access or no_access exists
//@ts-ignore
if (finalSelect === null || finalSelect === void 0 ? void 0 : finalSelect.length) {
query.select = finalSelect.reduce((acc, f) => {
acc[f] = true;
return acc;
}, {});
}
else if (query.select) {
return new core_1.GSStatus(false);
}
return new core_1.GSStatus(true);
}
function assertWhereIsFine(queryWhere, authzPerms, method) {
//Handle for Array
if (Array.isArray(queryWhere)) {
for (let o of queryWhere) {
const res = assertWhereIsFine(o, authzPerms, method);
if (!res.success) {
return res;
}
}
// If all clauses are fine. Just return from this function with success.
return { success: true };
}
//Handle for object
for (let key of Object.keys(queryWhere)) {
if (key === 'OR' || key === 'AND') {
// Go inside nested queries and check
const res = assertWhereIsFine(queryWhere[key], authzPerms, method);
if (!res.success) {
return res;
}
}
else if (!isFieldAllowed(key, authzPerms)) {
core_1.logger.warn('Tried to access not allowed column in where clause. Not executing query and silently returning empty response. Field %s. Where clause: %o', key, queryWhere);
return new core_1.GSStatus(false, responseCode(method), undefined, defaultResponse(method));
}
}
return { success: true };
}
function isFieldAllowed(key, authzPerms) {
if (authzPerms === null || authzPerms === void 0 ? void 0 : authzPerms.can_access) {
return authzPerms.can_access.includes(key);
}
else if (authzPerms === null || authzPerms === void 0 ? void 0 : authzPerms.no_access) {
return !authzPerms.no_access.includes(key);
}
else { // Since no authz perms are there, the field is allowed
return true;
}
}
function responseCode(method) {
return response_codes[method] || 200;
}
function defaultResponse(method) {
return method === 'findMany' ? [] : {};
}
const SourceType = 'DS';
exports.SourceType = SourceType;
const Type = 'prisma'; // this is the loader file of the plugin, So the final loader file will be `types/${Type.js}`
exports.Type = Type;
const CONFIG_FILE_NAME = 'prisma'; // in case of event source, this also works as event identifier, and in case of datasource works as datasource name
exports.CONFIG_FILE_NAME = CONFIG_FILE_NAME;
const DEFAULT_CONFIG = {};
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
//# sourceMappingURL=index.js.map