Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
5503d48
feat(data-access): add SentimentTopic and SentimentGuideline entities
HollywoodTonight Jan 21, 2026
75a36ea
feat(sentiment-topic): remove guidelineIds field - topics and guideli…
HollywoodTonight Jan 22, 2026
8ebd0e0
feat(data-access): move audits from SentimentTopic to SentimentGuideline
HollywoodTonight Jan 28, 2026
274e8df
refactor: rename allBySiteIdPaginated to allBySiteId for sentiment co…
HollywoodTonight Jan 29, 2026
6f0592c
test: add integration tests for SentimentTopic and SentimentGuideline
HollywoodTonight Jan 30, 2026
85ee8fe
fix: correct implementation bugs in sentiment models
HollywoodTonight Jan 30, 2026
a30c06f
fix: add generateCompositeKeys for composite key entities and fix tes…
HollywoodTonight Jan 30, 2026
6680c61
test: improve branch coverage in spacecat-shared-utils
HollywoodTonight Jan 30, 2026
e058061
test(data-access): fix unit tests for sentiment model collections
HollywoodTonight Jan 30, 2026
492aad3
Merge branch 'main' into brand-sentiment-guidelines
HollywoodTonight Jan 30, 2026
f70a6dc
Merge branch 'main' into brand-sentiment-guidelines
HollywoodTonight Feb 2, 2026
5115c39
Revert "test: improve branch coverage in spacecat-shared-utils"
HollywoodTonight Feb 2, 2026
10638fb
Merge branch 'main' into brand-sentiment-guidelines
HollywoodTonight Feb 2, 2026
1a37f7c
fix(data-access): update TypeScript definitions for sentiment collect…
HollywoodTonight Feb 4, 2026
220f557
Merge branch 'main' into brand-sentiment-guidelines
HollywoodTonight Feb 4, 2026
a1236f7
Merge branch 'main' into brand-sentiment-guidelines
HollywoodTonight Feb 4, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import ReportCollection from '../report/report.collection.js';
import TrialUserCollection from '../trial-user/trial-user.collection.js';
import TrialUserActivityCollection from '../trial-user-activity/trial-user-activity.collection.js';
import PageCitabilityCollection from '../page-citability/page-citability.collection.js';
import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guideline.collection.js';
import SentimentTopicCollection from '../sentiment-topic/sentiment-topic.collection.js';

import ApiKeySchema from '../api-key/api-key.schema.js';
import AsyncJobSchema from '../async-job/async-job.schema.js';
Expand Down Expand Up @@ -71,6 +73,8 @@ import ReportSchema from '../report/report.schema.js';
import TrialUserSchema from '../trial-user/trial-user.schema.js';
import TrialUserActivitySchema from '../trial-user-activity/trial-user-activity.schema.js';
import PageCitabilitySchema from '../page-citability/page-citability.schema.js';
import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline.schema.js';
import SentimentTopicSchema from '../sentiment-topic/sentiment-topic.schema.js';

/**
* EntityRegistry - A registry class responsible for managing entities, their schema and collection.
Expand Down Expand Up @@ -179,5 +183,7 @@ EntityRegistry.registerEntity(ReportSchema, ReportCollection);
EntityRegistry.registerEntity(TrialUserSchema, TrialUserCollection);
EntityRegistry.registerEntity(TrialUserActivitySchema, TrialUserActivityCollection);
EntityRegistry.registerEntity(PageCitabilitySchema, PageCitabilityCollection);
EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollection);
EntityRegistry.registerEntity(SentimentTopicSchema, SentimentTopicCollection);

export default EntityRegistry;
2 changes: 2 additions & 0 deletions packages/spacecat-shared-data-access/src/models/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ export type * from './suggestion';
export type * from './report';
export type * from './trial-user';
export type * from './trial-user-activity';
export type * from './sentiment-guideline';
export type * from './sentiment-topic';
2 changes: 2 additions & 0 deletions packages/spacecat-shared-data-access/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ export * from './report/index.js';
export * from './trial-user/index.js';
export * from './trial-user-activity/index.js';
export * from './page-citability/index.js';
export * from './sentiment-guideline/index.js';
export * from './sentiment-topic/index.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import type { BaseCollection, BaseModel, Site } from '../index';

/**
* SentimentGuideline entity representing a guideline for sentiment analysis.
* Composite primary key: siteId (PK) + guidelineId (SK)
*/
export interface SentimentGuideline extends BaseModel {
getGuidelineId(): string;
getName(): string;
getInstruction(): string;
getAudits(): string[];
getEnabled(): boolean;
getCreatedAt(): string;
getCreatedBy(): string;
getUpdatedAt(): string;
getUpdatedBy(): string;
getSite(): Promise<Site>;
getSiteId(): string;

setName(name: string): SentimentGuideline;
setInstruction(instruction: string): SentimentGuideline;
setAudits(audits: string[]): SentimentGuideline;
setEnabled(enabled: boolean): SentimentGuideline;
setUpdatedBy(updatedBy: string): SentimentGuideline;

isEnabled(): boolean;
isAuditEnabled(auditType: string): boolean;
enableAudit(auditType: string): SentimentGuideline;
disableAudit(auditType: string): SentimentGuideline;
}

