rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
219 lines • 14.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostgreJsonQueryBuilder = void 0;
const Clause_1 = require("../models/Clause");
const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery");
const ValueComponent_1 = require("../models/ValueComponent");
const SelectValueCollector_1 = require("./SelectValueCollector");
const PostgresObjectEntityCteBuilder_1 = require("./PostgresObjectEntityCteBuilder");
const PostgresArrayEntityCteBuilder_1 = require("./PostgresArrayEntityCteBuilder");
/**
* PostgreSQL JSON query builder that transforms SimpleSelectQuery into queries
* that return JSON arrays or single JSON objects using PostgreSQL JSON functions.
*/
class PostgreJsonQueryBuilder {
constructor() {
this.selectValueCollector = new SelectValueCollector_1.SelectValueCollector(null);
this.objectEntityCteBuilder = new PostgresObjectEntityCteBuilder_1.PostgresObjectEntityCteBuilder();
this.arrayEntityCteBuilder = new PostgresArrayEntityCteBuilder_1.PostgresArrayEntityCteBuilder();
}
/**
* Validates the JSON mapping and the original query.
* @param query Original query to transform
* @param mapping JSON mapping configuration
*/
validateMapping(query, mapping) {
var _a, _b;
const collector = new SelectValueCollector_1.SelectValueCollector();
const selectedValues = collector.collect(query);
// sv.name is the alias or derived name
const availableColumns = new Set(selectedValues.map(sv => sv.name));
// Check root entity columns
for (const jsonKey in mapping.rootEntity.columns) {
const sourceColumn = mapping.rootEntity.columns[jsonKey];
if (!availableColumns.has(sourceColumn)) {
throw new Error(`Validation Error: Column "${sourceColumn}" for JSON key "${jsonKey}" in root entity "${mapping.rootEntity.name}" not found in the query's select list.`);
}
}
// Check nested entity columns and parent-child relationships
const entityIds = new Set([mapping.rootEntity.id]);
const parentToChildrenMap = new Map();
mapping.nestedEntities.forEach(ne => {
entityIds.add(ne.id);
if (!parentToChildrenMap.has(ne.parentId)) {
parentToChildrenMap.set(ne.parentId, []);
}
parentToChildrenMap.get(ne.parentId).push(ne.id);
});
for (const entity of mapping.nestedEntities) {
if (!entityIds.has(entity.parentId)) {
throw new Error(`Validation Error: Parent entity with ID "${entity.parentId}" for nested entity "${entity.name}" (ID: ${entity.id}) not found.`);
}
for (const jsonKey in entity.columns) {
const sourceColumn = entity.columns[jsonKey];
if (!availableColumns.has(sourceColumn)) {
throw new Error(`Validation Error: Column "${sourceColumn}" for JSON key "${jsonKey}" in nested entity "${entity.name}" (ID: ${entity.id}) not found in the query's select list.`);
}
}
}
// Validate: An entity should not have multiple direct array children.
// Validate: Child propertyNames under a single parent must be unique.
const allParentIds = new Set([mapping.rootEntity.id, ...mapping.nestedEntities.map(ne => ne.parentId)]);
for (const parentId of allParentIds) {
const directChildren = mapping.nestedEntities.filter(ne => ne.parentId === parentId);
const directArrayChildrenCount = directChildren.filter(c => c.relationshipType === 'array').length;
if (directArrayChildrenCount > 1) {
const parentName = parentId === mapping.rootEntity.id ? mapping.rootEntity.name : (_a = mapping.nestedEntities.find(ne => ne.id === parentId)) === null || _a === void 0 ? void 0 : _a.name;
throw new Error(`Validation Error: Parent entity "${parentName}" (ID: ${parentId}) has multiple direct array children. This is not supported.`);
}
const propertyNames = new Set();
for (const child of directChildren) {
if (propertyNames.has(child.propertyName)) {
const parentName = parentId === mapping.rootEntity.id ? mapping.rootEntity.name : (_b = mapping.nestedEntities.find(ne => ne.id === parentId)) === null || _b === void 0 ? void 0 : _b.name;
throw new Error(`Validation Error: Parent entity "${parentName}" (ID: ${parentId}) has duplicate property name "${child.propertyName}" for its children.`);
}
propertyNames.add(child.propertyName);
}
}
}
/**
* Build JSON query from original query and mapping configuration.
* @param originalQuery Original query to transform
* @param mapping JSON mapping configuration
* @returns Transformed query with JSON aggregation
*/
buildJson(originalQuery, mapping) {
return this.buildJsonWithCteStrategy(originalQuery, mapping);
}
/**
* Builds the JSON structure using a unified CTE-based strategy.
* @param originalQuery Original query
* @param mapping JSON mapping configuration
* @returns Query with CTE-based JSON aggregation
*/
buildJsonWithCteStrategy(originalQuery, mapping) {
this.validateMapping(originalQuery, mapping);
// Step 1: Create the initial CTE from the original query
const { initialCte, initialCteAlias } = this.createInitialCte(originalQuery);
let ctesForProcessing = [initialCte];
let currentAliasToBuildUpon = initialCteAlias;
// Step 2: Prepare entity information
const allEntities = new Map();
allEntities.set(mapping.rootEntity.id, { ...mapping.rootEntity, isRoot: true, propertyName: mapping.rootName });
mapping.nestedEntities.forEach(ne => allEntities.set(ne.id, { ...ne, isRoot: false, propertyName: ne.propertyName })); // Step 2.5: Build CTEs for object entities using dedicated builder
const objectEntityResult = this.objectEntityCteBuilder.buildObjectEntityCtes(initialCte, allEntities, mapping);
// Important: Replace the entire CTE list with the result from object entity builder
// The object entity builder returns all CTEs including the initial one
ctesForProcessing = objectEntityResult.ctes;
currentAliasToBuildUpon = objectEntityResult.lastCteAlias;
// Step 3: Build CTEs for array entities using dedicated builder
const arrayCteBuildResult = this.arrayEntityCteBuilder.buildArrayEntityCtes(ctesForProcessing, currentAliasToBuildUpon, allEntities, mapping);
ctesForProcessing = arrayCteBuildResult.updatedCtes;
currentAliasToBuildUpon = arrayCteBuildResult.lastCteAlias;
// Step 4: Build the final SELECT query using all generated CTEs
return this.buildFinalSelectQuery(ctesForProcessing, currentAliasToBuildUpon, allEntities, mapping);
}
/**
* Creates the initial Common Table Expression (CTE) from the original query.
* @param originalQuery The base SimpleSelectQuery.
* @returns An object containing the initial CTE and its alias.
*/
createInitialCte(originalQuery) {
const originCteAlias = "origin_query";
const originCte = new Clause_1.CommonTable(originalQuery, new Clause_1.SourceAliasExpression(originCteAlias, null), null);
return { initialCte: originCte, initialCteAlias: originCteAlias };
}
/**
* Builds the final SELECT query that constructs the root JSON object (or array of objects).
* This query uses all previously generated CTEs.
* @param finalCtesList The complete list of all CTEs (initial and array CTEs).
* @param lastCteAliasForFromClause Alias of the final CTE from which the root object will be built.
* @param allEntities Map of all processable entities.
* @param mapping JSON mapping configuration.
* @returns The final SimpleSelectQuery.
*/
buildFinalSelectQuery(finalCtesList, lastCteAliasForFromClause, allEntities, mapping) {
const currentCtes = [...finalCtesList];
// Define rootObjectCteAlias outside of if block
const rootObjectCteAlias = `cte_root_${mapping.rootName.toLowerCase().replace(/[^a-z0-9_]/g, '_')}`;
const rootEntity = allEntities.get(mapping.rootEntity.id);
if (!rootEntity) {
throw new Error(`Root entity ${mapping.rootEntity.id} not found`);
}
if (mapping.resultFormat === "array" || !mapping.resultFormat) {
// Step 4.1a: Create a CTE that wraps the final result as the root object
// No alias needed for single table SELECT
const rootObjectBuilderExpression = this.buildEntityJsonObject(rootEntity, null, // No source alias for single table
mapping.nestedEntities, allEntities, mapping.useJsonb);
const rootObjectSelectItem = new Clause_1.SelectItem(rootObjectBuilderExpression, mapping.rootName);
const rootObjectCte = new Clause_1.CommonTable(new SimpleSelectQuery_1.SimpleSelectQuery({
selectClause: new Clause_1.SelectClause([rootObjectSelectItem]),
fromClause: new Clause_1.FromClause(new Clause_1.SourceExpression(new Clause_1.TableSource(null, new ValueComponent_1.IdentifierString(lastCteAliasForFromClause)), null // No alias
), null),
}), new Clause_1.SourceAliasExpression(rootObjectCteAlias, null), null);
currentCtes.push(rootObjectCte);
// Step 4.1b: Aggregate all the root objects
const aggregationFunc = mapping.useJsonb ? "jsonb_agg" : "json_agg";
const aggregateExpression = new ValueComponent_1.FunctionCall(null, new ValueComponent_1.RawString(aggregationFunc), new ValueComponent_1.ValueList([new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(mapping.rootName))]), null);
return new SimpleSelectQuery_1.SimpleSelectQuery({
withClause: new Clause_1.WithClause(false, currentCtes),
selectClause: new Clause_1.SelectClause([
new Clause_1.SelectItem(aggregateExpression, `${mapping.rootName}_array`)
]),
fromClause: new Clause_1.FromClause(new Clause_1.SourceExpression(new Clause_1.TableSource(null, new ValueComponent_1.IdentifierString(rootObjectCteAlias)), null), null),
});
}
else {
// For a single object result, create root object CTE without alias
const rootObjectBuilderExpression = this.buildEntityJsonObject(rootEntity, null, // No source alias for single table
mapping.nestedEntities, allEntities, mapping.useJsonb);
const rootObjectSelectItem = new Clause_1.SelectItem(rootObjectBuilderExpression, mapping.rootName);
const rootObjectCte = new Clause_1.CommonTable(new SimpleSelectQuery_1.SimpleSelectQuery({
selectClause: new Clause_1.SelectClause([rootObjectSelectItem]),
fromClause: new Clause_1.FromClause(new Clause_1.SourceExpression(new Clause_1.TableSource(null, new ValueComponent_1.IdentifierString(lastCteAliasForFromClause)), null // No alias
), null),
}), new Clause_1.SourceAliasExpression(rootObjectCteAlias, null), null);
currentCtes.push(rootObjectCte);
// Select directly from the root_object_cte with LIMIT 1
return new SimpleSelectQuery_1.SimpleSelectQuery({
withClause: new Clause_1.WithClause(false, currentCtes),
selectClause: new Clause_1.SelectClause([
new Clause_1.SelectItem(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(mapping.rootName)), mapping.rootName)
]),
fromClause: new Clause_1.FromClause(new Clause_1.SourceExpression(new Clause_1.TableSource(null, new ValueComponent_1.IdentifierString(rootObjectCteAlias)), null), null),
limitClause: new Clause_1.LimitClause(new ValueComponent_1.LiteralValue(1)) // Correctly use LimitClause
});
}
}
/**
* Build JSON object for entity, using parent JSON columns when available
*/
buildEntityJsonObject(entity, sourceAlias, nestedEntities, allEntities, useJsonb = false) {
const jsonBuildFunction = useJsonb ? "jsonb_build_object" : "json_build_object";
const args = []; // Add the entity's own columns
Object.entries(entity.columns).forEach(([jsonKey, sqlColumn]) => {
args.push(new ValueComponent_1.LiteralValue(jsonKey));
args.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(sqlColumn)));
});
// Find and process child entities (both object and array types)
const childEntities = nestedEntities.filter((ne) => ne.parentId === entity.id);
childEntities.forEach((childEntity) => {
const child = allEntities.get(childEntity.id);
if (!child)
return;
args.push(new ValueComponent_1.LiteralValue(childEntity.propertyName));
if (childEntity.relationshipType === "object") {
// For object relationships, use pre-computed JSON column
const jsonColumnName = `${child.name.toLowerCase()}_json`;
args.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(jsonColumnName)));
}
else if (childEntity.relationshipType === "array") {
// For array relationships, use the column directly
args.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(childEntity.propertyName)));
}
});
return new ValueComponent_1.FunctionCall(null, new ValueComponent_1.RawString(jsonBuildFunction), new ValueComponent_1.ValueList(args), null);
}
}
exports.PostgreJsonQueryBuilder = PostgreJsonQueryBuilder;
//# sourceMappingURL=PostgreJsonQueryBuilder.js.map