Skip to content

Commit 93f0a1d

Browse files
feat: add brand sentiment guidelines (#1297)
The Brand Sentiment Guidelines Store allows customers to define and configure topics and guidelines for sentiment analysis. Topics represent subjects of interest (e.g., "2026 Corvette Stingray", "BMW XM Latest") with associated sub-prompts, while Guidelines define analysis focus areas with specific audit type associations. Please ensure your pull request adheres to the following guidelines: - [ ] make sure to link the related issues in this description - [ ] when merging / squashing, make sure the fixed issue references are visible in the commits, for easy compilation of release notes ## Related Issues Thanks for contributing!
1 parent 5f09897 commit 93f0a1d

22 files changed

Lines changed: 2230 additions & 0 deletions

packages/spacecat-shared-data-access/src/models/base/entity.registry.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import ReportCollection from '../report/report.collection.js';
4242
import TrialUserCollection from '../trial-user/trial-user.collection.js';
4343
import TrialUserActivityCollection from '../trial-user-activity/trial-user-activity.collection.js';
4444
import PageCitabilityCollection from '../page-citability/page-citability.collection.js';
45+
import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guideline.collection.js';
46+
import SentimentTopicCollection from '../sentiment-topic/sentiment-topic.collection.js';
4547

4648
import ApiKeySchema from '../api-key/api-key.schema.js';
4749
import AsyncJobSchema from '../async-job/async-job.schema.js';
@@ -71,6 +73,8 @@ import ReportSchema from '../report/report.schema.js';
7173
import TrialUserSchema from '../trial-user/trial-user.schema.js';
7274
import TrialUserActivitySchema from '../trial-user-activity/trial-user-activity.schema.js';
7375
import PageCitabilitySchema from '../page-citability/page-citability.schema.js';
76+
import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline.schema.js';
77+
import SentimentTopicSchema from '../sentiment-topic/sentiment-topic.schema.js';
7478

7579
/**
7680
* EntityRegistry - A registry class responsible for managing entities, their schema and collection.
@@ -179,5 +183,7 @@ EntityRegistry.registerEntity(ReportSchema, ReportCollection);
179183
EntityRegistry.registerEntity(TrialUserSchema, TrialUserCollection);
180184
EntityRegistry.registerEntity(TrialUserActivitySchema, TrialUserActivityCollection);
181185
EntityRegistry.registerEntity(PageCitabilitySchema, PageCitabilityCollection);
186+
EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollection);
187+
EntityRegistry.registerEntity(SentimentTopicSchema, SentimentTopicCollection);
182188

183189
export default EntityRegistry;

packages/spacecat-shared-data-access/src/models/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ export type * from './suggestion';
3535
export type * from './report';
3636
export type * from './trial-user';
3737
export type * from './trial-user-activity';
38+
export type * from './sentiment-guideline';
39+
export type * from './sentiment-topic';

packages/spacecat-shared-data-access/src/models/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@ export * from './report/index.js';
4040
export * from './trial-user/index.js';
4141
export * from './trial-user-activity/index.js';
4242
export * from './page-citability/index.js';
43+
export * from './sentiment-guideline/index.js';
44+
export * from './sentiment-topic/index.js';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import type { BaseCollection, BaseModel, Site } from '../index';
14+
15+
/**
16+
* SentimentGuideline entity representing a guideline for sentiment analysis.
17+
* Composite primary key: siteId (PK) + guidelineId (SK)
18+
*/
19+
export interface SentimentGuideline extends BaseModel {
20+
getGuidelineId(): string;
21+
getName(): string;
22+
getInstruction(): string;
23+
getAudits(): string[];
24+
getEnabled(): boolean;
25+
getCreatedAt(): string;
26+
getCreatedBy(): string;
27+
getUpdatedAt(): string;
28+
getUpdatedBy(): string;
29+
getSite(): Promise<Site>;
30+
getSiteId(): string;
31+
32+
setName(name: string): SentimentGuideline;
33+
setInstruction(instruction: string): SentimentGuideline;
34+
setAudits(audits: string[]): SentimentGuideline;
35+
setEnabled(enabled: boolean): SentimentGuideline;
36+
setUpdatedBy(updatedBy: string): SentimentGuideline;
37+
38+
isEnabled(): boolean;
39+
isAuditEnabled(auditType: string): boolean;
40+
enableAudit(auditType: string): SentimentGuideline;
41+
disableAudit(auditType: string): SentimentGuideline;
42+
}
43+
44+
export interface SentimentGuidelineCollection extends BaseCollection<SentimentGuideline> {
45+
findById(siteId: string, guidelineId: string): Promise<SentimentGuideline | null>;
46+
allBySiteId(siteId: string, options?: { limit?: number; cursor?: string }): Promise<{ data: SentimentGuideline[]; cursor: string | null }>;
47+
allBySiteIdEnabled(siteId: string, options?: { limit?: number; cursor?: string }): Promise<{ data: SentimentGuideline[]; cursor: string | null }>;
48+
allBySiteIdAndAuditType(siteId: string, auditType: string, options?: { limit?: number; cursor?: string }): Promise<{ data: SentimentGuideline[]; cursor: string | null }>;
49+
findByIds(siteId: string, guidelineIds: string[]): Promise<SentimentGuideline[]>;
50+
removeForSiteId(siteId: string): Promise<void>;
51+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import SentimentGuideline from './sentiment-guideline.model.js';
14+
import SentimentGuidelineCollection from './sentiment-guideline.collection.js';
15+
16+
export {
17+
SentimentGuideline,
18+
SentimentGuidelineCollection,
19+
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { hasText } from '@adobe/spacecat-shared-utils';
14+
15+
import BaseCollection from '../base/base.collection.js';
16+
17+
/**
18+
* SentimentGuidelineCollection - A collection class for managing SentimentGuideline entities.
19+
* Extends BaseCollection to provide specific methods for sentiment guidelines.
20+
*
21+
* @class SentimentGuidelineCollection
22+
* @extends BaseCollection
23+
*/
24+
class SentimentGuidelineCollection extends BaseCollection {
25+
static COLLECTION_NAME = 'SentimentGuidelineCollection';
26+
27+
/**
28+
* Finds a sentiment guideline by its composite primary key (siteId + guidelineId).
29+
*
30+
* @param {string} siteId - The site ID (partition key).
31+
* @param {string} guidelineId - The guideline ID (sort key).
32+
* @returns {Promise<SentimentGuideline|null>} The found SentimentGuideline or null.
33+
*/
34+
async findById(siteId, guidelineId) {
35+
if (!hasText(siteId) || !hasText(guidelineId)) {
36+
throw new Error('Both siteId and guidelineId are required');
37+
}
38+
39+
return this.findByIndexKeys({ siteId, guidelineId });
40+
}
41+
42+
/**
43+
* Gets all sentiment guidelines for a site with pagination.
44+
*
45+
* @param {string} siteId - The site ID.
46+
* @param {object} [options={}] - Query options (limit, cursor).
47+
* @returns {Promise<{data: SentimentGuideline[], cursor: string|null}>} Paginated results.
48+
*/
49+
async allBySiteId(siteId, options = {}) {
50+
if (!hasText(siteId)) {
51+
throw new Error('SiteId is required');
52+
}
53+
54+
const result = await this.allByIndexKeys(
55+
{ siteId },
56+
{ ...options, returnCursor: true },
57+
);
58+
59+
return {
60+
data: result.data || [],
61+
cursor: result.cursor,
62+
};
63+
}
64+
65+
/**
66+
* Gets all enabled sentiment guidelines for a site.
67+
*
68+
* @param {string} siteId - The site ID.
69+
* @param {object} [options={}] - Query options (limit, cursor).
70+
* @returns {Promise<{data: SentimentGuideline[], cursor: string|null}>} Paginated results.
71+
*/
72+
async allBySiteIdEnabled(siteId, options = {}) {
73+
if (!hasText(siteId)) {
74+
throw new Error('SiteId is required');
75+
}
76+
77+
const result = await this.allByIndexKeys(
78+
{ siteId },
79+
{
80+
...options,
81+
returnCursor: true,
82+
where: (attr, op) => op.eq(attr.enabled, true),
83+
},
84+
);
85+
86+
return {
87+
data: result.data || [],
88+
cursor: result.cursor,
89+
};
90+
}
91+
92+
/**
93+
* Gets all sentiment guidelines for a site that have a specific audit type enabled.
94+
* Uses FilterExpression to filter at the database level.
95+
*
96+
* @param {string} siteId - The site ID.
97+
* @param {string} auditType - The audit type to filter by.
98+
* @param {object} [options={}] - Query options (limit, cursor).
99+
* @returns {Promise<{data: SentimentGuideline[], cursor: string|null}>} Paginated results.
100+
*/
101+
async allBySiteIdAndAuditType(siteId, auditType, options = {}) {
102+
if (!hasText(siteId) || !hasText(auditType)) {
103+
throw new Error('Both siteId and auditType are required');
104+
}
105+
106+
const result = await this.allByIndexKeys(
107+
{ siteId },
108+
{
109+
...options,
110+
returnCursor: true,
111+
where: (attr, op) => op.contains(attr.audits, auditType),
112+
},
113+
);
114+
115+
return {
116+
data: result.data || [],
117+
cursor: result.cursor,
118+
};
119+
}
120+
121+
/**
122+
* Finds multiple guidelines by their IDs using batch get.
123+
* Useful for resolving guidelineIds from a SentimentTopic.
124+
*
125+
* @param {string} siteId - The site ID.
126+
* @param {string[]} guidelineIds - Array of guideline IDs to fetch.
127+
* @returns {Promise<SentimentGuideline[]>} Array of found guidelines.
128+
*/
129+
async findByIds(siteId, guidelineIds) {
130+
if (!hasText(siteId)) {
131+
throw new Error('SiteId is required');
132+
}
133+
134+
if (!Array.isArray(guidelineIds) || guidelineIds.length === 0) {
135+
return [];
136+
}
137+
138+
// Fetch all guidelines for the site and filter
139+
// Note: For large datasets, consider implementing batch get
140+
const result = await this.allBySiteId(siteId);
141+
const allGuidelines = result.data || [];
142+
const guidelineIdSet = new Set(guidelineIds);
143+
144+
return allGuidelines.filter((guideline) => {
145+
const id = guideline.getGuidelineId?.() ?? guideline.guidelineId;
146+
return guidelineIdSet.has(id);
147+
});
148+
}
149+
150+
/**
151+
* Removes all sentiment guidelines for a specific site.
152+
*
153+
* @param {string} siteId - The site ID.
154+
* @returns {Promise<void>}
155+
*/
156+
async removeForSiteId(siteId) {
157+
if (!hasText(siteId)) {
158+
throw new Error('SiteId is required');
159+
}
160+
161+
const result = await this.allBySiteId(siteId);
162+
const guidelinesToRemove = result.data || [];
163+
164+
if (guidelinesToRemove.length > 0) {
165+
const keysToRemove = guidelinesToRemove.map((guideline) => ({
166+
siteId,
167+
guidelineId: guideline.getGuidelineId?.() ?? guideline.guidelineId,
168+
}));
169+
await this.removeByIndexKeys(keysToRemove);
170+
}
171+
}
172+
}
173+
174+
export default SentimentGuidelineCollection;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import BaseModel from '../base/base.model.js';
14+
15+
/**
16+
* SentimentGuideline - A class representing a sentiment analysis guideline.
17+
* Guidelines define how to analyze topics (e.g., "Focus on product quality").
18+
*
19+
* @class SentimentGuideline
20+
* @extends BaseModel
21+
*/
22+
class SentimentGuideline extends BaseModel {
23+
static ENTITY_NAME = 'SentimentGuideline';
24+
25+
/**
26+
* Checks if this guideline is currently enabled.
27+
* @returns {boolean} True if the guideline is enabled.
28+
*/
29+
isEnabled() {
30+
return this.getEnabled?.() ?? this.enabled ?? true;
31+
}
32+
33+
/**
34+
* Checks if this guideline is enabled for a specific audit type.
35+
* @param {string} auditType - The audit type to check.
36+
* @returns {boolean} True if the audit is enabled for this guideline.
37+
*/
38+
isAuditEnabled(auditType) {
39+
const audits = this.getAudits?.() ?? this.audits ?? [];
40+
return audits.includes(auditType);
41+
}
42+
43+
/**
44+
* Adds an audit type to the audits array if not already present.
45+
* @param {string} auditType - The audit type to add.
46+
* @returns {this} The current instance for chaining.
47+
*/
48+
enableAudit(auditType) {
49+
const audits = this.getAudits?.() ?? this.audits ?? [];
50+
if (!audits.includes(auditType)) {
51+
const updatedAudits = [...audits, auditType];
52+
if (this.setAudits) {
53+
this.setAudits(updatedAudits);
54+
} else {
55+
this.audits = updatedAudits;
56+
}
57+
}
58+
return this;
59+
}
60+
61+
/**
62+
* Removes an audit type from the audits array.
63+
* @param {string} auditType - The audit type to remove.
64+
* @returns {this} The current instance for chaining.
65+
*/
66+
disableAudit(auditType) {
67+
const audits = this.getAudits?.() ?? this.audits ?? [];
68+
const filtered = audits.filter((a) => a !== auditType);
69+
if (this.setAudits) {
70+
this.setAudits(filtered);
71+
} else {
72+
this.audits = filtered;
73+
}
74+
return this;
75+
}
76+
77+
/**
78+
* Generates the composite keys for remove/update operations.
79+
* Required for entities with composite primary keys.
80+
* @returns {Object} - The composite keys (siteId + guidelineId).
81+
*/
82+
generateCompositeKeys() {
83+
return {
84+
siteId: this.getSiteId(),
85+
guidelineId: this.getGuidelineId(),
86+
};
87+
}
88+
}
89+
90+
export default SentimentGuideline;

0 commit comments

Comments
 (0)