Fix relations mainField allowing hidden attributes

This commit is contained in:
Alexandre Bodin 2023-06-28 11:59:12 +02:00
parent 229d74e62c
commit 4df9a1112f
5 changed files with 273 additions and 11 deletions

View File

@ -99,4 +99,14 @@ module.exports = {
searchable: false,
},
},
config: {
attributes: {
resetPasswordToken: {
hidden: true,
},
registrationToken: {
hidden: true,
},
},
},
};

View File

@ -0,0 +1,228 @@
'use strict';
const createContext = require('../../../../../../test/helpers/create-context');
const relations = require('../relations');
const contentTypes = {
main: {
uid: 'main',
attributes: {
relation: { type: 'relation', target: 'target' },
relationWithHidden: { type: 'relation', target: 'targetWithHidden' },
},
},
targetWithHidden: {
uid: 'targetWithHidden',
attributes: { myField: { type: 'string' } },
config: {
attributes: {
myField: {
hidden: true,
},
},
},
},
target: {
uid: 'target',
attributes: { myField: { type: 'string' } },
},
};
describe('Relations', () => {
beforeAll(() => {
global.strapi = {
getModel: jest.fn((uid) => {
return contentTypes[uid];
}),
entityService: {
findPage: jest.fn(),
load: jest.fn(),
},
plugins: {
'content-manager': {
services: {
'permission-checker': {
create: () => ({
cannot: {
read: jest.fn().mockReturnValue(false),
},
sanitizedQuery: {
read: jest.fn().mockReturnValue({}),
},
}),
},
'populate-builder': () => ({
populateFromQuery: jest.fn().mockReturnThis(),
build: jest.fn().mockReturnValue({}),
}),
'content-types': {
findConfiguration: jest.fn().mockReturnValue({
metadatas: {
relation: {
edit: {
mainField: 'myField',
},
},
},
}),
},
'entity-manager': {
findOne: jest.fn(() => ({})),
},
},
},
},
};
});
afterAll(() => {
jest.clearAllMocks();
});
describe('findAvailable', () => {
test('Query mainField when mainField is listable', async () => {
const ctx = createContext(
{
params: {
model: 'main',
targetField: 'relation',
},
query: {
_q: 'foobar',
},
},
{
state: {
userAbility: {
can: jest.fn().mockReturnValue(false),
},
},
}
);
await relations.findAvailable(ctx);
expect(strapi.entityService.findPage).toHaveBeenCalledWith(
'target',
expect.objectContaining({
sort: 'myField',
fields: ['id', 'myField'],
filters: {
$and: [
{
myField: {
$containsi: 'foobar',
},
},
],
},
})
);
});
test('Replace mainField by id when mainField is not listable', async () => {
const ctx = createContext(
{
params: {
model: 'main',
targetField: 'relationWithHidden',
},
query: {
_q: 'foobar',
},
},
{
state: {
userAbility: {
can: jest.fn().mockReturnValue(false),
},
},
}
);
await relations.findAvailable(ctx);
expect(strapi.entityService.findPage).toHaveBeenCalledWith(
'targetWithHidden',
expect.objectContaining({
sort: 'id',
fields: ['id'],
filters: {
$and: [
{
id: {
$containsi: 'foobar',
},
},
],
},
})
);
});
});
describe('findExisting', () => {
test('Query mainField when mainField is listable', async () => {
const ctx = createContext(
{
params: {
model: 'main',
targetField: 'relation',
id: 1,
},
query: {
_q: 'foobar',
},
},
{
state: {
userAbility: {
can: jest.fn().mockReturnValue(false),
},
},
}
);
await relations.findExisting(ctx);
expect(strapi.entityService.load).toHaveBeenCalledWith(
'main',
{ id: 1 },
'relation',
expect.objectContaining({
fields: ['id', 'myField'],
})
);
});
test('Replace mainField by id when mainField is not listable', async () => {
const ctx = createContext(
{
params: {
model: 'main',
targetField: 'relationWithHidden',
id: 1,
},
},
{
state: {
userAbility: {
can: jest.fn().mockReturnValue(false),
},
},
}
);
await relations.findExisting(ctx);
expect(strapi.entityService.load).toHaveBeenCalledWith(
'main',
{ id: 1 },
'relationWithHidden',
expect.objectContaining({
fields: ['id'],
})
);
});
});
});

