@vendure/core
Version:
A modern, headless ecommerce framework
708 lines • 33.8 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ListQueryBuilder = void 0;
const common_1 = require("@nestjs/common");
const generated_types_1 = require("@vendure/common/lib/generated-types");
const unique_1 = require("@vendure/common/lib/unique");
const typeorm_1 = require("typeorm");
const common_2 = require("../../../common");
const instrument_decorator_1 = require("../../../common/instrument-decorator");
const config_1 = require("../../../config");
const connection_1 = require("../../../connection");
const tree_relations_qb_joiner_1 = require("../utils/tree-relations-qb-joiner");
const connection_utils_1 = require("./connection-utils");
const get_calculated_columns_1 = require("./get-calculated-columns");
const parse_filter_params_1 = require("./parse-filter-params");
const parse_sort_params_1 = require("./parse-sort-params");
/**
* Counter for generating unique aliases in EXISTS subqueries.
* Using a module-level counter ensures uniqueness across all queries in a session.
*/
let existsSubqueryCounter = 0;
/**
* @description
* This helper class is used when fetching entities the database from queries which return a {@link PaginatedList} type.
* These queries all follow the same format:
*
* In the GraphQL definition, they return a type which implements the `Node` interface, and the query returns a
* type which implements the `PaginatedList` interface:
*
* ```GraphQL
* type BlogPost implements Node {
* id: ID!
* published: DateTime!
* title: String!
* body: String!
* }
*
* type BlogPostList implements PaginatedList {
* items: [BlogPost!]!
* totalItems: Int!
* }
*
* # Generated at run-time by Vendure
* input BlogPostListOptions
*
* extend type Query {
* blogPosts(options: BlogPostListOptions): BlogPostList!
* }
* ```
* When Vendure bootstraps, it will find the `BlogPostListOptions` input and, because it is used in a query
* returning a `PaginatedList` type, it knows that it should dynamically generate this input. This means
* all primitive field of the `BlogPost` type (namely, "published", "title" and "body") will have `filter` and
* `sort` inputs created for them, as well a `skip` and `take` fields for pagination.
*
* Your resolver function will then look like this:
*
* ```ts
* \@Resolver()
* export class BlogPostResolver
* constructor(private blogPostService: BlogPostService) {}
*
* \@Query()
* async blogPosts(
* \@Ctx() ctx: RequestContext,
* \@Args() args: any,
* ): Promise<PaginatedList<BlogPost>> {
* return this.blogPostService.findAll(ctx, args.options || undefined);
* }
* }
* ```
*
* and the corresponding service will use the ListQueryBuilder:
*
* ```ts
* \@Injectable()
* export class BlogPostService {
* constructor(private listQueryBuilder: ListQueryBuilder) {}
*
* findAll(ctx: RequestContext, options?: ListQueryOptions<BlogPost>) {
* return this.listQueryBuilder
* .build(BlogPost, options)
* .getManyAndCount()
* .then(async ([items, totalItems]) => {
* return { items, totalItems };
* });
* }
* }
* ```
*
* @docsCategory data-access
* @docsPage ListQueryBuilder
* @docsWeight 0
*/
let ListQueryBuilder = class ListQueryBuilder {
constructor(connection, configService) {
this.connection = connection;
this.configService = configService;
}
/** @internal */
onApplicationBootstrap() {
this.registerSQLiteRegexpFunction();
}
/**
* @description
* Used to determine whether a list query `filter` object contains the
* given property, either at the top level or nested inside a boolean
* `_and` or `_or` expression.
*
* This is useful when a custom property map is used to map a filter
* field to a related entity, and we need to determine whether the
* filter object contains that property, which then means we would need
* to join that relation.
*/
filterObjectHasProperty(filterObject, property) {
if (!filterObject) {
return false;
}
for (const key in filterObject) {
if (!filterObject[key]) {
continue;
}
if (key === property) {
return true;
}
if (key === '_and' || key === '_or') {
const value = filterObject[key];
for (const condition of value) {
if (this.filterObjectHasProperty(condition, property)) {
return true;
}
}
}
}
return false;
}
/*
* @description
* Creates and configures a SelectQueryBuilder for queries that return paginated lists of entities.
*/
build(entity, options = {}, extendedOptions = {}) {
var _a, _b, _c, _d;
const apiType = (_b = (_a = extendedOptions.ctx) === null || _a === void 0 ? void 0 : _a.apiType) !== null && _b !== void 0 ? _b : 'shop';
const { take, skip } = this.parseTakeSkipParams(apiType, options, extendedOptions.ignoreQueryLimits);
const repo = extendedOptions.ctx
? this.connection.getRepository(extendedOptions.ctx, entity)
: this.connection.rawConnection.getRepository(entity);
const alias = extendedOptions.entityAlias || entity.name.toLowerCase();
const minimumRequiredRelations = this.getMinimumRequiredRelations(repo, options, extendedOptions);
const qb = repo.createQueryBuilder(alias);
let relations = (0, unique_1.unique)([...minimumRequiredRelations, ...((_c = extendedOptions === null || extendedOptions === void 0 ? void 0 : extendedOptions.relations) !== null && _c !== void 0 ? _c : [])]);
// Special case for the 'collection' entity, which has a complex nested structure
// and requires special handling to ensure that only the necessary relations are joined.
// This is bypassed an issue in TypeORM where it would join the same relation multiple times.
// See https://github.com/typeorm/typeorm/issues/9936 for more context.
const processedRelations = (0, tree_relations_qb_joiner_1.joinTreeRelationsDynamically)(qb, entity, relations);
// Remove any relations which are related to the 'collection' tree, as these are handled separately
// to avoid duplicate joins.
relations = relations.filter(relationPath => !processedRelations.has(relationPath));
qb.setFindOptions({
relations,
take,
skip,
where: extendedOptions.where || {},
relationLoadStrategy: 'query',
});
// join the tables required by calculated columns
this.joinCalculatedColumnRelations(qb, entity, options);
const { customPropertyMap } = extendedOptions;
// Store the original customPropertyMap before normalization for EXISTS subquery generation.
// This is needed because normalizeCustomPropertyMap mutates customPropertyMap, but
// parseFilterParams needs the original paths to detect *-to-Many relations.
const originalCustomPropertyMap = customPropertyMap ? Object.assign({}, customPropertyMap) : undefined;
if (customPropertyMap) {
this.normalizeCustomPropertyMap(customPropertyMap, options, qb);
}
const customFieldsForType = this.configService.customFields[entity.name];
const sortParams = Object.assign({}, options.sort, extendedOptions.orderBy);
this.applyTranslationConditions(qb, entity, sortParams, extendedOptions.ctx);
const sort = (0, parse_sort_params_1.parseSortParams)(qb.connection, entity, sortParams, customPropertyMap, qb.alias, customFieldsForType);
const filter = (0, parse_filter_params_1.parseFilterParams)({
connection: qb.connection,
entity,
filterParams: options.filter,
customPropertyMap,
originalCustomPropertyMap,
entityAlias: qb.alias,
});
if (filter.length) {
const filterOperator = (_d = options.filterOperator) !== null && _d !== void 0 ? _d : generated_types_1.LogicalOperator.AND;
qb.andWhere(new typeorm_1.Brackets(qb1 => {
for (const condition of filter) {
if ('conditions' in condition) {
this.addNestedWhereClause(qb1, condition, filterOperator, qb, entity);
}
else {
this.applyWhereCondition(qb1, condition, filterOperator, qb, entity);
}
}
}));
}
if (extendedOptions.channelId) {
qb.innerJoin(`${qb.alias}.channels`, 'lqb__channel', 'lqb__channel.id = :channelId', {
channelId: extendedOptions.channelId,
});
}
qb.orderBy(sort);
// Note: EntityAccessControlStrategy.applyAccessControl() is NOT called explicitly
// here. When a strategy is configured, the QueryBuilder returned from
// getRepository(ctx, entity).createQueryBuilder() is Proxy-wrapped, and the
// strategy is applied automatically before any terminal method (getMany, getCount, etc.).
return qb;
}
addNestedWhereClause(qb, whereGroup, parentOperator, mainQb, entity) {
if (whereGroup.conditions.length) {
const subQb = new typeorm_1.Brackets(qb1 => {
whereGroup.conditions.forEach(condition => {
if ('conditions' in condition) {
this.addNestedWhereClause(qb1, condition, whereGroup.operator, mainQb, entity);
}
else {
this.applyWhereCondition(qb1, condition, whereGroup.operator, mainQb, entity);
}
});
});
if (parentOperator === generated_types_1.LogicalOperator.AND) {
qb.andWhere(subQb);
}
else {
qb.orWhere(subQb);
}
}
}
/**
* Applies a WHERE condition to the query builder. For conditions that need EXISTS subquery
* treatment (duplicate custom property fields in _and blocks), generates an EXISTS subquery
* instead of a simple WHERE clause.
*/
applyWhereCondition(qb, condition, operator, mainQb, entity) {
if (condition.isExistsCondition) {
// Generate EXISTS subquery for duplicate custom property conditions
const existsClause = this.buildExistsSubquery(condition, mainQb, entity);
if (existsClause) {
if (operator === generated_types_1.LogicalOperator.AND) {
qb.andWhere(existsClause.clause, existsClause.parameters);
}
else {
qb.orWhere(existsClause.clause, existsClause.parameters);
}
return;
}
}
// Standard WHERE clause handling
if (operator === generated_types_1.LogicalOperator.AND) {
qb.andWhere(condition.clause, condition.parameters);
}
else {
qb.orWhere(condition.clause, condition.parameters);
}
}
/**
* Builds an EXISTS subquery for a custom property condition on a *-to-Many relation.
* This is necessary because a simple WHERE clause on a joined table cannot express
* "entity has related item with value A AND entity has related item with value B"
* when those are in separate rows of the related table.
*
* Supports both:
* - ManyToMany relations (uses junction table)
* - OneToMany relations (direct foreign key on the related table)
*
* @see https://github.com/vendurehq/vendure/issues/3267
*/
buildExistsSubquery(condition, mainQb, entity) {
var _a, _b, _c;
if (!condition.isExistsCondition) {
return null;
}
const { customPropertyPath } = condition.isExistsCondition;
const pathParts = customPropertyPath.split('.');
if (pathParts.length < 2) {
return null;
}
const relationName = pathParts[0]; // e.g., 'facetValues' or 'orderLines'
const columnName = pathParts[1]; // e.g., 'id'
const metadata = (_a = mainQb.expressionMap.mainAlias) === null || _a === void 0 ? void 0 : _a.metadata;
if (!metadata) {
return null;
}
const relation = metadata.findRelationWithPropertyPath(relationName);
if (!relation) {
return null;
}
// Get the related entity's table and column info
const inverseEntityMeta = relation.inverseEntityMetadata;
const inverseTableName = inverseEntityMeta.tablePath;
// Generate unique alias using counter
existsSubqueryCounter++;
const aliasBase = `lqb_exists_${existsSubqueryCounter}`;
// Determine the comparison operator from the original clause
const comparisonOperator = this.extractComparisonOperator(condition.clause);
// Copy all parameters with 'exists_' prefix to ensure uniqueness.
// This handles operators with multiple params like BETWEEN (arg1_a, arg1_b).
const parameters = {};
const paramKeys = Object.keys(condition.parameters);
for (const key of paramKeys) {
parameters[`exists_${key}`] = condition.parameters[key];
}
// Use the first param key as the base for the WHERE clause construction.
// For BETWEEN this will be 'exists_arg1' (we strip the _a/_b suffix).
const baseParamKey = (_c = (_b = paramKeys[0]) === null || _b === void 0 ? void 0 : _b.replace(/_[ab]$/, '')) !== null && _c !== void 0 ? _c : 'arg';
const newParamKey = `exists_${baseParamKey}`;
// Helper to escape identifiers for the current database driver (handles PostgreSQL quoting)
const escapeId = (name) => mainQb.connection.driver.escape(name);
const escapeTablePath = (path) => path.split('.').map(segment => mainQb.connection.driver.escape(segment)).join('.');
let existsQuery;
if (relation.isManyToMany) {
// ManyToMany: Uses a junction table
const junctionMeta = relation.junctionEntityMetadata;
if (!junctionMeta) {
return null;
}
const junctionTableName = junctionMeta.tablePath;
const ownerColumn = junctionMeta.ownerColumns[0];
const inverseColumn = junctionMeta.inverseColumns[0];
if (!ownerColumn || !inverseColumn) {
return null;
}
const junctionAlias = aliasBase;
const relatedAlias = `${aliasBase}_related`;
const whereCondition = this.buildWhereConditionClause(relatedAlias, columnName, comparisonOperator, newParamKey, escapeId);
// EXISTS (SELECT 1 FROM junction_table jt
// INNER JOIN related_table rt ON jt.inverseColumn = rt.id
// WHERE jt.ownerColumn = main_entity.id AND rt.columnName = :paramValue)
existsQuery = `EXISTS (
SELECT 1 FROM ${escapeTablePath(junctionTableName)} ${escapeId(junctionAlias)}
INNER JOIN ${escapeTablePath(inverseTableName)} ${escapeId(relatedAlias)}
ON ${escapeId(junctionAlias)}.${escapeId(inverseColumn.databaseName)} = ${escapeId(relatedAlias)}.${escapeId('id')}
WHERE ${escapeId(junctionAlias)}.${escapeId(ownerColumn.databaseName)} = ${escapeId(mainQb.alias)}.${escapeId('id')} AND ${whereCondition}
)`;
}
else if (relation.isOneToMany) {
// OneToMany: The related table has a foreign key back to the main entity
const relatedAlias = aliasBase;
// Find the foreign key column on the related entity that points back to the main entity
const inverseRelation = relation.inverseRelation;
if (!inverseRelation) {
return null;
}
// Get the join columns from the inverse relation (ManyToOne side)
const joinColumns = inverseRelation.joinColumns;
if (!joinColumns || joinColumns.length === 0) {
return null;
}
const foreignKeyColumn = joinColumns[0].databaseName;
if (!foreignKeyColumn) {
return null;
}
const whereCondition = this.buildWhereConditionClause(relatedAlias, columnName, comparisonOperator, newParamKey, escapeId);
// EXISTS (SELECT 1 FROM related_table rt
// WHERE rt.foreignKey = main_entity.id AND rt.columnName = :paramValue)
existsQuery = `EXISTS (
SELECT 1 FROM ${escapeTablePath(inverseTableName)} ${escapeId(relatedAlias)}
WHERE ${escapeId(relatedAlias)}.${escapeId(foreignKeyColumn)} = ${escapeId(mainQb.alias)}.${escapeId('id')} AND ${whereCondition}
)`;
}
else {
// Not a *-to-Many relation, shouldn't happen but fall back gracefully
return null;
}
return {
clause: existsQuery,
parameters,
};
}
/**
* Extracts the comparison operator from a SQL clause string.
*/
extractComparisonOperator(clause) {
if (clause.includes('!=')) {
return '!=';
}
else if (clause.includes('>=')) {
return '>=';
}
else if (clause.includes('<=')) {
return '<=';
}
else if (clause.includes('>')) {
return '>';
}
else if (clause.includes('<')) {
return '<';
}
else if (clause.includes(' IN ')) {
return 'IN';
}
else if (clause.includes(' NOT IN ')) {
return 'NOT IN';
}
else if (clause.includes(' ILIKE ')) {
return 'ILIKE';
}
else if (clause.includes(' NOT LIKE ') || clause.includes(' NOT ILIKE ')) {
return clause.includes('ILIKE') ? 'NOT ILIKE' : 'NOT LIKE';
}
else if (clause.includes(' LIKE ')) {
return 'LIKE';
}
else if (clause.includes(' IS NULL')) {
return 'IS NULL';
}
else if (clause.includes(' IS NOT NULL')) {
return 'IS NOT NULL';
}
else if (clause.includes(' BETWEEN ')) {
return 'BETWEEN';
}
return '=';
}
/**
* Builds a WHERE condition clause string for the EXISTS subquery.
*/
buildWhereConditionClause(alias, columnName, operator, paramKey, escapeId) {
const col = `${escapeId(alias)}.${escapeId(columnName)}`;
if (operator === 'IN') {
return `${col} IN (:...${paramKey})`;
}
else if (operator === 'NOT IN') {
return `${col} NOT IN (:...${paramKey})`;
}
else if (operator === 'IS NULL') {
return `${col} IS NULL`;
}
else if (operator === 'IS NOT NULL') {
return `${col} IS NOT NULL`;
}
else if (operator === 'BETWEEN') {
return `${col} BETWEEN :${paramKey}_a AND :${paramKey}_b`;
}
return `${col} ${operator} :${paramKey}`;
}
parseTakeSkipParams(apiType, options, ignoreQueryLimits = false) {
var _a;
const { shopListQueryLimit, adminListQueryLimit } = this.configService.apiOptions;
const takeLimit = ignoreQueryLimits
? Number.MAX_SAFE_INTEGER
: apiType === 'admin'
? adminListQueryLimit
: shopListQueryLimit;
if (options.take && options.take > takeLimit) {
throw new common_2.UserInputError('error.list-query-limit-exceeded', { limit: takeLimit });
}
const rawConnection = this.connection.rawConnection;
const skip = Math.max((_a = options.skip) !== null && _a !== void 0 ? _a : 0, 0);
// `take` must not be negative, and must not be greater than takeLimit
let take = options.take == null ? takeLimit : Math.min(Math.max(options.take, 0), takeLimit);
if (options.skip !== undefined && options.take === undefined) {
take = takeLimit;
}
return { take, skip };
}
/**
* @description
* As part of list optimization, we only join the minimum required relations which are needed to
* get the base list query. Other relations are then joined individually in the patched `getManyAndCount()`
* method.
*/
getMinimumRequiredRelations(repository, options, extendedOptions) {
const requiredRelations = [];
if (extendedOptions.channelId) {
requiredRelations.push('channels');
}
if (extendedOptions.customPropertyMap) {
const metadata = repository.metadata;
for (const [property, path] of Object.entries(extendedOptions.customPropertyMap)) {
if (!this.customPropertyIsBeingUsed(property, options)) {
// If the custom property is not being used to filter or sort, then we don't need
// to join the associated relations.
continue;
}
const relationPath = path.split('.').slice(0, -1);
let targetMetadata = metadata;
const reconstructedPath = [];
for (const relationPathPart of relationPath) {
const relationMetadata = targetMetadata.findRelationWithPropertyPath(relationPathPart);
if (relationMetadata) {
reconstructedPath.push(relationMetadata.propertyName);
requiredRelations.push(reconstructedPath.join('.'));
targetMetadata = relationMetadata.inverseEntityMetadata;
}
}
}
}
return (0, unique_1.unique)(requiredRelations);
}
customPropertyIsBeingUsed(property, options) {
var _a;
return !!(((_a = options.sort) === null || _a === void 0 ? void 0 : _a[property]) || this.isPropertyUsedInFilter(property, options.filter));
}
isPropertyUsedInFilter(property, filter) {
var _a, _b;
return !!(filter &&
(filter[property] ||
((_a = filter._and) === null || _a === void 0 ? void 0 : _a.some(nestedFilter => this.isPropertyUsedInFilter(property, nestedFilter))) ||
((_b = filter._or) === null || _b === void 0 ? void 0 : _b.some(nestedFilter => this.isPropertyUsedInFilter(property, nestedFilter)))));
}
/**
* If a customPropertyMap is provided, we need to take the path provided and convert it to the actual
* relation aliases being used by the SelectQueryBuilder.
*
* This method mutates the customPropertyMap object.
*/
normalizeCustomPropertyMap(customPropertyMap, options, qb) {
var _a;
for (const [property, value] of Object.entries(customPropertyMap)) {
if (!this.customPropertyIsBeingUsed(property, options)) {
continue;
}
let parts = customPropertyMap[property].split('.');
const normalizedRelationPath = [];
let entityMetadata = (_a = qb.expressionMap.mainAlias) === null || _a === void 0 ? void 0 : _a.metadata;
let entityAlias = qb.alias;
while (parts.length > 1) {
const entityPart = 2 <= parts.length ? parts[0] : qb.alias;
const columnPart = parts[parts.length - 1];
if (!entityMetadata) {
config_1.Logger.error(`Could not get metadata for entity ${qb.alias}`);
continue;
}
const relationMetadata = entityMetadata.findRelationWithPropertyPath(entityPart);
if (!relationMetadata || !(relationMetadata === null || relationMetadata === void 0 ? void 0 : relationMetadata.propertyName)) {
config_1.Logger.error(`The customPropertyMap entry "${property}:${value}" could not be resolved to a related table`);
delete customPropertyMap[property];
return;
}
const alias = `${entityMetadata.tableName}_${relationMetadata.propertyName}`;
if (!this.isRelationAlreadyJoined(qb, alias)) {
qb.leftJoinAndSelect(`${entityAlias}.${relationMetadata.propertyName}`, alias);
}
parts = parts.slice(1);
entityMetadata = relationMetadata === null || relationMetadata === void 0 ? void 0 : relationMetadata.inverseEntityMetadata;
normalizedRelationPath.push(entityAlias);
if (parts.length === 1) {
normalizedRelationPath.push(alias, columnPart);
}
else {
entityAlias = alias;
}
}
customPropertyMap[property] = normalizedRelationPath.slice(-2).join('.');
}
}
/**
* Some calculated columns (those with the `@Calculated()` decorator) require extra joins in order
* to derive the data needed for their expressions.
*/
joinCalculatedColumnRelations(qb, entity, options) {
const calculatedColumns = (0, get_calculated_columns_1.getCalculatedColumns)(entity);
const filterAndSortFields = this.getFilterAndSortFields(options);
const alias = (0, connection_utils_1.getEntityAlias)(this.connection.rawConnection, entity);
for (const field of filterAndSortFields) {
const calculatedColumnDef = calculatedColumns.find(c => c.name === field);
const instruction = calculatedColumnDef === null || calculatedColumnDef === void 0 ? void 0 : calculatedColumnDef.listQuery;
if (instruction) {
const relations = instruction.relations || [];
for (const relation of relations) {
const relationIsAlreadyJoined = qb.expressionMap.joinAttributes.find(ja => ja.entityOrProperty === `${alias}.${relation}`);
if (!relationIsAlreadyJoined) {
const propertyPath = relation.includes('.') ? relation : `${alias}.${relation}`;
const relationAlias = relation.includes('.')
? relation.split('.').reverse()[0]
: relation;
qb.innerJoinAndSelect(propertyPath, relationAlias);
}
}
if (typeof instruction.query === 'function') {
instruction.query(qb);
}
}
}
}
getFilterAndSortFields(options) {
const sortFields = Object.keys(options.sort || {});
// filter fields can be immediate children of the filter object
// or nested inside _and or _or
const filterFields = this.getFilterFields(options.filter);
return (0, unique_1.unique)([...sortFields, ...filterFields]);
}
getFilterFields(filter) {
if (!filter) {
return [];
}
const filterFields = [];
for (const key in filter) {
if (key === '_and' || key === '_or') {
const value = filter[key];
for (const condition of value) {
filterFields.push(...this.getFilterFields(condition));
}
}
else if (filter[key]) {
filterFields.push(key);
}
}
return (0, unique_1.unique)(filterFields);
}
/**
* @description
* If this entity is Translatable, and we are sorting on one of the translatable fields,
* then we need to apply appropriate WHERE clauses to limit
* the joined translation relations.
*/
applyTranslationConditions(qb, entity, sortParams, ctx) {
const languageCode = (ctx === null || ctx === void 0 ? void 0 : ctx.languageCode) || this.configService.defaultLanguageCode;
const { translationColumns } = (0, connection_utils_1.getColumnMetadata)(qb.connection, entity);
const alias = qb.alias;
const sortKeys = Object.keys(sortParams);
let sortingOnTranslatableKey = false;
for (const translationColumn of translationColumns) {
if (sortKeys.includes(translationColumn.propertyName)) {
sortingOnTranslatableKey = true;
}
}
if (translationColumns.length && sortingOnTranslatableKey) {
const translationsAlias = qb.connection.namingStrategy.joinTableName(alias, 'translations', '', '');
if (!this.isRelationAlreadyJoined(qb, translationsAlias)) {
qb.leftJoinAndSelect(`${alias}.translations`, translationsAlias);
}
qb.andWhere(new typeorm_1.Brackets(qb1 => {
var _a;
qb1.where(`${translationsAlias}.languageCode = :languageCode`, { languageCode });
const defaultLanguageCode = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.channel.defaultLanguageCode) !== null && _a !== void 0 ? _a : this.configService.defaultLanguageCode;
const translationEntity = translationColumns[0].entityMetadata.target;
if (languageCode !== defaultLanguageCode) {
// If the current languageCode is not the default, then we create a more
// complex WHERE clause to allow us to use the non-default translations and
// fall back to the default language if no translation exists.
qb1.orWhere(new typeorm_1.Brackets(qb2 => {
const subQb1 = this.connection.rawConnection
.createQueryBuilder(translationEntity, 'translation')
.where(`translation.base = ${alias}.id`)
.andWhere('translation.languageCode = :defaultLanguageCode');
const subQb2 = this.connection.rawConnection
.createQueryBuilder(translationEntity, 'translation')
.where(`translation.base = ${alias}.id`)
.andWhere('translation.languageCode = :nonDefaultLanguageCode');
qb2.where(`EXISTS (${subQb1.getQuery()})`).andWhere(`NOT EXISTS (${subQb2.getQuery()})`);
}));
}
else {
qb1.orWhere(new typeorm_1.Brackets(qb2 => {
const subQb1 = this.connection.rawConnection
.createQueryBuilder(translationEntity, 'translation')
.where(`translation.base = ${alias}.id`)
.andWhere('translation.languageCode = :defaultLanguageCode');
const subQb2 = this.connection.rawConnection
.createQueryBuilder(translationEntity, 'translation')
.where(`translation.base = ${alias}.id`)
.andWhere('translation.languageCode != :defaultLanguageCode');
qb2.where(`NOT EXISTS (${subQb1.getQuery()})`).andWhere(`EXISTS (${subQb2.getQuery()})`);
}));
}
qb.setParameters({
nonDefaultLanguageCode: languageCode,
defaultLanguageCode,
});
}));
}
}
/**
* Registers a user-defined function (for flavors of SQLite driver that support it)
* so that we can run regex filters on string fields.
*/
registerSQLiteRegexpFunction() {
const regexpFn = (pattern, value) => {
const result = new RegExp(`${pattern}`, 'i').test(value);
return result ? 1 : 0;
};
const dbType = this.connection.rawConnection.options.type;
if (dbType === 'better-sqlite3') {
const driver = this.connection.rawConnection.driver;
driver.databaseConnection.function('regexp', regexpFn);
}
if (dbType === 'sqljs') {
const driver = this.connection.rawConnection.driver;
driver.databaseConnection.create_function('regexp', regexpFn);
}
}
isRelationAlreadyJoined(qb, alias) {
return qb.expressionMap.joinAttributes.some(ja => ja.alias.name === alias);
}
};
exports.ListQueryBuilder = ListQueryBuilder;
exports.ListQueryBuilder = ListQueryBuilder = __decorate([
(0, common_1.Injectable)(),
(0, instrument_decorator_1.Instrument)(),
__metadata("design:paramtypes", [connection_1.TransactionalConnection,
config_1.ConfigService])
], ListQueryBuilder);
//# sourceMappingURL=list-query-builder.js.map