Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support media deep filtering & relation shortcut filters #19971

Merged
merged 3 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/filters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ backend:
- 'packages/{utils,generators,cli,providers}/**'
- 'packages/core/*/{lib,bin,ee}/**'
- 'tests/api/**'
- 'packages/core/database/**'
frontend:
- '.github/actions/yarn-nm-install/*.yml'
- '.github/workflows/**'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@
"type": "relation",
"relation": "morphToMany"
},
"morph_one": {
"type": "relation",
"relation": "morphOne",
"target": "api::tag.tag",
"morphBy": "taggable"
},
"custom_field": {
"type": "customField",
"customField": "plugin::color-picker.color"
Expand Down
13 changes: 13 additions & 0 deletions examples/getstarted/src/api/tag/content-types/tag/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@
"relation": "oneToOne",
"target": "api::kitchensink.kitchensink",
"mappedBy": "one_to_one_tag"
},
"taggable": {
"type": "relation",
"relation": "morphToOne",
"morphColumn": {
"typeColumn": {
"name": "taggable_type"
},
"idColumn": {
"name": "taggable_id",
"referencedColumn": "id"
}
}
}
}
}
10 changes: 4 additions & 6 deletions packages/core/database/src/metadata/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,13 @@ const createManyToMany = (
* set info in the traget
*/
const createMorphToOne = (attributeName: string, attribute: Relation.MorphToOne) => {
const idColumnName = 'target_id';
const typeColumnName = 'target_type';
// TODO: (breaking) support ${attributeName}_id and ${attributeName}_type as default column names
const idColumnName = `target_id`;
const typeColumnName = `target_type`;

Object.assign(attribute, {
owner: true,
morphColumn: {
// TODO: add referenced column
morphColumn: attribute.morphColumn ?? {
typeColumn: {
name: typeColumnName,
},
Expand All @@ -225,8 +225,6 @@ const createMorphToOne = (attributeName: string, attribute: Relation.MorphToOne)
},
},
});

// TODO: implement bidirectional
};

/**
Expand Down
57 changes: 56 additions & 1 deletion packages/core/database/src/query/helpers/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,69 @@ const createPivotJoin = (
};

const createJoin = (ctx: Ctx, { alias, refAlias, attributeName, attribute }: JoinOptions) => {
const { db, qb } = ctx;
const { db, qb, uid } = ctx;

if (attribute.type !== 'relation') {
throw new Error(`Cannot join on non relational field ${attributeName}`);
}

const targetMeta = db.metadata.get(attribute.target);

if (['morphOne', 'morphMany'].includes(attribute.relation)) {
const targetAttribute = targetMeta.attributes[attribute.morphBy];

// @ts-expect-error - morphBy is not defined on the attribute
const { joinTable, morphColumn } = targetAttribute;

if (morphColumn) {
const subAlias = refAlias || qb.getAlias();

qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: morphColumn.idColumn.name,
rootColumn: morphColumn.idColumn.referencedColumn,
rootTable: alias,
on: {
[morphColumn.typeColumn.name]: uid,
...morphColumn.on,
},
});

return subAlias;
}

if (joinTable) {
const joinAlias = qb.getAlias();

qb.join({
alias: joinAlias,
referencedTable: joinTable.name,
referencedColumn: joinTable.morphColumn.idColumn.name,
rootColumn: joinTable.morphColumn.idColumn.referencedColumn,
rootTable: alias,
on: {
[joinTable.morphColumn.typeColumn.name]: uid,
field: attributeName,
},
});

const subAlias = refAlias || qb.getAlias();

qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinTable.joinColumn.referencedColumn,
rootColumn: joinTable.joinColumn.name,
rootTable: joinAlias,
});

return subAlias;
}

return alias;
}

const { joinColumn } = attribute;

if (joinColumn) {
Expand Down
49 changes: 39 additions & 10 deletions packages/core/database/src/query/helpers/where.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { isArray, castArray, keys, isPlainObject } from 'lodash/fp';
import { isArray, castArray, isPlainObject } from 'lodash/fp';
import type { Knex } from 'knex';

import { isOperatorOfType } from '@strapi/utils';
import { isOperator, isOperatorOfType } from '@strapi/utils';
import * as types from '../../utils/types';
import { createField } from '../../fields';
import { createJoin } from './join';
Expand All @@ -12,6 +12,8 @@ import { isKnexQuery } from '../../utils/knex';
import type { Ctx } from '../types';
import type { Attribute } from '../../types';

type WhereCtx = Ctx & { alias?: string; isGroupRoot?: boolean };

const isRecord = (value: unknown): value is Record<string, unknown> => isPlainObject(value);

const castValue = (value: unknown, attribute: Attribute | null) => {
Expand Down Expand Up @@ -72,7 +74,34 @@ const processNested = (where: unknown, ctx: WhereCtx) => {
return processWhere(where, ctx);
};

type WhereCtx = Ctx & { alias?: string };
const processRelationWhere = (where: unknown, ctx: WhereCtx) => {
const { qb, alias } = ctx;

const idAlias = qb.aliasColumn('id', alias);
if (!isRecord(where)) {
return { [idAlias]: where };
}

const keys = Object.keys(where);
const operatorKeys = keys.filter((key) => isOperator(key));

if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) {
throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`);
}

if (operatorKeys.length > 1) {
throw new Error(
`Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`
);
}

if (operatorKeys.length === 1) {
const operator = operatorKeys[0];
return { [idAlias]: { [operator]: processNested(where[operator], ctx) } };
}

return processWhere(where, ctx);
};

/**
* Process where parameter
Expand Down Expand Up @@ -100,8 +129,12 @@ function processWhere(
for (const key of Object.keys(where)) {
const value = where[key];

// if operator $and $or then loop over them
if (isOperatorOfType('group', key) && Array.isArray(value)) {
// if operator $and $or -> process recursively
if (isOperatorOfType('group', key)) {
if (!Array.isArray(value)) {
throw new Error(`Operator ${key} must be an array`);
}

filters[key] = value.map((sub) => processNested(sub, ctx));
continue;
}
Expand Down Expand Up @@ -132,17 +165,13 @@ function processWhere(
attribute,
});

let nestedWhere = processNested(value, {
const nestedWhere = processRelationWhere(value, {
db,
qb,
alias: subAlias,
uid: attribute.target,
});

if (!isRecord(nestedWhere) || isOperatorOfType('where', keys(nestedWhere)[0])) {
nestedWhere = { [qb.aliasColumn('id', subAlias)]: nestedWhere };
}

// TODO: use a better merge logic (push to $and when collisions)
Object.assign(filters, nestedWhere);

Expand Down