UNPKG

@godspeedsystems/plugins-prisma-as-datastore

Version:

Prisma as a datasource plugin for Godspeed Framework.

303 lines 14.2 kB
"use strict"; 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