UNPKG

prismaql

Version:

A powerful tool for managing and editing Prisma schema files using a SQL-like DSL.

299 lines (294 loc) 11 kB
import chalk from "chalk"; const DSL_PATTERN = /^([A-Z]+)(?:\s+([A-Z_]+))?(?:\s+([\w\s,*]+))?(?:\s*\(\{([\s\S]*?)\}\))?(?:\s*\(([^)]*?)\))?$/i; const ACTION_COMMAND_MAP = { GET: ["MODELS", "DB", "GENERATORS", "MODEL", "ENUM_RELATIONS", "FIELDS", "RELATIONS", "ENUMS", "MODELS_LIST"], ADD: ["MODEL", "GENERATOR", "FIELD", "RELATION", "ENUM"], DELETE: ["MODEL", "FIELD", "RELATION", "ENUM", "GENERATOR"], UPDATE: ["FIELD", "ENUM", "GENERATOR", "DB"], PRINT: [], VALIDATE: [], }; export class PrismaQlDslParser { argsProcessors; customCommands = {}; actionTypeMap = { GET: "query", ADD: "mutation", DELETE: "mutation", UPDATE: "mutation", PRINT: "query", VALIDATE: "query", }; constructor(argsProcessors) { this.argsProcessors = argsProcessors; } registerCommand(action, command, type) { if (!this.customCommands[action]) { this.customCommands[action] = []; } this.customCommands[action].push(command); this.actionTypeMap[action] = type; } getCommands() { return { ...ACTION_COMMAND_MAP, ...this.customCommands, }; } parseCommand(input) { const trimmed = input.trim(); if (!trimmed.endsWith(";")) { const errorMessage = ` ${chalk.red("Syntax Error: Missing semicolon (;) at the end of the command.")} ${chalk.yellow("Each DSL command must end with a semicolon.")} For example: ${chalk.green("GET MODELS;")} ${chalk.green("DELETE MODEL Product;")} Please check your input and try again. `; console.error(errorMessage); throw new Error("DSL command must end with a semicolon."); } const raw = trimmed.slice(0, -1).trim(); const match = raw.match(DSL_PATTERN); if (!match) { throw new Error(`Unable to parse DSL line: "${raw}"`); } const actionStr = match[1].toUpperCase(); const commandStr = match[2]?.toUpperCase(); const argsStr = match[3]?.trim() || undefined; let prismaBlockStr = match[4]?.trim() || undefined; if (prismaBlockStr) { prismaBlockStr = prismaBlockStr.replace(/'/g, '"'); prismaBlockStr = prismaBlockStr.replace(/\\n/g, "\n"); prismaBlockStr = prismaBlockStr.replace(/\|/g, "\n"); } let optionsStr = match[5]?.trim() || undefined; if (optionsStr) { optionsStr = optionsStr.replace(/'/g, '"'); optionsStr = optionsStr.replace(/\\n/g, "\n"); optionsStr = optionsStr.replace(/\|/g, "\n"); } if (!(actionStr in ACTION_COMMAND_MAP) && !(actionStr in this.customCommands)) { throw new Error(`Unsupported action "${actionStr}". Supported actions: ${Object.keys(ACTION_COMMAND_MAP).join(", ")}`); } let finalCommand; const actionKey = actionStr; const commands = this.getCommands(); const availableCommands = commands[actionKey] || []; if (commandStr) { if (!availableCommands.includes(commandStr)) { throw new Error(`Invalid command "${commandStr}" for action "${actionStr}". Supported: ${availableCommands.join(", ")}`); } finalCommand = commandStr; } const parsedOptions = optionsStr ? this.parseParams(optionsStr) : undefined; const baseArgs = this.parseArgs(argsStr); const argsProcessor = this.argsProcessors[actionStr][finalCommand || "default"]; const finalArgs = argsProcessor ? argsProcessor(baseArgs, argsStr) : baseArgs; return { action: actionStr, command: finalCommand, args: finalArgs, options: parsedOptions, prismaBlock: prismaBlockStr, raw: input, type: this.actionTypeMap[actionStr], }; } parseParams(input) { const result = {}; const tokens = input.split(",").map(t => t.trim()).filter(Boolean); for (const token of tokens) { const eqIndex = token.indexOf("="); if (eqIndex > 0) { const key = token.slice(0, eqIndex).trim(); let valueStr = token.slice(eqIndex + 1).trim(); if (/^\d+$/.test(valueStr)) { result[key] = parseInt(valueStr, 10); } if (valueStr === "true") { result[key] = true; } else if (valueStr === "false") { result[key] = false; } else { result[key] = valueStr; } if (valueStr.includes(",")) { result[key] = valueStr.split(",").map(v => v.trim()); } else { try { result[key] = JSON.parse(valueStr); } catch (e) { result[key] = valueStr; } } } else { const flag = token.trim(); result[flag] = true; } } return result; } parseArgs(argsStr) { const args = {}; if (!argsStr) return args; const tokens = argsStr.split(",").map(t => t.trim()).filter(Boolean); for (const token of tokens) { args.models = args.models || []; args.models.push(token); } return args; } detectActionType(source) { const DSL_ACTION_PATTERN = /^([A-Z]+)/i; const match = source.match(DSL_ACTION_PATTERN); if (!match) return null; const actionStr = match[1].toUpperCase(); return this.actionTypeMap[actionStr] || null; } isValid(source) { try { this.parseCommand(source); return true; } catch (e) { return e; } } } export const basePrismaQlAgsProcessor = { GET: { default: (parsedArgs) => parsedArgs, MODEL: (parsedArgs, rawArgs) => { if (rawArgs?.includes("IN")) { return { models: [rawArgs.split("IN")[1].trim()] }; } return parsedArgs; }, MODELS: (_, rawArgs) => { return { models: rawArgs ? rawArgs.split(",").map(m => m.trim()) : [] }; }, RELATIONS: (_, rawArgs) => { return { models: rawArgs ? rawArgs.split(",").map(r => r.trim()) : [] }; }, FIELDS: (parsedArgs, rawArgs) => { const [fieldsStr, modelName] = rawArgs?.split("IN") || []; if (!fieldsStr || !modelName) return parsedArgs; return { models: [modelName.trim()], fields: fieldsStr.split(",").map(f => f.trim()) }; }, ENUMS: (_, rawArgs) => { return { enums: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; }, ENUM_RELATIONS: (_, rawArgs) => { return { enums: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; } }, ADD: { default: (parsedArgs) => parsedArgs, MODEL: (_, rawArgs) => { return { models: rawArgs ? rawArgs.split(",").map(m => m.trim()) : [] }; }, GENERATOR: (parsedArgs, rawArgs) => { return { generators: rawArgs ? rawArgs.split(",").map(g => g.trim()) : [] }; }, ENUM: (_, rawArgs) => { return { enums: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; }, FIELD: (parsedArgs, rawArgs) => { const [fieldName, modelName] = rawArgs?.split("TO") || []; if (!fieldName || !modelName) return parsedArgs; return { models: [modelName.trim()], fields: [fieldName.trim()] }; }, RELATION: (parsedArgs, rawArgs) => { const [fromModel, toModel] = rawArgs?.split("AND") || []; if (!fromModel || !toModel) return parsedArgs; return { models: [fromModel.trim(), toModel.trim()] }; } }, DELETE: { default: (parsedArgs) => parsedArgs, MODEL: (_, rawArgs) => { return { models: rawArgs ? rawArgs.split(",").map(m => m.trim()) : [] }; }, ENUM: (_, rawArgs) => { return { enums: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; }, FIELD: (parsedArgs, rawArgs) => { const [fieldName, modelName] = rawArgs?.split("IN") || []; if (!fieldName || !modelName) return parsedArgs; return { models: [modelName.trim()], fields: [fieldName.trim()] }; }, RELATION: (_, rawArgs) => { return { models: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; }, GENERATOR: (_, rawArgs) => { return { generators: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; } }, UPDATE: { default: (parsedArgs) => parsedArgs, FIELD: (parsedArgs, rawArgs) => { const [fieldName, modelName, prismaBlock] = rawArgs?.split("IN") || []; if (!fieldName || !modelName) return parsedArgs; return { models: [modelName.trim()], fields: [fieldName .trim()], prismaBlock: prismaBlock?.trim() }; }, ENUM: (_, rawArgs) => { return { enums: rawArgs ? rawArgs.split(",").map(e => e.trim()) : [] }; }, GENERATOR: (parsedArgs, rawArgs) => { return { generators: rawArgs ? rawArgs.split(",").map(g => g.trim()) : [] }; } }, PRINT: { default: (parsedArgs) => parsedArgs, }, VALIDATE: { default: (parsedArgs) => parsedArgs, }, }; export const prismaQlParser = new PrismaQlDslParser(basePrismaQlAgsProcessor); /** * Example of extending the base parser with custom actions and commands * type CustomAction = BasePrismaQlDSLAction | "SAY"; type CustomCommand = BasePrismaQLDSLCommand | "HI"; type CustomParserArgsProcessors = Record< CustomAction, { default: PrismaQlDSLArgsProcessor<any, any>; } & Partial<Record<CustomCommand, PrismaQlDSLArgsProcessor<any, any>>> >; const customArgsProcessors: CustomParserArgsProcessors = { ...basePrismaQlAgsProcessor, SAY: { default: (parsedArgs) => parsedArgs, HI: (parsedArgs, rawArgs) => { return { models: rawArgs ? rawArgs.split(",").map(m => m.trim()) : [], }; }, }, }; export const customParser = new PrismaQlDslParser<CustomAction, CustomCommand>( customArgsProcessors ); customParser.registerCommand("SAY", "HI", "query"); console.log('test', customParser.parseCommand('SAY HI model1, model2;')); */ //# sourceMappingURL=dsl.js.map