export interface SentimentGuidelineCollection extends BaseCollection<SentimentGuideline> {
findById(siteId: string, guidelineId: string): Promise<SentimentGuideline | null>;
allBySiteId(siteId: string, options?: { limit?: number; cursor?: string }): Promise<{ data: SentimentGuideline[]; cursor: string | null }>;
allBySiteIdEnabled(siteId: string, options?: { limit?: number; cursor?: string }): Promise<{ data: SentimentGuideline[]; cursor: string | null }>;
allBySiteIdAndAuditType(siteId: string, auditType: string, options?: { limit?: number; cursor?: string }): Promise<{ data: SentimentGuideline[]; cursor: string | null }>;
findByIds(siteId: string, guidelineIds: string[]): Promise<SentimentGuideline[]>;
removeForSiteId(siteId: string): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import SentimentGuideline from './sentiment-guideline.model.js';
import SentimentGuidelineCollection from './sentiment-guideline.collection.js';

export {
SentimentGuideline,
SentimentGuidelineCollection,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { hasText } from '@adobe/spacecat-shared-utils';

import BaseCollection from '../base/base.collection.js';

/**
* SentimentGuidelineCollection - A collection class for managing SentimentGuideline entities.
* Extends BaseCollection to provide specific methods for sentiment guidelines.
*
* @class SentimentGuidelineCollection
* @extends BaseCollection
*/
class SentimentGuidelineCollection extends BaseCollection {
static COLLECTION_NAME = 'SentimentGuidelineCollection';

/**
* Finds a sentiment guideline by its composite primary key (siteId + guidelineId).
*
* @param {string} siteId - The site ID (partition key).
* @param {string} guidelineId - The guideline ID (sort key).
* @returns {Promise<SentimentGuideline|null>} The found SentimentGuideline or null.
*/
async findById(siteId, guidelineId) {
if (!hasText(siteId) || !hasText(guidelineId)) {
throw new Error('Both siteId and guidelineId are required');
}

return this.findByIndexKeys({ siteId, guidelineId });
}

/**
* Gets all sentiment guidelines for a site with pagination.
*
* @param {string} siteId - The site ID.
* @param {object} [options={}] - Query options (limit, cursor).
* @returns {Promise<{data: SentimentGuideline[], cursor: string|null}>} Paginated results.
*/
async allBySiteId(siteId, options = {}) {
if (!hasText(siteId)) {
throw new Error('SiteId is required');
}

const result = await this.allByIndexKeys(
{ siteId },
{ ...options, returnCursor: true },
);

return {
data: result.data || [],
cursor: result.cursor,
};
}

/**
* Gets all enabled sentiment guidelines for a site.
*
* @param {string} siteId - The site ID.
* @param {object} [options={}] - Query options (limit, cursor).
* @returns {Promise<{data: SentimentGuideline[], cursor: string|null}>} Paginated results.
*/
async allBySiteIdEnabled(siteId, options = {}) {
if (!hasText(siteId)) {
throw new Error('SiteId is required');
}

const result = await this.allByIndexKeys(
{ siteId },
{
...options,
returnCursor: true,
where: (attr, op) => op.eq(attr.enabled, true),
},
);

return {
data: result.data || [],
cursor: result.cursor,
};
}

/**
* Gets all sentiment guidelines for a site that have a specific audit type enabled.
* Uses FilterExpression to filter at the database level.
*
* @param {string} siteId - The site ID.
* @param {string} auditType - The audit type to filter by.
* @param {object} [options={}] - Query options (limit, cursor).
* @returns {Promise<{data: SentimentGuideline[], cursor: string|null}>} Paginated results.
*/
async allBySiteIdAndAuditType(siteId, auditType, options = {}) {
if (!hasText(siteId) || !hasText(auditType)) {
throw new Error('Both siteId and auditType are required');
}

const result = await this.allByIndexKeys(
{ siteId },
{
...options,
returnCursor: true,
where: (attr, op) => op.contains(attr.audits, auditType),
},
);

return {
data: result.data || [],
cursor: result.cursor,
};
}

/**
* Finds multiple guidelines by their IDs using batch get.
* Useful for resolving guidelineIds from a SentimentTopic.
*
* @param {string} siteId - The site ID.
* @param {string[]} guidelineIds - Array of guideline IDs to fetch.
* @returns {Promise<SentimentGuideline[]>} Array of found guidelines.
*/
async findByIds(siteId, guidelineIds) {
if (!hasText(siteId)) {
throw new Error('SiteId is required');
}

if (!Array.isArray(guidelineIds) || guidelineIds.length === 0) {
return [];
}

// Fetch all guidelines for the site and filter
// Note: For large datasets, consider implementing batch get
const result = await this.allBySiteId(siteId);
const allGuidelines = result.data || [];
const guidelineIdSet = new Set(guidelineIds);

return allGuidelines.filter((guideline) => {
const id = guideline.getGuidelineId?.() ?? guideline.guidelineId;
return guidelineIdSet.has(id);
});
}

/**
* Removes all sentiment guidelines for a specific site.
*
* @param {string} siteId - The site ID.
* @returns {Promise<void>}
*/
async removeForSiteId(siteId) {
if (!hasText(siteId)) {
throw new Error('SiteId is required');
}

const result = await this.allBySiteId(siteId);
const guidelinesToRemove = result.data || [];

if (guidelinesToRemove.length > 0) {
const keysToRemove = guidelinesToRemove.map((guideline) => ({
siteId,
guidelineId: guideline.getGuidelineId?.() ?? guideline.guidelineId,
}));
await this.removeByIndexKeys(keysToRemove);
}
}
}

export default SentimentGuidelineCollection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import BaseModel from '../base/base.model.js';

/**
* SentimentGuideline - A class representing a sentiment analysis guideline.
* Guidelines define how to analyze topics (e.g., "Focus on product quality").
*
* @class SentimentGuideline
* @extends BaseModel
*/
class SentimentGuideline extends BaseModel {
static ENTITY_NAME = 'SentimentGuideline';

/**
* Checks if this guideline is currently enabled.
* @returns {boolean} True if the guideline is enabled.
*/
isEnabled() {
return this.getEnabled?.() ?? this.enabled ?? true;
}

/**
* Checks if this guideline is enabled for a specific audit type.
* @param {string} auditType - The audit type to check.
* @returns {boolean} True if the audit is enabled for this guideline.
*/
isAuditEnabled(auditType) {
const audits = this.getAudits?.() ?? this.audits ?? [];
return audits.includes(auditType);
}

/**
* Adds an audit type to the audits array if not already present.
* @param {string} auditType - The audit type to add.
* @returns {this} The current instance for chaining.
*/
enableAudit(auditType) {
const audits = this.getAudits?.() ?? this.audits ?? [];
if (!audits.includes(auditType)) {
const updatedAudits = [...audits, auditType];
if (this.setAudits) {
this.setAudits(updatedAudits);
} else {
this.audits = updatedAudits;
}
}
return this;
}

/**
* Removes an audit type from the audits array.
* @param {string} auditType - The audit type to remove.
* @returns {this} The current instance for chaining.
*/
disableAudit(auditType) {
const audits = this.getAudits?.() ?? this.audits ?? [];
const filtered = audits.filter((a) => a !== auditType);
if (this.setAudits) {
this.setAudits(filtered);
} else {
this.audits = filtered;
}
return this;
}

/**
* Generates the composite keys for remove/update operations.
* Required for entities with composite primary keys.
* @returns {Object} - The composite keys (siteId + guidelineId).
*/
generateCompositeKeys() {
return {
siteId: this.getSiteId(),
guidelineId: this.getGuidelineId(),
};
}
}

export default SentimentGuideline;
Loading
Loading