mirror of
https://github.com/strapi/strapi.git
synced 2025-09-22 14:59:07 +00:00
fix: syncing components in draft and publish relations (#24227)
Co-authored-by: Mark Kaylor <mark.kaylor@strapi.io> Co-authored-by: Ben Irvin <ben.irvin@strapi.io> Co-authored-by: Jamie Howard <48524071+jhoward1994@users.noreply.github.com>
This commit is contained in:
parent
80e8a7c212
commit
a1da9b829e
@ -2,6 +2,12 @@ import _ from 'lodash';
|
|||||||
import { has, omit, pipe, assign, curry } from 'lodash/fp';
|
import { has, omit, pipe, assign, curry } from 'lodash/fp';
|
||||||
import type { Utils, UID, Schema, Data, Modules } from '@strapi/types';
|
import type { Utils, UID, Schema, Data, Modules } from '@strapi/types';
|
||||||
import { contentTypes as contentTypesUtils, async, errors } from '@strapi/utils';
|
import { contentTypes as contentTypesUtils, async, errors } from '@strapi/utils';
|
||||||
|
import {
|
||||||
|
getComponentJoinTableName,
|
||||||
|
getComponentJoinColumnEntityName,
|
||||||
|
getComponentJoinColumnInverseName,
|
||||||
|
getComponentTypeColumn,
|
||||||
|
} from '../../utils/transform-content-types-to-models';
|
||||||
|
|
||||||
// type aliases for readability
|
// type aliases for readability
|
||||||
type Input<T extends UID.Schema> = Modules.Documents.Params.Data.Input<T>;
|
type Input<T extends UID.Schema> = Modules.Documents.Params.Data.Input<T>;
|
||||||
@ -431,6 +437,158 @@ const assignComponentData = curry(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** *************************
|
||||||
|
Component relation handling for document operations
|
||||||
|
************************** */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the parent entry of a component instance.
|
||||||
|
*
|
||||||
|
* Given a component model, a specific component instance id, and the list of
|
||||||
|
* possible parent content types (those that can embed this component),
|
||||||
|
* this function checks each parent's *_cmps join table to see if the component
|
||||||
|
* instance is linked to a parent entity.
|
||||||
|
*
|
||||||
|
* - Returns the parent uid, parent table name, and parent id if found.
|
||||||
|
* - Returns null if no parent relationship exists.
|
||||||
|
*/
|
||||||
|
const findComponentParent = async (
|
||||||
|
componentSchema: Schema.Component,
|
||||||
|
componentId: number | string,
|
||||||
|
parentSchemasForComponent: Schema.ContentType[],
|
||||||
|
opts?: { trx?: any }
|
||||||
|
): Promise<{ uid: string; table: string; parentId: number | string } | null> => {
|
||||||
|
if (!componentSchema?.uid) return null;
|
||||||
|
|
||||||
|
const schemaBuilder = strapi.db.getSchemaConnection(opts?.trx);
|
||||||
|
const withTrx = (qb: any) => (opts?.trx ? qb.transacting(opts.trx) : qb);
|
||||||
|
|
||||||
|
for (const parent of parentSchemasForComponent) {
|
||||||
|
if (!parent.collectionName) continue;
|
||||||
|
|
||||||
|
// Use the exact same functions that create the tables
|
||||||
|
const identifiers = strapi.db.metadata.identifiers;
|
||||||
|
const joinTableName = getComponentJoinTableName(parent.collectionName, identifiers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableExists = await schemaBuilder.hasTable(joinTableName);
|
||||||
|
if (!tableExists) continue;
|
||||||
|
|
||||||
|
// Use the exact same functions that create the columns
|
||||||
|
const entityIdColumn = getComponentJoinColumnEntityName(identifiers);
|
||||||
|
const componentIdColumn = getComponentJoinColumnInverseName(identifiers);
|
||||||
|
const componentTypeColumn = getComponentTypeColumn(identifiers);
|
||||||
|
|
||||||
|
const parentRow = await withTrx(strapi.db.getConnection(joinTableName))
|
||||||
|
.where({
|
||||||
|
[componentIdColumn]: componentId,
|
||||||
|
[componentTypeColumn]: componentSchema.uid,
|
||||||
|
})
|
||||||
|
.first(entityIdColumn);
|
||||||
|
|
||||||
|
if (parentRow) {
|
||||||
|
return {
|
||||||
|
uid: parent.uid,
|
||||||
|
table: parent.collectionName,
|
||||||
|
parentId: parentRow[entityIdColumn],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds content types that contain the given component and have draft & publish enabled.
|
||||||
|
*/
|
||||||
|
const getParentSchemasForComponent = (componentSchema: Schema.Component): Schema.ContentType[] => {
|
||||||
|
return Object.values(strapi.contentTypes).filter((contentType: any) => {
|
||||||
|
if (!contentType.options?.draftAndPublish) return false;
|
||||||
|
|
||||||
|
return Object.values(contentType.attributes).some((attr: any) => {
|
||||||
|
return (
|
||||||
|
(attr.type === 'component' && attr.component === componentSchema.uid) ||
|
||||||
|
(attr.type === 'dynamiczone' && attr.components?.includes(componentSchema.uid))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a component relation should be propagated to a new document version
|
||||||
|
* when a document with draft and publish is updated.
|
||||||
|
*/
|
||||||
|
const shouldPropagateComponentRelationToNewVersion = async (
|
||||||
|
componentRelation: Record<string, any>,
|
||||||
|
componentSchema: Schema.Component,
|
||||||
|
parentSchemasForComponent: Schema.ContentType[],
|
||||||
|
trx: any
|
||||||
|
): Promise<boolean> => {
|
||||||
|
// Get the component ID column name using the actual component model name
|
||||||
|
const componentIdColumn = strapi.db.metadata.identifiers.getJoinColumnAttributeIdName(
|
||||||
|
_.snakeCase(componentSchema.modelName)
|
||||||
|
);
|
||||||
|
|
||||||
|
const componentId = componentRelation[componentIdColumn];
|
||||||
|
|
||||||
|
const parent = await findComponentParent(
|
||||||
|
componentSchema,
|
||||||
|
componentId,
|
||||||
|
parentSchemasForComponent,
|
||||||
|
{ trx }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep relation if component has no parent entry
|
||||||
|
if (!parent?.uid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentContentType = strapi.contentTypes[parent.uid as UID.ContentType];
|
||||||
|
|
||||||
|
// Keep relation if parent doesn't have draft & publish enabled
|
||||||
|
if (!parentContentType?.options?.draftAndPublish) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard relation if parent has draft & publish enabled
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a filter function for component relations that can be passed to the generic
|
||||||
|
* unidirectional relations utility
|
||||||
|
*/
|
||||||
|
const createComponentRelationFilter = () => {
|
||||||
|
return async (
|
||||||
|
relation: Record<string, any>,
|
||||||
|
model: Schema.Component | Schema.ContentType,
|
||||||
|
trx: any
|
||||||
|
): Promise<boolean> => {
|
||||||
|
// Only apply component-specific filtering for components
|
||||||
|
if (model.modelType !== 'component') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentSchema = model as Schema.Component;
|
||||||
|
const parentSchemas = getParentSchemasForComponent(componentSchema);
|
||||||
|
|
||||||
|
// Exit if no draft & publish parent types exist
|
||||||
|
if (parentSchemas.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldPropagateComponentRelationToNewVersion(
|
||||||
|
relation,
|
||||||
|
componentSchema,
|
||||||
|
parentSchemas,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
omitComponentData,
|
omitComponentData,
|
||||||
assignComponentData,
|
assignComponentData,
|
||||||
@ -439,4 +597,5 @@ export {
|
|||||||
updateComponents,
|
updateComponents,
|
||||||
deleteComponents,
|
deleteComponents,
|
||||||
deleteComponent,
|
deleteComponent,
|
||||||
|
createComponentRelationFilter,
|
||||||
};
|
};
|
||||||
|
@ -309,10 +309,16 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Load any unidirectional relation targetting the old published entries
|
// Load any unidirectional relation targetting the old published entries
|
||||||
const relationsToSync = await unidirectionalRelations.load(uid, {
|
const relationsToSync = await unidirectionalRelations.load(
|
||||||
newVersions: draftsToPublish,
|
uid,
|
||||||
oldVersions: oldPublishedVersions,
|
{
|
||||||
});
|
newVersions: draftsToPublish,
|
||||||
|
oldVersions: oldPublishedVersions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldPropagateRelation: components.createComponentRelationFilter(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const bidirectionalRelationsToSync = await bidirectionalRelations.load(uid, {
|
const bidirectionalRelationsToSync = await bidirectionalRelations.load(uid, {
|
||||||
newVersions: draftsToPublish,
|
newVersions: draftsToPublish,
|
||||||
@ -399,10 +405,16 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Load any unidirectional relation targeting the old drafts
|
// Load any unidirectional relation targeting the old drafts
|
||||||
const relationsToSync = await unidirectionalRelations.load(uid, {
|
const relationsToSync = await unidirectionalRelations.load(
|
||||||
newVersions: versionsToDraft,
|
uid,
|
||||||
oldVersions: oldDrafts,
|
{
|
||||||
});
|
newVersions: versionsToDraft,
|
||||||
|
oldVersions: oldDrafts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldPropagateRelation: components.createComponentRelationFilter(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const bidirectionalRelationsToSync = await bidirectionalRelations.load(uid, {
|
const bidirectionalRelationsToSync = await bidirectionalRelations.load(uid, {
|
||||||
newVersions: versionsToDraft,
|
newVersions: versionsToDraft,
|
||||||
|
@ -3,18 +3,41 @@ import { keyBy, omit } from 'lodash/fp';
|
|||||||
|
|
||||||
import type { UID, Schema } from '@strapi/types';
|
import type { UID, Schema } from '@strapi/types';
|
||||||
|
|
||||||
|
import type { JoinTable } from '@strapi/database';
|
||||||
|
|
||||||
interface LoadContext {
|
interface LoadContext {
|
||||||
oldVersions: { id: string; locale: string }[];
|
oldVersions: { id: string; locale: string }[];
|
||||||
newVersions: { id: string; locale: string }[];
|
newVersions: { id: string; locale: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RelationUpdate {
|
||||||
|
joinTable: JoinTable;
|
||||||
|
relations: Record<string, any>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelationFilterOptions {
|
||||||
|
/**
|
||||||
|
* Function to determine if a relation should be propagated to new document versions
|
||||||
|
* This replaces the hardcoded component-specific logic
|
||||||
|
*/
|
||||||
|
shouldPropagateRelation?: (
|
||||||
|
relation: Record<string, any>,
|
||||||
|
model: Schema.Component | Schema.ContentType,
|
||||||
|
trx: any
|
||||||
|
) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads lingering relations that need to be updated when overriding a published or draft entry.
|
* Loads lingering relations that need to be updated when overriding a published or draft entry.
|
||||||
* This is necessary because the relations are uni-directional and the target entry is not aware of the source entry.
|
* This is necessary because the relations are uni-directional and the target entry is not aware of the source entry.
|
||||||
* This is not the case for bi-directional relations, where the target entry is also linked to the source entry.
|
* This is not the case for bi-directional relations, where the target entry is also linked to the source entry.
|
||||||
*/
|
*/
|
||||||
const load = async (uid: UID.ContentType, { oldVersions, newVersions }: LoadContext) => {
|
const load = async (
|
||||||
const updates = [] as any;
|
uid: UID.ContentType,
|
||||||
|
{ oldVersions, newVersions }: LoadContext,
|
||||||
|
options: RelationFilterOptions = {}
|
||||||
|
): Promise<RelationUpdate[]> => {
|
||||||
|
const updates: RelationUpdate[] = [];
|
||||||
|
|
||||||
// Iterate all components and content types to find relations that need to be updated
|
// Iterate all components and content types to find relations that need to be updated
|
||||||
await strapi.db.transaction(async ({ trx }) => {
|
await strapi.db.transaction(async ({ trx }) => {
|
||||||
@ -74,10 +97,10 @@ const load = async (uid: UID.ContentType, { oldVersions, newVersions }: LoadCont
|
|||||||
* if published version link exists & not draft version link
|
* if published version link exists & not draft version link
|
||||||
* create link to new draft version
|
* create link to new draft version
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (!model.options?.draftAndPublish) {
|
if (!model.options?.draftAndPublish) {
|
||||||
const ids = newVersions.map((entry) => entry.id);
|
const ids = newVersions.map((entry) => entry.id);
|
||||||
|
|
||||||
|
// This is the step were we query the join table based on the id of the document
|
||||||
const newVersionsRelations = await strapi.db
|
const newVersionsRelations = await strapi.db
|
||||||
.getConnection()
|
.getConnection()
|
||||||
.select('*')
|
.select('*')
|
||||||
@ -85,19 +108,30 @@ const load = async (uid: UID.ContentType, { oldVersions, newVersions }: LoadCont
|
|||||||
.whereIn(targetColumnName, ids)
|
.whereIn(targetColumnName, ids)
|
||||||
.transacting(trx);
|
.transacting(trx);
|
||||||
|
|
||||||
if (newVersionsRelations.length > 0) {
|
let versionRelations = newVersionsRelations;
|
||||||
|
if (options.shouldPropagateRelation) {
|
||||||
|
const relationsToPropagate = [];
|
||||||
|
for (const relation of newVersionsRelations) {
|
||||||
|
if (await options.shouldPropagateRelation(relation, model, trx)) {
|
||||||
|
relationsToPropagate.push(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
versionRelations = relationsToPropagate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionRelations.length > 0) {
|
||||||
// when publishing a draft that doesn't have a published version yet,
|
// when publishing a draft that doesn't have a published version yet,
|
||||||
// copy the links to the draft over to the published version
|
// copy the links to the draft over to the published version
|
||||||
// when discarding a published version, if no drafts exists
|
// when discarding a published version, if no drafts exists
|
||||||
const discardToAdd = newVersionsRelations
|
const discardToAdd = versionRelations
|
||||||
.filter((relation) => {
|
.filter((relation) => {
|
||||||
const matchingOldVerion = oldVersionsRelations.find((oldRelation) => {
|
const matchingOldVersion = oldVersionsRelations.find((oldRelation) => {
|
||||||
return oldRelation[sourceColumnName] === relation[sourceColumnName];
|
return oldRelation[sourceColumnName] === relation[sourceColumnName];
|
||||||
});
|
});
|
||||||
|
|
||||||
return !matchingOldVerion;
|
return !matchingOldVersion;
|
||||||
})
|
})
|
||||||
.map(omit('id'));
|
.map(omit(strapi.db.metadata.identifiers.ID_COLUMN));
|
||||||
|
|
||||||
updates.push({ joinTable, relations: discardToAdd });
|
updates.push({ joinTable, relations: discardToAdd });
|
||||||
}
|
}
|
||||||
@ -112,6 +146,10 @@ const load = async (uid: UID.ContentType, { oldVersions, newVersions }: LoadCont
|
|||||||
/**
|
/**
|
||||||
* Updates uni directional relations to target the right entries when overriding published or draft entries.
|
* Updates uni directional relations to target the right entries when overriding published or draft entries.
|
||||||
*
|
*
|
||||||
|
* This function:
|
||||||
|
* 1. Creates new relations pointing to the new entry versions
|
||||||
|
* 2. Precisely deletes only the old relations being replaced to prevent orphaned links
|
||||||
|
*
|
||||||
* @param oldEntries The old entries that are being overridden
|
* @param oldEntries The old entries that are being overridden
|
||||||
* @param newEntries The new entries that are overriding the old ones
|
* @param newEntries The new entries that are overriding the old ones
|
||||||
* @param oldRelations The relations that were previously loaded with `load` @see load
|
* @param oldRelations The relations that were previously loaded with `load` @see load
|
||||||
@ -155,3 +193,4 @@ const sync = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { load, sync };
|
export { load, sync };
|
||||||
|
export type { RelationFilterOptions };
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { Knex } from 'knex';
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { Dialect, getDialect } from './dialects';
|
import { Dialect, getDialect } from './dialects';
|
||||||
import { createSchemaProvider, SchemaProvider } from './schema';
|
import { createSchemaProvider, SchemaProvider } from './schema';
|
||||||
import { createMetadata, Metadata } from './metadata';
|
import { createMetadata, Metadata } from './metadata';
|
||||||
@ -11,7 +12,7 @@ import { createConnection } from './connection';
|
|||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
import { Callback, transactionCtx, TransactionObject } from './transaction-context';
|
import { Callback, transactionCtx, TransactionObject } from './transaction-context';
|
||||||
import { validateDatabase } from './validations';
|
import { validateDatabase } from './validations';
|
||||||
import type { Model } from './types';
|
import type { Model, JoinTable } from './types';
|
||||||
import type { Identifiers } from './utils/identifiers';
|
import type { Identifiers } from './utils/identifiers';
|
||||||
import { createRepairManager, type RepairManager } from './repairs';
|
import { createRepairManager, type RepairManager } from './repairs';
|
||||||
|
|
||||||
@ -263,4 +264,4 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { Database, errors };
|
export { Database, errors };
|
||||||
export type { Model, Identifiers, Migration };
|
export type { Model, JoinTable, Identifiers, Migration };
|
||||||
|
@ -24,17 +24,24 @@ const populate = {
|
|||||||
compo: {
|
compo: {
|
||||||
populate: {
|
populate: {
|
||||||
tag: true,
|
tag: true,
|
||||||
|
tags: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const componentModel = {
|
const componentModel = {
|
||||||
|
collectionName: 'components_compo',
|
||||||
attributes: {
|
attributes: {
|
||||||
tag: {
|
tag: {
|
||||||
type: 'relation',
|
type: 'relation',
|
||||||
relation: 'oneToOne',
|
relation: 'oneToOne',
|
||||||
target: TAG_UID,
|
target: TAG_UID,
|
||||||
},
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'relation',
|
||||||
|
relation: 'oneToMany',
|
||||||
|
target: TAG_UID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
displayName: 'compo',
|
displayName: 'compo',
|
||||||
};
|
};
|
||||||
@ -164,4 +171,114 @@ describe('Document Service unidirectional relations', () => {
|
|||||||
compo: { tag: { id: tag1Id } },
|
compo: { tag: { id: tag1Id } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should not create orphaned relations for a draft and publish content-type when updating from the parent side', async () => {
|
||||||
|
const joinTableName = 'components_default_compos_tags_lnk';
|
||||||
|
|
||||||
|
// Step 1: Create Product with component tag relation (draft)
|
||||||
|
const testProduct = await strapi.documents(PRODUCT_UID).create({
|
||||||
|
data: {
|
||||||
|
name: 'GhostRelationBugTest',
|
||||||
|
compo: {
|
||||||
|
tags: [{ documentId: 'Tag3' }], // Component relation to Tag (still in draft)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check join table after step 1
|
||||||
|
let result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
let joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
|
||||||
|
// 1 entry is created for draft to draft
|
||||||
|
expect(joinTableRows.length).toBe(1);
|
||||||
|
|
||||||
|
// Step 2: Publish Tag FIRST - this triggers ghost relation creation
|
||||||
|
await strapi.documents(TAG_UID).publish({ documentId: 'Tag3' });
|
||||||
|
|
||||||
|
// Check join table after step 2
|
||||||
|
result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
|
||||||
|
// No new entry should be created in the join table
|
||||||
|
expect(joinTableRows.length).toBe(1);
|
||||||
|
|
||||||
|
// Step 3: Publish Product - creates published component version
|
||||||
|
await strapi.documents(PRODUCT_UID).publish({ documentId: testProduct.documentId });
|
||||||
|
|
||||||
|
// Check join table after step 3
|
||||||
|
result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
|
||||||
|
// 1 entry should be created (2 total) in the join table for published to published
|
||||||
|
expect(joinTableRows.length).toBe(2);
|
||||||
|
|
||||||
|
// Cleanup - Delete the entry
|
||||||
|
await strapi.documents(PRODUCT_UID).delete({ documentId: testProduct.documentId });
|
||||||
|
|
||||||
|
result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
expect(joinTableRows.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not create orphaned relations for a draft and publish content-type when updating from the relation side', async () => {
|
||||||
|
const joinTableName = 'components_default_compos_tags_lnk';
|
||||||
|
|
||||||
|
// Step 1: Create and publish a tag
|
||||||
|
await strapi.documents(TAG_UID).create({
|
||||||
|
data: {
|
||||||
|
name: 'Tag4',
|
||||||
|
documentId: 'Tag4',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const tag = await strapi.documents(TAG_UID).publish({ documentId: 'Tag4' });
|
||||||
|
const tagId = tag.entries[0].id;
|
||||||
|
|
||||||
|
// Step 2: Create Product with component tag relation (published)
|
||||||
|
const testProduct = await strapi.documents(PRODUCT_UID).create({
|
||||||
|
data: {
|
||||||
|
name: 'GhostRelationBugTest',
|
||||||
|
compo: {
|
||||||
|
tags: [{ id: tagId }], // Component relation to Tag (published)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Poublish the product
|
||||||
|
await strapi.documents(PRODUCT_UID).publish({ documentId: testProduct.documentId });
|
||||||
|
|
||||||
|
// Check join table after step 1
|
||||||
|
let result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
let joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
|
||||||
|
// Expect 2 entries (draft to draft, published to published)
|
||||||
|
expect(joinTableRows.length).toBe(2);
|
||||||
|
|
||||||
|
// Step 4: Update the tag and publish
|
||||||
|
await strapi.documents(TAG_UID).update({ documentId: 'Tag4', name: 'Tag4 update' });
|
||||||
|
await strapi.documents(TAG_UID).publish({ documentId: 'Tag4' });
|
||||||
|
|
||||||
|
// Check join table after step 4
|
||||||
|
result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
|
||||||
|
// No new entry should be created in the join table
|
||||||
|
expect(joinTableRows.length).toBe(2);
|
||||||
|
|
||||||
|
// Step 5: Republish the parent
|
||||||
|
await strapi.documents(PRODUCT_UID).publish({ documentId: testProduct.documentId });
|
||||||
|
|
||||||
|
// Check join table after step 5
|
||||||
|
result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
|
||||||
|
// No new entry should be created in the join table
|
||||||
|
expect(joinTableRows.length).toBe(2);
|
||||||
|
|
||||||
|
// Cleanup - Delete the entry
|
||||||
|
await strapi.documents(PRODUCT_UID).delete({ documentId: testProduct.documentId });
|
||||||
|
|
||||||
|
result = await strapi.db.connection.raw(`SELECT * FROM ${joinTableName}`);
|
||||||
|
joinTableRows = Array.isArray(result) ? result : result.rows || result;
|
||||||
|
expect(joinTableRows.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user