prisma-util
Version:
Prisma Util is an easy to use tool that merges multiple Prisma schema files, allows extending of models, resolves naming conflicts both manually and automatically and provides easy access to Prisma commands and timing reports. It's mostly a plug-and-play
387 lines (386 loc) • 15.9 kB
JavaScript
import { convertPathToLocal, normalizePath } from "../../utils.js";
import * as fs from 'fs/promises';
import { log } from '../../logger.js';
import chalk from "chalk";
/**
* Safe null for strings.
*/
const SAFE_NULL_STRING = "<NULL>";
/**
* Ignore search step.
*/
export const IGNORE_SEARCH_STEP = "<IGNORE_SEARCH>";
/**
* Check if a string is valid.
* @param checked The string to check.
* @returns Whether the checked string is valid or not.
*/
function validString(checked) {
return !(!checked) && checked != SAFE_NULL_STRING;
}
/**
* Project Toolchain default assets to be used in the Generation Toolchain.
*
* This map includes all runtime declarations of `@prisma/client/runtime`:
*
* PRISMA_CLIENT_RUNTIME: {
* EDGE: "node_modules/@prisma/client/runtime/edge.js",
* EDGE_ESM: "node_modules/@prisma/client/runtime/edge-esm.js",
* INDEX: "node_modules/@prisma/client/runtime/index.js",
* }
*
* This map includes all generated declarations of `.prisma/client`:
*
* PRISMA_CLIENT_GENERATED: {
* EDGE: "node_modules/.prisma/client/edge.js",
* INDEX: "node_modules/.prisma/client/index.js",
* INDEX_TYPES: "node_modules/.prisma/client/index.d.ts",
* }
*/
const DEFAULT_ASSETS = {
PRISMA_CLIENT_RUNTIME: {
EDGE: "node_modules/@prisma/client/runtime/edge.js",
EDGE_ESM: "node_modules/@prisma/client/runtime/edge-esm.js",
INDEX: "node_modules/@prisma/client/runtime/index.js",
},
PRISMA_CLIENT_GENERATED: {
EDGE: "node_modules/.prisma/client/edge.js",
INDEX: "node_modules/.prisma/client/index.js",
INDEX_TYPES: "node_modules/.prisma/client/index.d.ts",
}
};
/**
* Escape a search query.
* @param string The string to escape.
* @returns Escaped string to use inside of regex.
*/
function escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
/**
* The strategy that the Generation Toolchain should use for an {@link EditTransaction}.
*/
export var EditStrategy;
(function (EditStrategy) {
EditStrategy[EditStrategy["REGEX"] = 0] = "REGEX";
EditStrategy[EditStrategy["JOIN"] = 1] = "JOIN";
EditStrategy[EditStrategy["REPLACE"] = 2] = "REPLACE";
EditStrategy[EditStrategy["REPLACE_UNSAFE"] = 3] = "REPLACE_UNSAFE";
EditStrategy[EditStrategy["REPLACE_FULL"] = 4] = "REPLACE_FULL";
EditStrategy[EditStrategy["NONE"] = 5] = "NONE";
})(EditStrategy || (EditStrategy = {}));
/**
* Project Toolchain backend implementation.
*
* This class orchestrates code generation and editing and provides an unified API for using the generated code.
* It provides utilities as well as state handling between edits to make sure that the correct code is written.
*
* This API is intended for internal use only. You should not instantiate this class, but rather use the exported
* instance. ({@link GenerationToolchainInstance})
*/
class GenerationToolchain {
constructor() {
this._assets = DEFAULT_ASSETS;
this.queue = [];
this.processPromise = undefined;
this._useExtensions = false;
}
/**
* Use extensions instead of code edits.
* @param useExtensions Whether extensions should be used or not.
* @returns This instance for chaining.
*/
useExtensions(useExtensions) {
this._useExtensions = useExtensions;
return this;
}
/**
* Returns the current repository of assets.
*/
get ASSETS() {
return this._assets;
}
/**
* This function allows you to add assets that you can use later when generating.
* @param assets The assets that should be added to the repository.
*/
addAssets(assets) {
this._assets = {
...assets,
...this._assets
};
}
/**
* Create a transaction to modify internal assets from PrismaClient.
* @returns An {@link EditTransaction} that you can use to modify internal assets.
*/
createEditTransaction() {
return new EditTransaction(this);
}
/**
* Add an edit transaction to the processing queue. Transactions are processed sequentially and can't
* be created while the Generation Toolchain is processing.
* @param transaction The transaction that should be queued.
* @returns True if the transaction has been added to the queue, false otherwise.
*/
queueEditTransaction(transaction) {
if (!this.processPromise)
this.queue.push(transaction);
return !this.processPromise;
}
/**
* Start processing the transaction queue.
* @returns A Promise that will resolve when processing finishes.
*/
process() {
if (this.processPromise)
return this.processPromise;
log("Prisma Util Toolchain is starting to process the transaction queue...", "\n");
this.processPromise = new Promise(async (resolve) => {
const transactionRepository = {};
const processedTransactions = [];
const processedBlocksForTransactions = {};
const useTransactionRepository = async (requestedKey) => {
if (!transactionRepository[requestedKey])
transactionRepository[requestedKey] = await fs.readFile(requestedKey, "utf8");
return transactionRepository[requestedKey];
};
const updateTransactionRepository = (assetPath, text) => {
transactionRepository[assetPath] = text;
};
while (this.queue.length > 0) {
const transaction = this.queue.pop();
if (!transaction)
continue;
const requestedAsset = transaction === null || transaction === void 0 ? void 0 : transaction.changedAsset;
if (!validString(requestedAsset))
continue;
const assetPath = convertPathToLocal(requestedAsset);
const blocks = this._useExtensions ? transaction.blocks.filter(block => block.ignoreExtensions) : transaction.blocks;
let processedCount = 0;
for (const block of blocks) {
const { from, to, ammend, strategy, search } = block;
let snapshot = "";
let text = "";
if (EditStrategy.REPLACE_FULL != strategy && typeof from != "string" && typeof to != "string") {
snapshot = await useTransactionRepository(assetPath);
text = snapshot;
text = text.replace(from, (match, ...g) => {
return to(match, ...g);
});
}
else {
if (EditStrategy.REPLACE_FULL != strategy) {
if (!validString(typeof from == "string" ? from : SAFE_NULL_STRING) || !validString(typeof to == "string" ? to : SAFE_NULL_STRING) || strategy == EditStrategy.NONE)
continue;
}
snapshot = await useTransactionRepository(assetPath);
if (EditStrategy.REPLACE_FULL != strategy) {
if (new RegExp(escapeRegex(typeof to == "string" ? to : SAFE_NULL_STRING), "gms").test(snapshot))
continue;
}
const regex = new RegExp(escapeRegex(typeof from == "string" ? from : SAFE_NULL_STRING), "gms");
if (EditStrategy.REPLACE_FULL != strategy) {
if (!regex.test(snapshot))
continue;
}
text = snapshot;
switch (strategy) {
case EditStrategy.REGEX:
const lines = text.split("\n");
const item = lines.filter(line => regex.test(line))[0];
if (!item)
continue;
let index = lines.indexOf(item);
if (index == -1)
continue;
index = index + ammend;
text = `${lines.slice(0, index).join("\n")}\n${to}\n${lines.slice(index).join("\n")}`;
break;
case EditStrategy.JOIN:
text = text.split(regex).join(`${to}${from}`);
break;
case EditStrategy.REPLACE:
text = text.replace(regex, `${from}${to}`);
break;
case EditStrategy.REPLACE_FULL:
text = typeof to == "function" ? `${to(text)}${text}` : text;
break;
}
}
updateTransactionRepository(assetPath, text);
processedCount++;
}
if (processedCount > 0)
processedTransactions.push(assetPath);
processedBlocksForTransactions[assetPath] = processedBlocksForTransactions[assetPath] ? processedBlocksForTransactions[assetPath] + processedCount : processedCount;
}
for (const [file, content] of Object.entries(transactionRepository)) {
await fs.writeFile(file, content);
}
const frequencies = {};
for (const transaction of processedTransactions) {
frequencies[transaction] = frequencies[transaction] ? frequencies[transaction] + 1 : 1;
}
const blockCount = Object.values(processedBlocksForTransactions).reduce((partialSum, a) => partialSum + a, 0);
log(processedTransactions.length > 0 ? `Prisma Util Toolchain has processed the following transactions: \n${[...new Set(processedTransactions)].map(key => `- ${normalizePath(key)} ${chalk.white(chalk.bold(`(${chalk.blue(frequencies[key])} ${frequencies[key] == 1 ? "transaction" : "transactions"}, ${chalk.blue(processedBlocksForTransactions[key])} ${processedBlocksForTransactions[key] == 1 ? "block" : "blocks"})`))}`).join("\n")}\nTOTAL: ${chalk.white(`${chalk.blue(processedTransactions.length)} ${processedTransactions.length == 1 ? "transaction" : "transactions"}, ${chalk.blue(blockCount)} ${blockCount == 1 ? "block" : "blocks"}`)}` : "Prisma Util Toolchain couldn't find any differences, so it didn't process any transactions.");
resolve();
});
return this.processPromise;
}
}
/**
* Edit Transaction is an interface that allows you to edit a PrismaClient internal asset without worrying
* about index shifting or file searching.
*
* To create an EditTransaction, use the {@link GenerationToolchain.createEditTransaction} function and chain the
* function calls to this class, then use {@link EditTransaction.end} when you're done.
*/
export class EditTransaction {
constructor(generationToolchain) {
this.requestedAsset = SAFE_NULL_STRING;
this.transactionBlocks = [];
this.generationToolchain = generationToolchain;
}
/**
* Returns the path to the requested asset of this transaction.
*/
get changedAsset() {
return this.requestedAsset;
}
/**
* Returns the changes for this transaction.
*/
get blocks() {
return this.transactionBlocks;
}
/**
* Mark this transaction as finished. This function will add the transaction to the queue for edits
* and will be processed sequentially.
* @returns The {@link GenerationToolchain} instance that was used for this transaction.
*/
end() {
this.generationToolchain.queueEditTransaction(this);
return this.generationToolchain;
}
/**
* Change the asset being edited in this transaction.
* @param assetName The asset that you want to edit.
* @returns This transaction for chaining.
*/
requestAsset(assetName) {
this.requestedAsset = assetName;
return this;
}
/**
* Add a transaction block to this edit transaction.
*
* This method isn't supposed to be called manually.
*
* Method Flags: @Internal @NoManual
* @param transactionBlock The transaction block to add.
*/
pushTransactionBlock(transactionBlock) {
this.transactionBlocks.push(transactionBlock);
}
/**
* Create a new change for this transaction.
* @returns A new transaction block.
*/
createBlock() {
return new EditTransactionBlock(this);
}
}
/**
* A transaction block handles a change in a transaction.
*/
export class EditTransactionBlock {
constructor(editTransaction) {
this.strategy = EditStrategy.NONE;
this.from = SAFE_NULL_STRING;
this.to = SAFE_NULL_STRING;
this.search = SAFE_NULL_STRING;
this.ammend = 0;
this.ignoreExtensions = false;
this.editTransaction = editTransaction;
}
/**
* Change the edit strategy for this block.
* @param strategy The new strategy to use.
* @returns This transaction block for chaining.
*/
setStrategy(strategy) {
this.strategy = strategy;
return this;
}
/**
* Disable this block based on extension status.
* @param ignoreExtensions Whether this block should be ran even if extensions are enabked.
* @returns This transaction block for chaining.
*/
setIgnoreExtensions(ignoreExtensions) {
this.ignoreExtensions = ignoreExtensions;
return this;
}
/**
* Change a line from this asset.
* @param from The line to search for.
* @param modifier The value that will be added to the index.
* @returns This transaction block for chaining.
*/
findLine(from, modifier = 0) {
this.from = from;
this.ammend = modifier;
return this;
}
/**
* Append content to the file.
* @param to The content to add.
* @returns This transaction block for chaining.
*/
appendContent(to) {
this.to = to;
return this;
}
/**
* Add search query to be used with {@link EditStrategy.REPLACE_UNSAFE}.
* @param search The search query that will be used for security.
* @returns This transaction block for chaining.
*/
setSearch(search) {
this.search = search;
return this;
}
/**
* Create a new change for this transaction.
* @returns A new transaction block.
*/
createBlock() {
this.editTransaction.pushTransactionBlock(this);
return this.editTransaction.createBlock();
}
/**
* Mark this transaction as finished. This function will add the transaction to the queue for edits
* and will be processed sequentially.
* @returns The {@link GenerationToolchain} instance that was used for this transaction.
*/
end() {
this.editTransaction.pushTransactionBlock(this);
return this.editTransaction.end();
}
/**
* Mark this transaction block as finished.
* @returns The {@link EditTransaction} that this block belongs to.
*/
endBlock() {
this.editTransaction.pushTransactionBlock(this);
return this.editTransaction;
}
}
/**
* Instance of the Project Toolchain backend implementation.
*
* This is the entry-point to all code generation and PrismaClient edits, as it provides an unified interface
* for making changes and creating comments.
*/
export const GenerationToolchainInstance = new GenerationToolchain();