View File

@ -1,12 +1,13 @@
'use strict';
const { prop, isEmpty } = require('lodash/fp');
const { prop, isEmpty, uniq } = require('lodash/fp');
const { hasDraftAndPublish } = require('@strapi/utils').contentTypes;
const { isAnyToMany } = require('@strapi/utils').relations;
const { PUBLISHED_AT_ATTRIBUTE } = require('@strapi/utils').contentTypes.constants;
const { getService } = require('../utils');
const { validateFindAvailable, validateFindExisting } = require('./validation/relations');
const { isListable } = require('../services/utils/configuration/attributes');
const addFiltersClause = (params, filtersClause) => {
params.filters = params.filters || {};
@ -40,8 +41,6 @@ module.exports = {
const isComponent = modelSchema.modelType === 'component';
// RBAC checks when it's a content-type
// TODO: do RBAC check for components too
if (!isComponent) {
const permissionChecker = getService('permission-checker').create({
userAbility,
@ -86,9 +85,13 @@ module.exports = {
const modelConfig = isComponent
? await getService('components').findConfiguration(modelSchema)
: await getService('content-types').findConfiguration(modelSchema);
const mainField = prop(`metadatas.${targetField}.edit.mainField`, modelConfig) || 'id';
const fieldsToSelect = ['id', mainField];
let mainField = prop(`metadatas.${targetField}.edit.mainField`, modelConfig) || 'id';
if (!isListable(targetedModel, mainField)) {
mainField = 'id';
}
const fieldsToSelect = uniq(['id', mainField]);
if (hasDraftAndPublish(targetedModel)) {
fieldsToSelect.push(PUBLISHED_AT_ATTRIBUTE);
}
@ -153,8 +156,6 @@ module.exports = {
const isComponent = modelSchema.modelType === 'component';
// RBAC checks when it's a content-type
// TODO: do RBAC check for components too
if (!isComponent) {
const entityManager = getService('entity-manager');
const permissionChecker = getService('permission-checker').create({
@ -194,9 +195,12 @@ module.exports = {
? await getService('components').findConfiguration(modelSchema)
: await getService('content-types').findConfiguration(modelSchema);
const mainField = prop(`metadatas.${targetField}.edit.mainField`, modelConfig) || 'id';
let mainField = prop(`metadatas.${targetField}.edit.mainField`, modelConfig) || 'id';
if (!isListable(targetedModel, mainField)) {
mainField = 'id';
}
const fieldsToSelect = ['id', mainField];
const fieldsToSelect = uniq(['id', mainField]);
if (hasDraftAndPublish(targetedModel)) {
fieldsToSelect.push(PUBLISHED_AT_ATTRIBUTE);
}

View File

@ -1,6 +1,7 @@
'use strict';
const { yup } = require('@strapi/utils');
const { getService } = require('../../utils');
const {
isListable,
hasEditableAttribute,
@ -51,7 +52,25 @@ const createMetadasSchema = (schema) => {
placeholder: yup.string(),
editable: yup.boolean(),
visible: yup.boolean(),
mainField: yup.string(),
mainField: yup.lazy((value) => {
if (!value) {
return yup.string();
}
const targetSchema = getService('content-types').findContentType(
schema.attributes[key].targetModel
);
if (!targetSchema) {
return yup.string();
}
const validAttributes = Object.keys(targetSchema.attributes).filter((key) =>
isListable(targetSchema, key)
);
return yup.string().oneOf(validAttributes.concat('id')).default('id');
}),
step: yup
.number()
.integer()

View File

@ -6,6 +6,7 @@ const {
isSortable,
isSearchable,
isVisible,
isListable,
isRelation,
getDefaultMainField,
} = require('./attributes');
@ -121,7 +122,7 @@ async function syncMetadatas(configuration, schema) {
if (!targetSchema) return acc;
if (!isSortable(targetSchema, edit.mainField)) {
if (!isSortable(targetSchema, edit.mainField) && !isListable(targetSchema, edit.mainField)) {
_.set(updatedMeta, ['edit', 'mainField'], getDefaultMainField(targetSchema));
_.set(acc, [key], updatedMeta);
return acc;