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,9 +1,14 @@
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 './utils/dimensions';

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

/**
* Create a new document.
*
Expand All @@ -12,7 +17,7 @@ import { getDocumentLocaleAndStatus } from './utils/dimensions';
* @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 @@ -54,7 +59,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,9 +1,11 @@
import type { UID } from '@strapi/types';
import type { UID, Modules } from '@strapi/types';
import { setCreatorFields, async, errors } from '@strapi/utils';

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

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 @@ -24,7 +26,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
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
60 changes: 28 additions & 32 deletions packages/core/utils/src/convert-query-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,15 @@ 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 });
return { ...acc, [key]: subPopulateAsBoolean };
} catch {
// ignore
}
}

Bassel17 marked this conversation as resolved.
Show resolved Hide resolved
if (_.isBoolean(subPopulate)) {
return { ...acc, [key]: subPopulate };
}
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
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
18 changes: 0 additions & 18 deletions packages/core/utils/src/traverse/query-populate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,26 +221,8 @@ const populate = traverseFactory()
}

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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ describe('Core API - Validate', () => {
[{ dz: { on: { 'default.component-a': true } } }, 'default.component-a', 2],
[{ dz: { on: { 'default.component-b': true } } }, 'default.component-b', 1],
])(
'Populates a dynamic-use using populate fragments (%s)',
'Populates a dynamic-zone using populate fragments (%s)',
async (populate, componentUID, expectedLength) => {
const res = await rq.get('/api/documents', { qs: { populate } });

Expand Down