Skip to content

Commit

Permalink
feat: support media deept filtering & relation shortcut filters
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrebodin committed Mar 30, 2024
1 parent a8ca954 commit 456a884
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 17 deletions.
1 change: 1 addition & 0 deletions .github/filters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ backend:
- 'packages/**/strapi-server.js'
- 'packages/{utils,generators,cli,providers}/**'
- 'packages/core/*/{lib,bin,ee}/**'
- 'packages/core/database/**'
- 'api-tests/**'
frontend:
- '.github/actions/yarn-nm-install/*.yml'
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 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

0 comments on commit 456a884

Please sign in to comment.