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

Remove root level nested params #20172

Merged
merged 11 commits into from
May 28, 2024
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { setCreatorFields, async, errors } from '@strapi/utils';

import type { Modules, UID } from '@strapi/types';

import { getService } from '../utils';
import { validateBulkActionInput } from './validation';
import { getProhibitedCloningFields, excludeNotCreatableFields } from './utils/clone';
import { getDocumentLocaleAndStatus } from './validation/dimensions';
import { formatDocumentWithMetadata } from './utils/metadata';

type Options = Modules.Documents.Params.Pick<UID.ContentType, 'populate:object'>;

/**
* Create a new document.
*
Expand All @@ -13,7 +18,7 @@ import { formatDocumentWithMetadata } from './utils/metadata';
* @param opts.populate - Populate options of the returned document.
* By default documentManager will populate all relations.
*/
const createDocument = async (ctx: any, opts?: { populate?: object }) => {
const createDocument = async (ctx: any, opts?: Options) => {
const { userAbility, user } = ctx.state;
const { model } = ctx.params;
const { body } = ctx.request;
Expand Down Expand Up @@ -55,7 +60,7 @@ const createDocument = async (ctx: any, opts?: { populate?: object }) => {
* @param opts - Options
* @param opts.populate - Populate options of the returned document
*/
const updateDocument = async (ctx: any, opts?: { populate?: object }) => {
const updateDocument = async (ctx: any, opts?: Options) => {
const { userAbility, user } = ctx.state;
const { id, model } = ctx.params;
const { body } = ctx.request;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { UID } from '@strapi/types';
import type { UID, Modules } from '@strapi/types';
import { setCreatorFields, async, errors } from '@strapi/utils';

import { getDocumentLocaleAndStatus } from './validation/dimensions';
import { getService } from '../utils';
import { formatDocumentWithMetadata } from './utils/metadata';

type OptionsWithPopulate = Modules.Documents.Params.Pick<UID.ContentType, 'populate:object'>;

const buildPopulateFromQuery = async (query: any, model: any) => {
return getService('populate-builder')(model)
.populateFromQuery(query)
Expand All @@ -25,7 +27,7 @@ const findDocument = async (query: any, uid: UID.SingleType, opts: any = {}) =>
);
};

const createOrUpdateDocument = async (ctx: any, opts?: { populate: object }) => {
const createOrUpdateDocument = async (ctx: any, opts?: OptionsWithPopulate) => {
const { user, userAbility } = ctx.state;
const { model } = ctx.params;
const { body, query } = ctx.request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ const getDeepPopulateDraftCount = (uid: UID.Schema) => {
let hasRelations = false;

const populate = Object.keys(model.attributes).reduce((populateAcc: any, attributeName) => {
const attribute: any = model.attributes[attributeName];
const attribute: Schema.Attribute.AnyAttribute = model.attributes[attributeName];

switch (attribute.type) {
case 'relation': {
Expand All @@ -258,24 +258,29 @@ const getDeepPopulateDraftCount = (uid: UID.Schema) => {
attribute.component
);
if (childHasRelations) {
populateAcc[attributeName] = { populate };
populateAcc[attributeName] = {
populate,
};
hasRelations = true;
}
break;
}
case 'dynamiczone': {
const dzPopulate = (attribute.components || []).reduce((acc: any, componentUID: any) => {
const { populate, hasRelations: childHasRelations } =
const dzPopulateFragment = attribute.components?.reduce((acc, componentUID) => {
const { populate: componentPopulate, hasRelations: componentHasRelations } =
getDeepPopulateDraftCount(componentUID);
if (childHasRelations) {

if (componentHasRelations) {
hasRelations = true;
return merge(acc, populate);

return { ...acc, [componentUID]: { populate: componentPopulate } };
}

return acc;
}, {});

if (!isEmpty(dzPopulate)) {
populateAcc[attributeName] = { populate: dzPopulate };
if (!isEmpty(dzPopulateFragment)) {
populateAcc[attributeName] = { on: dzPopulateFragment };
}
break;
}
Expand Down
20 changes: 8 additions & 12 deletions packages/core/types/src/modules/documents/params/populate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type * as Schema from '../../../schema';

import type * as UID from '../../../uid';
import type { Constants, Guard, If, And, DoesNotExtends, IsNotNever } from '../../../utils';
import type { Constants, Guard, If, And, DoesNotExtends, IsNotNever, XOR } from '../../../utils';

import type { Params } from '..';

Expand Down Expand Up @@ -108,20 +108,16 @@ export type ObjectNotation<TSchemaUID extends UID.Schema> = [
Schema.Attribute.MorphTargets<Schema.AttributeByName<TSchemaUID, TKey>>,
UID.Schema
>
>
// TODO: V5: Remove root-level nested params for morph data structures and only allow fragments
| NestedParams<UID.Schema>;
>;
}
>,
// Loose fallback when registries are not extended
| { [TKey in string]?: boolean | NestedParams<UID.Schema> }
| {
[TKey in string]?:
| boolean
| Fragment<UID.Schema>
// TODO: V5: Remove root-level nested params for morph data structures and only allow fragments
| NestedParams<UID.Schema>;
}
{
[key: string]:
| boolean
// We can't have both populate fragments and nested params, hence the xor
| XOR<NestedParams<UID.Schema>, Fragment<UID.Schema>>;
}
>
: never;

Expand Down
22 changes: 9 additions & 13 deletions packages/core/types/src/modules/entity-service/params/populate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type * as Schema from '../../../schema';

import type * as UID from '../../../uid';
import type { Constants, Guard, If, And, DoesNotExtends, IsNotNever } from '../../../utils';
import type { Constants, Guard, If, And, DoesNotExtends, IsNotNever, XOR } from '../../../utils';

import type { Params } from '..';

Expand Down Expand Up @@ -60,7 +60,7 @@ type GetPopulatableKeysWithoutTarget<TSchemaUID extends UID.Schema> = Exclude<
* Fragment populate notation for polymorphic attributes
*/
export type Fragment<TMaybeTargets extends UID.Schema> = {
on?: { [TKey in TMaybeTargets]: boolean | NestedParams<TKey> };
on?: { [TKey in TMaybeTargets]?: boolean | NestedParams<TKey> };
};

type PopulateClause<
Expand Down Expand Up @@ -108,20 +108,16 @@ export type ObjectNotation<TSchemaUID extends UID.Schema> = [
Schema.Attribute.MorphTargets<Schema.AttributeByName<TSchemaUID, TKey>>,
UID.Schema
>
>
// TODO: V5: Remove root-level nested params for morph data structures and only allow fragments
| NestedParams<UID.Schema>;
>;
}
>,
// Loose fallback when registries are not extended
| { [key: string]: boolean | NestedParams<UID.Schema> }
| {
[key: string]:
| boolean
| Fragment<UID.Schema>
// TODO: V5: Remove root-level nested params for morph data structures and only allow fragments
| NestedParams<UID.Schema>;
}
{
[key: string]:
| boolean
// We can't have both populate fragments and nested params, hence the xor
| XOR<NestedParams<UID.Schema>, Fragment<UID.Schema>>;
}
>
: never;

Expand Down
68 changes: 32 additions & 36 deletions packages/core/utils/src/convert-query-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
isObject,
cloneDeep,
get,
mergeAll,
isArray,
isString,
} from 'lodash/fp';
Expand Down Expand Up @@ -341,10 +340,20 @@ const createTransformer = ({ getModel }: TransformerOptions) => {
}

const { attributes } = schema;

return Object.entries(populate).reduce((acc, [key, subPopulate]) => {
if (_.isString(subPopulate)) {
try {
const subPopulateAsBoolean = parseType({ type: 'boolean', value: subPopulate });
// Only true is accepted as a boolean populate value
return subPopulateAsBoolean === true ? { ...acc, [key]: true } : acc;
} catch {
// ignore
}
}

if (_.isBoolean(subPopulate)) {
return { ...acc, [key]: subPopulate };
// Only true is accepted as a boolean populate value
return subPopulate === true ? { ...acc, [key]: true } : acc;
}

const attribute = attributes[key];
Expand All @@ -357,42 +366,29 @@ const createTransformer = ({ getModel }: TransformerOptions) => {
const isAllowedAttributeForFragmentPopulate =
isDynamicZoneAttribute(attribute) || isMorphToRelationalAttribute(attribute);

if (isAllowedAttributeForFragmentPopulate && hasFragmentPopulateDefined(subPopulate)) {
return {
...acc,
[key]: {
on: Object.entries(subPopulate.on).reduce(
(acc, [type, typeSubPopulate]) => ({
...acc,
[type]: convertNestedPopulate(typeSubPopulate, getModel(type)),
}),
{}
),
},
};
}

// TODO: This is a query's populate fallback for DynamicZone and is kept for legacy purpose.
// Removing it could break existing user queries but it should be removed in V5.
if (isDynamicZoneAttribute(attribute)) {
const populates = attribute.components
.map((uid) => getModel(uid))
.map((schema) => convertNestedPopulate(subPopulate, schema))
.map((populate) => (populate === true ? {} : populate)) // cast boolean to empty object to avoid merging issues
.filter((populate) => populate !== false);

if (isEmpty(populates)) {
return acc;
if (isAllowedAttributeForFragmentPopulate) {
if (hasFragmentPopulateDefined(subPopulate)) {
Bassel17 marked this conversation as resolved.
Show resolved Hide resolved
// does it have 'on' AND no other property
return {
...acc,
[key]: {
on: Object.entries(subPopulate.on).reduce(
(acc, [type, typeSubPopulate]) => ({
...acc,
[type]: convertNestedPopulate(typeSubPopulate, getModel(type)),
}),
{}
),
},
};
}

return {
...acc,
[key]: mergeAll(populates),
};
throw new Error(
`Invalid nested populate. Expected a fragment ("on") but found ${JSON.stringify(subPopulate)}`
);
}

if (isMorphToRelationalAttribute(attribute)) {
return { ...acc, [key]: convertNestedPopulate(subPopulate, undefined) };
if (!isAllowedAttributeForFragmentPopulate && hasFragmentPopulateDefined(subPopulate)) {
throw new Error(`Using fragments is not permitted to populate "${key}" in "${schema.uid}"`);
}

// NOTE: Retrieve the target schema UID.
Expand Down
53 changes: 11 additions & 42 deletions packages/core/utils/src/traverse/query-populate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
cloneDeep,
join,
first,
omit,
merge,
} from 'lodash/fp';

import traverseFactory from './factory';
Expand Down Expand Up @@ -178,6 +176,8 @@ const populate = traverseFactory()
const newValue = await recurse(visitor, { schema, path, getModel }, { on: value?.on });

set(key, { on: newValue });

return;
}

const targetSchemaUID = attribute.target;
Expand Down Expand Up @@ -214,48 +214,17 @@ const populate = traverseFactory()
set(key, newValue);
})
// Handle populate on dynamic zones
.onDynamicZone(
async ({ key, value, attribute, schema, visitor, path, getModel }, { set, recurse }) => {
if (isNil(value)) {
return;
}

if (isObject(value)) {
const { components } = attribute;

const newValue = {};

// Handle legacy DZ params
let newProperties: unknown = omit('on', value);

for (const componentUID of components) {
const componentSchema = getModel(componentUID);

const properties = await recurse(
visitor,
{ schema: componentSchema, path, getModel },
value
);
newProperties = merge(newProperties, properties);
}

Object.assign(newValue, newProperties);

// Handle new morph fragment syntax
if ('on' in value && value.on) {
const newOn = await recurse(visitor, { schema, path, getModel }, { on: value.on });

// Recompose both syntaxes
Object.assign(newValue, newOn);
}
.onDynamicZone(async ({ key, value, schema, visitor, path, getModel }, { set, recurse }) => {
if (isNil(value) || !isObject(value)) {
return;
}

set(key, newValue);
} else {
const newValue = await recurse(visitor, { schema, path, getModel }, value);
// Handle fragment syntax
if ('on' in value && value.on) {
const newOn = await recurse(visitor, { schema, path, getModel }, { on: value.on });

set(key, newValue);
}
set(key, newOn);
}
);
});

export default curry(populate.traverse);
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,12 @@ describe('Populate filters', () => {
test('Populate every component in the dynamic zone', async () => {
const qs = {
populate: {
dz: '*',
dz: {
on: {
'default.foo': true,
'default.bar': true,
},
},
},
};

Expand Down
Loading
Loading