Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/core/e2e/entity-event-update-state.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AdministratorEvent,
CollectionEvent,
defaultShippingCalculator,
defaultShippingEligibilityChecker,
dummyPaymentHandler,
Expand All @@ -22,10 +23,12 @@ import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-conf

import {
createAdministratorDocument,
createCollectionDocument,
createPromotionDocument,
createRoleDocument,
createShippingMethodDocument,
updateAdministratorDocument,
updateCollectionDocument,
updatePromotionDocument,
updateShippingMethodDocument,
} from './graphql/shared-definitions';
Expand Down Expand Up @@ -364,6 +367,58 @@ describe('Entity event updated state', () => {
expect(event.entity.enabled).toBe(false);
});
});

describe('CollectionEvent', () => {
let collectionId: string;

it('setup: create collection', async () => {
const { createCollection } = await adminClient.query(createCollectionDocument, {
input: {
filters: [],
translations: [
{
languageCode: LanguageCode.en,
name: 'Event Test Collection',
description: '',
slug: 'event-test-collection',
},
],
},
});
collectionId = createCollection.id;
expect(collectionId).toBeDefined();
});

it('exposes the previous entity state on update', async () => {
const eventPromise = firstValueFrom(eventBus.ofType(CollectionEvent));

await adminClient.query(updateCollectionDocument, {
input: {
id: collectionId,
translations: [
{
languageCode: LanguageCode.en,
name: 'Updated Collection Name',
slug: 'event-test-collection',
},
],
},
});

const event = await eventPromise;

expect(event.type).toBe('updated');
const current = event.entity.translations.find(t => t.languageCode === LanguageCode.en);
expect(current?.name).toBe('Updated Collection Name');
// previousEntity carries the pre-update state (#4402)
const previous = event.previousEntity?.translations.find(t => t.languageCode === LanguageCode.en);
expect(previous?.name).toBe('Event Test Collection');
// previousEntity is loaded with its relation set (not just translations), so a
// non-translation relation must be present too — guards against silently dropping one.
expect(event.previousEntity?.channels).toBeDefined();
expect(event.previousEntity?.parent).toBeDefined();
});
});
});

const PAYMENT_METHOD_FRAGMENT = gql`
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/event-bus/events/collection-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export class CollectionEvent extends VendureEntityEvent<Collection, CollectionIn
entity: Collection,
type: 'created' | 'updated' | 'deleted',
input?: CollectionInputTypes,
/**
* @description
* The state of the Collection prior to the update, populated for `updated` events so that
* subscribers can diff against the previous values. It is loaded with the `featuredAsset`,
* `assets`, `channels`, `parent` and `translations` relations. Note this relation set is not
* identical to that of `entity` (the post-update collection), so reliable diffing is limited
* to `translations`, `featuredAsset`, `assets` and scalar columns.
*
* @since 3.8.0
*/
public readonly previousEntity?: Collection,
) {
super(entity, type, ctx, input);
}
Expand Down
16 changes: 12 additions & 4 deletions packages/core/src/service/services/collection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,16 @@ export class CollectionService implements OnModuleInit {
}

async update(ctx: RequestContext, input: UpdateCollectionInput): Promise<Translated<Collection>> {
// Ensure the entity belongs to the active channel before updating.
await this.connection.getEntityOrThrow(ctx, Collection, input.id, { channelId: ctx.channelId });
// Load the collection as it exists before the update. This both ensures the collection
// belongs to the active channel and captures its previous state, exposed as `previousEntity`
// on the CollectionEvent below so subscribers can diff against the pre-update values.
const previousEntity = await this.connection.getEntityOrThrow(ctx, Collection, input.id, {
channelId: ctx.channelId,
relations: ['featuredAsset', 'assets', 'channels', 'parent', 'translations'],
});
await this.slugValidator.validateSlugs(ctx, input, CollectionTranslation);
const collection = await this.translatableSaver.update({

const collection = await this.translatableSaver.update<Collection>({
ctx,
input,
entityType: Collection,
Expand All @@ -585,7 +591,9 @@ export class CollectionService implements OnModuleInit {
const affectedVariantIds = await this.getCollectionProductVariantIds(collection);
await this.eventBus.publish(new CollectionModificationEvent(ctx, collection, affectedVariantIds));
}
await this.eventBus.publish(new CollectionEvent(ctx, collection, 'updated', input));
await this.eventBus.publish(
new CollectionEvent(ctx, collection, 'updated', input, previousEntity),
);
return assertFound(this.findOne(ctx, collection.id));
}

Expand Down
Loading