diff --git a/package-lock.json b/package-lock.json index 83f3ecf71..3af845950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1027,7 +1027,6 @@ "node_modules/@aws-sdk/client-dynamodb": { "version": "3.893.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1922,7 +1921,6 @@ "node_modules/@aws-sdk/client-sso-oidc": { "version": "3.726.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2340,7 +2338,6 @@ "node_modules/@aws-sdk/client-sts": { "version": "3.726.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2950,7 +2947,6 @@ "version": "7.28.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -3758,7 +3754,6 @@ "version": "7.0.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -5306,7 +5301,6 @@ "version": "14.1.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -5439,7 +5433,6 @@ "version": "8.44.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -5647,7 +5640,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5938,7 +5930,6 @@ "node_modules/aws-xray-sdk-core": { "version": "3.10.3", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -6135,7 +6126,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -6318,7 +6308,6 @@ "version": "6.0.1", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -7962,7 +7951,6 @@ "version": "9.36.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10864,7 +10852,6 @@ "version": "4.3.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -14415,7 +14402,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15873,7 +15859,6 @@ "version": "4.52.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -16010,7 +15995,6 @@ "version": "24.2.9", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -16749,7 +16733,6 @@ "version": "21.0.0", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", @@ -17540,7 +17523,6 @@ "version": "5.9.2", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17628,7 +17610,6 @@ "node_modules/unified": { "version": "11.0.5", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -19225,7 +19206,6 @@ "packages/spacecat-shared-ahrefs-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -20896,7 +20876,6 @@ "packages/spacecat-shared-athena-client/node_modules/@aws-sdk/client-dynamodb": { "version": "3.859.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -23131,7 +23110,6 @@ "packages/spacecat-shared-athena-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -25079,7 +25057,6 @@ "packages/spacecat-shared-brand-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -26528,7 +26505,6 @@ "packages/spacecat-shared-content-client/node_modules/@aws-sdk/client-dynamodb": { "version": "3.859.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -32111,7 +32087,6 @@ "packages/spacecat-shared-data-access/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -32608,7 +32583,6 @@ "packages/spacecat-shared-google-client/node_modules/@adobe/spacecat-shared-data-access/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.716.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -32660,7 +32634,6 @@ "packages/spacecat-shared-google-client/node_modules/@adobe/spacecat-shared-data-access/node_modules/@aws-sdk/client-sts": { "version": "3.716.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -33763,7 +33736,6 @@ "packages/spacecat-shared-google-client/node_modules/@adobe/spacecat-shared-http-utils/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.716.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -33815,7 +33787,6 @@ "packages/spacecat-shared-google-client/node_modules/@adobe/spacecat-shared-http-utils/node_modules/@aws-sdk/client-sts": { "version": "3.716.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -35067,7 +35038,6 @@ "packages/spacecat-shared-google-client/node_modules/@aws-sdk/client-dynamodb": { "version": "3.721.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -37156,7 +37126,6 @@ "packages/spacecat-shared-google-client/node_modules/@aws-sdk/client-sts": { "version": "3.721.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -40049,7 +40018,6 @@ "packages/spacecat-shared-google-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -40547,7 +40515,6 @@ "packages/spacecat-shared-gpt-client/node_modules/@adobe/spacecat-shared-ims-client/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.721.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -40599,7 +40566,6 @@ "packages/spacecat-shared-gpt-client/node_modules/@adobe/spacecat-shared-ims-client/node_modules/@aws-sdk/client-sts": { "version": "3.721.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -42499,7 +42465,6 @@ "packages/spacecat-shared-gpt-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -43544,7 +43509,6 @@ "packages/spacecat-shared-http-utils/node_modules/@aws-sdk/client-dynamodb": { "version": "3.859.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -45635,7 +45599,6 @@ "packages/spacecat-shared-http-utils/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -46539,7 +46502,6 @@ "packages/spacecat-shared-ims-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -46652,7 +46614,7 @@ }, "packages/spacecat-shared-rum-api-client": { "name": "@adobe/spacecat-shared-rum-api-client", - "version": "2.37.7", + "version": "2.37.8", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "4.2.3", @@ -48356,7 +48318,6 @@ "packages/spacecat-shared-rum-api-client/node_modules/@aws-sdk/client-dynamodb": { "version": "3.859.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -50591,7 +50552,6 @@ "packages/spacecat-shared-rum-api-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -51532,7 +51492,6 @@ "packages/spacecat-shared-scrape-client/node_modules/@aws-sdk/client-dynamodb": { "version": "3.859.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -54576,7 +54535,6 @@ "packages/spacecat-shared-slack-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -56233,7 +56191,6 @@ "packages/spacecat-shared-tier-client/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -57851,7 +57808,6 @@ "packages/spacecat-shared-utils/node_modules/@aws-sdk/client-dynamodb": { "version": "3.859.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -59739,7 +59695,6 @@ "packages/spacecat-shared-utils/node_modules/aws-xray-sdk-core": { "version": "3.10.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", diff --git a/packages/spacecat-shared-data-access/docs/schema.json b/packages/spacecat-shared-data-access/docs/schema.json index 5d15e622a..c416b0237 100755 --- a/packages/spacecat-shared-data-access/docs/schema.json +++ b/packages/spacecat-shared-data-access/docs/schema.json @@ -133,6 +133,18 @@ { "AttributeName": "updatedAt", "AttributeType": "S" + }, + { + "AttributeName": "projectId", + "AttributeType": "S" + }, + { + "AttributeName": "externalOwnerId", + "AttributeType": "S" + }, + { + "AttributeName": "externalSiteId", + "AttributeType": "S" } ], "GlobalSecondaryIndexes": [ @@ -179,6 +191,34 @@ "Projection": { "ProjectionType": "ALL" } + }, + { + "IndexName": "spacecat-services-sites-by-external-ids", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "externalOwnerId", + "AttributeType": "S" + }, + "SortKey": { + "AttributeName": "externalSiteId", + "AttributeType": "S" + } + }, + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "spacecat-services-all-sites-project", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "projectId", + "AttributeType": "S" + } + }, + "Projection": { + "ProjectionType": "ALL" + } } ], "DataAccess": { diff --git a/packages/spacecat-shared-data-access/src/models/base/entity.registry.js b/packages/spacecat-shared-data-access/src/models/base/entity.registry.js index 83f332594..c1c5e5fd4 100755 --- a/packages/spacecat-shared-data-access/src/models/base/entity.registry.js +++ b/packages/spacecat-shared-data-access/src/models/base/entity.registry.js @@ -26,6 +26,7 @@ import KeyEventCollection from '../key-event/key-event.collection.js'; import LatestAuditCollection from '../latest-audit/latest-audit.collection.js'; import OpportunityCollection from '../opportunity/opportunity.collection.js'; import OrganizationCollection from '../organization/organization.collection.js'; +import ProjectCollection from '../project/project.collection.js'; import ScrapeJobCollection from '../scrape-job/scrape-job.collection.js'; import ScrapeUrlCollection from '../scrape-url/scrape-url.collection.js'; import SiteCandidateCollection from '../site-candidate/site-candidate.collection.js'; @@ -52,6 +53,7 @@ import KeyEventSchema from '../key-event/key-event.schema.js'; import LatestAuditSchema from '../latest-audit/latest-audit.schema.js'; import OpportunitySchema from '../opportunity/opportunity.schema.js'; import OrganizationSchema from '../organization/organization.schema.js'; +import ProjectSchema from '../project/project.schema.js'; import ScrapeJobSchema from '../scrape-job/scrape-job.schema.js'; import ScrapeUrlSchema from '../scrape-url/scrape-url.schema.js'; import SiteSchema from '../site/site.schema.js'; @@ -147,6 +149,7 @@ EntityRegistry.registerEntity(KeyEventSchema, KeyEventCollection); EntityRegistry.registerEntity(LatestAuditSchema, LatestAuditCollection); EntityRegistry.registerEntity(OpportunitySchema, OpportunityCollection); EntityRegistry.registerEntity(OrganizationSchema, OrganizationCollection); +EntityRegistry.registerEntity(ProjectSchema, ProjectCollection); EntityRegistry.registerEntity(ScrapeJobSchema, ScrapeJobCollection); EntityRegistry.registerEntity(ScrapeUrlSchema, ScrapeUrlCollection); EntityRegistry.registerEntity(SiteSchema, SiteCollection); diff --git a/packages/spacecat-shared-data-access/src/models/index.js b/packages/spacecat-shared-data-access/src/models/index.js index 61c327fa6..90ec5129d 100755 --- a/packages/spacecat-shared-data-access/src/models/index.js +++ b/packages/spacecat-shared-data-access/src/models/index.js @@ -24,6 +24,7 @@ export * from './key-event/index.js'; export * from './latest-audit/index.js'; export * from './opportunity/index.js'; export * from './organization/index.js'; +export * from './project/index.js'; export * from './scrape-job/index.js'; export * from './scrape-url/index.js'; export * from './site-candidate/index.js'; diff --git a/packages/spacecat-shared-data-access/src/models/organization/index.d.ts b/packages/spacecat-shared-data-access/src/models/organization/index.d.ts index 6e931d7f3..0f2a6f258 100644 --- a/packages/spacecat-shared-data-access/src/models/organization/index.d.ts +++ b/packages/spacecat-shared-data-access/src/models/organization/index.d.ts @@ -11,7 +11,7 @@ */ import type { - BaseCollection, BaseModel, Site, Entitlement, OrganizationIdentityProvider, TrialUser, + BaseCollection, BaseModel, Site, Project, Entitlement, OrganizationIdentityProvider, TrialUser, } from '../index'; export interface Organization extends BaseModel { @@ -20,6 +20,7 @@ export interface Organization extends BaseModel { getImsOrgId(): string; getName(): string; getSites(): Promise; + getProjects(): Promise; getEntitlements(): Promise; getOrganizationIdentityProviders(): Promise; getTrialUsers(): Promise; diff --git a/packages/spacecat-shared-data-access/src/models/organization/organization.schema.js b/packages/spacecat-shared-data-access/src/models/organization/organization.schema.js index 8a48249f8..b33e69d11 100644 --- a/packages/spacecat-shared-data-access/src/models/organization/organization.schema.js +++ b/packages/spacecat-shared-data-access/src/models/organization/organization.schema.js @@ -28,6 +28,7 @@ Indexes Doc: https://electrodb.dev/en/modeling/indexes/ const schema = new SchemaBuilder(Organization, OrganizationCollection) // this will add an attribute 'organizationId' as well as an index 'byOrganizationId' .addReference('has_many', 'Sites') + .addReference('has_many', 'Projects') .addReference('has_many', 'Entitlements') .addReference('has_many', 'TrialUsers') .addAttribute('config', { diff --git a/packages/spacecat-shared-data-access/src/models/project/index.d.ts b/packages/spacecat-shared-data-access/src/models/project/index.d.ts new file mode 100644 index 000000000..066d9dfda --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/project/index.d.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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, Organization, Site, +} from '../index'; + +export interface Project extends BaseModel { + getProjectName(): string; + getOrganization(): Promise; + getOrganizationId(): string; + getSites(): Promise; + getPrimaryLocaleSites(): Promise; + setProjectName(projectName: string): Project; + setOrganizationId(organizationId: string): Project; +} + +export interface ProjectCollection extends BaseCollection { + allByOrganizationId(organizationId: string): Promise; + findByOrganizationId(organizationId: string): Promise; + findByProjectName(projectName: string): Promise; +} diff --git a/packages/spacecat-shared-data-access/src/models/project/index.js b/packages/spacecat-shared-data-access/src/models/project/index.js new file mode 100644 index 000000000..75c407028 --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/project/index.js @@ -0,0 +1,14 @@ +/* + * Copyright 2024 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. + */ + +export { default as Project } from './project.model.js'; +export { default as ProjectCollection } from './project.collection.js'; diff --git a/packages/spacecat-shared-data-access/src/models/project/project.collection.js b/packages/spacecat-shared-data-access/src/models/project/project.collection.js new file mode 100644 index 000000000..6731692dd --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/project/project.collection.js @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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 BaseCollection from '../base/base.collection.js'; + +/** + * ProjectCollection - A collection class responsible for managing Project entities. + * Extends the BaseCollection to provide specific methods for interacting with Project records. + * + * @class ProjectCollection + * @extends BaseCollection + */ +class ProjectCollection extends BaseCollection { +} + +export default ProjectCollection; diff --git a/packages/spacecat-shared-data-access/src/models/project/project.model.js b/packages/spacecat-shared-data-access/src/models/project/project.model.js new file mode 100644 index 000000000..6cacb36c3 --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/project/project.model.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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'; + +/** + * Project - A class representing a Project entity. + * Provides methods to access and manipulate Project-specific data. + * + * @class Project + * @extends BaseModel + */ +class Project extends BaseModel { + async getPrimaryLocaleSites() { + const sites = await this.getSites(); + return sites.filter((site) => site.getIsPrimaryLocale()); + } +} + +export default Project; diff --git a/packages/spacecat-shared-data-access/src/models/project/project.schema.js b/packages/spacecat-shared-data-access/src/models/project/project.schema.js new file mode 100644 index 000000000..32f32e874 --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/project/project.schema.js @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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. + */ + +/* c8 ignore start */ + +import { hasText } from '@adobe/spacecat-shared-utils'; + +import SchemaBuilder from '../base/schema.builder.js'; +import Project from './project.model.js'; +import ProjectCollection from './project.collection.js'; + +/* +Schema Doc: https://electrodb.dev/en/modeling/schema/ +Attribute Doc: https://electrodb.dev/en/modeling/attributes/ +Indexes Doc: https://electrodb.dev/en/modeling/indexes/ + */ + +const schema = new SchemaBuilder(Project, ProjectCollection) + .addReference('belongs_to', 'Organization') + .addReference('has_many', 'Sites') + .addAttribute('projectName', { + type: 'string', + required: true, + validate: (value) => hasText(value), + }) + .addAllIndex(['projectName']); + +export default schema.build(); diff --git a/packages/spacecat-shared-data-access/src/models/site/index.d.ts b/packages/spacecat-shared-data-access/src/models/site/index.d.ts index cccf4f288..22230c119 100644 --- a/packages/spacecat-shared-data-access/src/models/site/index.d.ts +++ b/packages/spacecat-shared-data-access/src/models/site/index.d.ts @@ -19,6 +19,7 @@ import type { LatestAudit, Opportunity, Organization, + Project, SiteCandidate, SiteEnrollment, SiteTopPage, @@ -214,6 +215,11 @@ export interface Site extends BaseModel { getOpportunitiesByStatusAndUpdatedAt(status: string, updatedAt: string): Promise; getOrganization(): Promise; getOrganizationId(): string; + getProject(): Promise; + getProjectId(): string; + getIsPrimaryLocale(): boolean; + getLanguage(): string; + getRegion(): string; getSiteCandidates(): Promise; getSiteEnrollments(): Promise; getSiteTopPages(): Promise; @@ -234,18 +240,27 @@ export interface Site extends BaseModel { setIsSandbox(isSandbox: boolean): Site; setIsLiveToggledAt(isLiveToggledAt: string): Site; setOrganizationId(organizationId: string): Site; + setProjectId(projectId: string): Site; + setIsPrimaryLocale(primaryLocale: boolean): Site; + setLanguage(language: string): Site; + setRegion(region: string): Site; toggleLive(): Site; } -export interface SiteCollection extends BaseCollection { +export interface SiteCollection extends BaseCollection { allByBaseURL(baseURL: string): Promise; allByDeliveryType(deliveryType: string): Promise; allByOrganizationId(organizationId: string): Promise; + allByProjectId(projectId: string): Promise; + allByProjectName(projectName: string): Promise; + allByOrganizationIdAndProjectId(organizationId: string, projectId: string): Promise; + allByOrganizationIdAndProjectName(organizationId: string, projectName: string): Promise; allSitesToAudit(): Promise; allWithLatestAudit(auditType: string, order?: string, deliveryType?: string): Promise; findByBaseURL(baseURL: string): Promise; findByDeliveryType(deliveryType: string): Promise; findByOrganizationId(organizationId: string): Promise; + findByProjectId(projectId: string): Promise; findByPreviewURL(previewURL: string): Promise; findByExternalOwnerIdAndExternalSiteId( externalOwnerId: string, externalSiteId: string diff --git a/packages/spacecat-shared-data-access/src/models/site/site.collection.js b/packages/spacecat-shared-data-access/src/models/site/site.collection.js index 36bd77b6f..213c29b0f 100755 --- a/packages/spacecat-shared-data-access/src/models/site/site.collection.js +++ b/packages/spacecat-shared-data-access/src/models/site/site.collection.js @@ -102,6 +102,72 @@ class SiteCollection extends BaseCollection { throw new DataAccessError(`Unsupported preview URL: ${previewURL}`, this); } } + + async allByProjectName(projectName) { + if (!hasText(projectName)) { + throw new DataAccessError('projectName is required', this); + } + + const projectCollection = this.entityRegistry.getCollection('ProjectCollection'); + const project = await projectCollection.findByProjectName(projectName); + + if (!project) { + return []; + } + return this.allByProjectId(project.getId()); + } + + async allByOrganizationIdAndProjectId(organizationId, projectId) { + if (!hasText(organizationId)) { + throw new DataAccessError('organizationId is required', this); + } + if (!hasText(projectId)) { + throw new DataAccessError('projectId is required', this); + } + + const organizationCollection = this.entityRegistry.getCollection('OrganizationCollection'); + const organization = await organizationCollection.findById(organizationId); + + if (!organization) { + return []; + } + + const projectCollection = this.entityRegistry.getCollection('ProjectCollection'); + const projects = await projectCollection.allByOrganizationId(organizationId); + const project = projects.find((p) => p.getId() === projectId); + + if (!project) { + return []; + } + + return this.allByProjectId(projectId); + } + + async allByOrganizationIdAndProjectName(organizationId, projectName) { + if (!hasText(organizationId)) { + throw new DataAccessError('organizationId is required', this); + } + if (!hasText(projectName)) { + throw new DataAccessError('projectName is required', this); + } + + const organizationCollection = this.entityRegistry.getCollection('OrganizationCollection'); + const organization = await organizationCollection.findById(organizationId); + + if (!organization) { + return []; + } + + const projectCollection = this.entityRegistry.getCollection('ProjectCollection'); + const projects = await projectCollection.allByOrganizationId(organizationId); + const project = projects.find((p) => p.getProjectName() === projectName); + + if (!project) { + return []; + } + + return this.allByProjectId(project.getId()); + } } export default SiteCollection; diff --git a/packages/spacecat-shared-data-access/src/models/site/site.schema.js b/packages/spacecat-shared-data-access/src/models/site/site.schema.js index dcd90cc57..f8101927c 100755 --- a/packages/spacecat-shared-data-access/src/models/site/site.schema.js +++ b/packages/spacecat-shared-data-access/src/models/site/site.schema.js @@ -34,6 +34,8 @@ Indexes Doc: https://electrodb.dev/en/modeling/indexes/ const schema = new SchemaBuilder(Site, SiteCollection) // this will add an attribute 'organizationId' as well as an index 'byOrganizationId' .addReference('belongs_to', 'Organization') + // this will add an attribute 'projectId' as well as an index 'byProjectId' + .addReference('belongs_to', 'Project', ['updatedAt'], { required: false }) // has_many references do not add attributes or indexes .addReference('has_many', 'Audits') .addReference('has_many', 'Experiments') @@ -55,6 +57,20 @@ const schema = new SchemaBuilder(Site, SiteCollection) .addAttribute('name', { type: 'string', }) + .addAttribute('isPrimaryLocale', { + type: 'boolean', + required: false, + }) + .addAttribute('language', { + type: 'string', + required: false, + validate: (value) => !value || /^[a-z]{2}$/.test(value), // ISO 639-1 format + }) + .addAttribute('region', { + type: 'string', + required: false, + validate: (value) => !value || /^[A-Z]{2}$/.test(value), // ISO 3166-1 alpha-2 format + }) .addAttribute('config', { type: 'any', required: true, diff --git a/packages/spacecat-shared-data-access/test/fixtures/index.fixtures.js b/packages/spacecat-shared-data-access/test/fixtures/index.fixtures.js index a1ce8ffdb..cbe18fa5a 100644 --- a/packages/spacecat-shared-data-access/test/fixtures/index.fixtures.js +++ b/packages/spacecat-shared-data-access/test/fixtures/index.fixtures.js @@ -22,6 +22,7 @@ import scrapeUrls from './scrape-urls.fixture.js'; import keyEvents from './key-events.fixture.js'; import opportunities from './opportunities.fixture.js'; import organizations from './organizations.fixture.js'; +import projects from './projects.fixture.js'; import siteCandidates from './site-candidates.fixture.js'; import siteTopForms from './site-top-forms.fixture.js'; import siteTopPages from './site-top-pages.fixture.js'; @@ -47,6 +48,7 @@ export default { keyEvents, opportunities, organizations, + projects, siteCandidates, siteTopForms, siteTopPages, diff --git a/packages/spacecat-shared-data-access/test/fixtures/projects.fixture.js b/packages/spacecat-shared-data-access/test/fixtures/projects.fixture.js new file mode 100644 index 000000000..7bc015828 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/fixtures/projects.fixture.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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. + */ + +export default [ + { + projectId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + projectName: 'Test Project 1', + organizationId: '4854e75e-894b-4a74-92bf-d674abad1423', + }, + { + projectId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + projectName: 'Test Project 2', + organizationId: '4854e75e-894b-4a74-92bf-d674abad1423', + }, + { + projectId: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', + projectName: 'Another Project', + organizationId: '5965f86f-9a5c-5b85-a3c0-e785bcbe2534', + }, +]; diff --git a/packages/spacecat-shared-data-access/test/fixtures/sites.fixture.js b/packages/spacecat-shared-data-access/test/fixtures/sites.fixture.js index 8882bbc28..7cac99d2c 100644 --- a/packages/spacecat-shared-data-access/test/fixtures/sites.fixture.js +++ b/packages/spacecat-shared-data-access/test/fixtures/sites.fixture.js @@ -19,6 +19,7 @@ const sites = [ authoringType: 'cs/crosswalk', gitHubURL: 'https://github.com/org-0/test-repo', organizationId: '4854e75e-894b-4a74-92bf-d674abad1423', + projectId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', isLive: true, isSandbox: false, isLiveToggledAt: '2024-11-29T07:45:55.952Z', @@ -74,6 +75,7 @@ const sites = [ deliveryType: 'aem_cs', gitHubURL: 'https://github.com/org-1/test-repo', organizationId: '757ceb98-05c8-4e07-bb23-bc722115b2b0', + projectId: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', isLive: true, isSandbox: false, isLiveToggledAt: '2024-11-29T07:45:55.952Z', @@ -157,6 +159,7 @@ const sites = [ deliveryType: 'aem_cs', gitHubURL: 'https://github.com/org-3/test-repo', organizationId: '4854e75e-894b-4a74-92bf-d674abad1423', + projectId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', isLive: true, isSandbox: true, isLiveToggledAt: '2024-11-29T07:45:55.952Z', diff --git a/packages/spacecat-shared-data-access/test/it/project/project.test.js b/packages/spacecat-shared-data-access/test/it/project/project.test.js new file mode 100644 index 000000000..8d49e59a9 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/it/project/project.test.js @@ -0,0 +1,135 @@ +/* + * Copyright 2024 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. + */ + +/* eslint-env mocha */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { sanitizeIdAndAuditFields, sanitizeTimestamps } from '../../../src/util/util.js'; +import { getDataAccess } from '../util/db.js'; +import { seedDatabase } from '../util/seed.js'; + +use(chaiAsPromised); + +describe('Project IT', async () => { + let sampleData; + let Project; + + before(async () => { + sampleData = await seedDatabase(); + const dataAccess = getDataAccess(); + Project = dataAccess.Project; + }); + + it('gets all projects', async () => { + const projects = await Project.all(); + + expect(projects).to.be.an('array'); + expect(projects.length).to.equal(sampleData.projects.length); + + // Sort both arrays by project name for consistent comparison + const sortedProjects = projects.sort( + (a, b) => a.getProjectName().localeCompare(b.getProjectName()), + ); + const sortedSampleProjects = sampleData.projects.sort( + (a, b) => a.getProjectName().localeCompare(b.getProjectName()), + ); + + for (let i = 0; i < sortedProjects.length; i += 1) { + const project = sortedProjects[i]; + const sampleProject = sortedSampleProjects[i]; + + expect(project).to.be.an('object'); + expect(project.getId()).to.be.a('string'); + expect(project.getProjectName()).to.be.a('string'); + expect(project.getOrganizationId()).to.be.a('string'); + + expect( + sanitizeTimestamps(project.toJSON()), + ).to.eql( + sanitizeTimestamps(sampleProject.toJSON()), + ); + } + }); + + it('gets a project by id', async () => { + const sampleProject = sampleData.projects[0]; + const project = await Project.findById(sampleProject.getId()); + + expect(project).to.be.an('object'); + expect( + sanitizeTimestamps(project.toJSON()), + ).to.eql( + sanitizeTimestamps(sampleProject.toJSON()), + ); + }); + + it('gets projects by organization id', async () => { + const organizationId = sampleData.organizations[0].getId(); + const projects = await Project.allByOrganizationId(organizationId); + + expect(projects).to.be.an('array'); + expect(projects.length).to.be.greaterThan(0); + + for (let i = 0; i < projects.length; i += 1) { + const project = projects[i]; + expect(project.getOrganizationId()).to.equal(organizationId); + } + }); + + it('adds a new project', async () => { + const data = { + projectName: 'New Integration Project', + organizationId: sampleData.organizations[0].getId(), + updatedBy: 'system', + }; + + const project = await Project.create(data); + + expect(project).to.be.an('object'); + expect(project.getProjectName()).to.equal(data.projectName); + expect(project.getOrganizationId()).to.equal(data.organizationId); + + expect( + sanitizeIdAndAuditFields('Project', project.toJSON()), + ).to.eql(data); + }); + + it('updates a project', async () => { + const project = await Project.findById(sampleData.projects[0].getId()); + + const data = { + projectName: 'Updated Project Name', + }; + + project.setProjectName(data.projectName); + + await project.save(); + + const updatedProject = await Project.findById(project.getId()); + + expect(updatedProject.getProjectName()).to.equal(data.projectName); + expect(updatedProject.getId()).to.equal(project.getId()); + expect(updatedProject.record.createdAt).to.equal(project.record.createdAt); + expect(updatedProject.record.updatedAt).to.not.equal(project.record.updatedAt); + }); + + it('removes a project', async () => { + const project = await Project.findById(sampleData.projects[0].getId()); + + await project.remove(); + + const notFound = await Project.findById(sampleData.projects[0].getId()); + expect(notFound).to.be.null; + }); +}); diff --git a/packages/spacecat-shared-data-access/test/it/site/site.test.js b/packages/spacecat-shared-data-access/test/it/site/site.test.js index e7fe52da4..f9ceeea23 100644 --- a/packages/spacecat-shared-data-access/test/it/site/site.test.js +++ b/packages/spacecat-shared-data-access/test/it/site/site.test.js @@ -751,4 +751,101 @@ describe('Site IT', async () => { await updatedSite.remove(); }); }); + describe('Project-Site relationship', () => { + it('gets sites by project id', async () => { + const projectId = sampleData.projects[0].getId(); + const sites = await Site.allByProjectId(projectId); + + expect(sites).to.be.an('array'); + + for (let i = 0; i < sites.length; i += 1) { + const site = sites[i]; + expect(site.getProjectId()).to.equal(projectId); + } + }); + + it('gets sites by project name', async () => { + const projectName = sampleData.projects[0].getProjectName(); + const sites = await Site.allByProjectName(projectName); + + expect(sites).to.be.an('array'); + + for (let i = 0; i < sites.length; i += 1) { + const site = sites[i]; + expect(site.getProjectId()).to.equal(sampleData.projects[0].getId()); + } + }); + + it('gets sites by organization id and project id', async () => { + const organizationId = sampleData.organizations[0].getId(); + const projectId = sampleData.projects[0].getId(); + const sites = await Site.allByOrganizationIdAndProjectId(organizationId, projectId); + + expect(sites).to.be.an('array'); + + for (let i = 0; i < sites.length; i += 1) { + const site = sites[i]; + expect(site.getProjectId()).to.equal(projectId); + expect(site.getOrganizationId()).to.equal(organizationId); + } + }); + + it('gets sites by organization id and project name', async () => { + const organizationId = sampleData.organizations[0].getId(); + const projectName = sampleData.projects[0].getProjectName(); + const sites = await Site.allByOrganizationIdAndProjectName(organizationId, projectName); + + expect(sites).to.be.an('array'); + + for (let i = 0; i < sites.length; i += 1) { + const site = sites[i]; + expect(site.getProjectId()).to.equal(sampleData.projects[0].getId()); + expect(site.getOrganizationId()).to.equal(organizationId); + } + }); + }); + + describe('Site localization fields', () => { + it('creates a site with localization data', async () => { + const siteData = { + baseURL: 'https://localized-example.com', + name: 'localized-site', + organizationId: sampleData.organizations[0].getId(), + projectId: sampleData.projects[0].getId(), + isPrimaryLocale: false, + language: 'en', + region: 'US', + isLive: true, + isLiveToggledAt: '2024-12-06T08:35:24.125Z', + }; + + const site = await Site.create(siteData); + + expect(site.getIsPrimaryLocale()).to.equal(false); + expect(site.getLanguage()).to.equal('en'); + expect(site.getRegion()).to.equal('US'); + expect(site.getProjectId()).to.equal(sampleData.projects[0].getId()); + + // Clean up + await site.remove(); + }); + + it('updates site localization data', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + + site.setIsPrimaryLocale(true); + site.setLanguage('fr'); + site.setRegion('FR'); + site.setProjectId(sampleData.projects[0].getId()); + + await site.save(); + + const updatedSite = await Site.findById(site.getId()); + + expect(updatedSite.getIsPrimaryLocale()).to.equal(true); + expect(updatedSite.getLanguage()).to.equal('fr'); + expect(updatedSite.getRegion()).to.equal('FR'); + expect(updatedSite.getProjectId()).to.equal(sampleData.projects[0].getId()); + }); + }); }); diff --git a/packages/spacecat-shared-data-access/test/unit/models/project/project.collection.test.js b/packages/spacecat-shared-data-access/test/unit/models/project/project.collection.test.js new file mode 100644 index 000000000..ff545f179 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/models/project/project.collection.test.js @@ -0,0 +1,195 @@ +/* + * Copyright 2024 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. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import { createElectroMocks } from '../../util.js'; +import projectsFixture from '../../../fixtures/projects.fixture.js'; +import { Project } from '../../../../src/index.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +describe('ProjectCollection', () => { + let instance; + + let mockElectroService; + let mockEntityRegistry; + let mockLogger; + let model; + let schema; + + const mockRecord = projectsFixture[0]; + + beforeEach(() => { + ({ + mockElectroService, + mockEntityRegistry, + mockLogger, + collection: instance, + model, + schema, + } = createElectroMocks(Project, mockRecord)); + }); + + describe('constructor', () => { + it('initializes the ProjectCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.schema).to.equal(schema); + expect(instance.log).to.equal(mockLogger); + + expect(model).to.be.an('object'); + }); + }); + + describe('allByOrganizationId', () => { + it('returns all projects for an organization', async () => { + const mockProjects = [ + { getId: () => 'p12345', getOrganizationId: () => 'org123' }, + { getId: () => 'p67890', getOrganizationId: () => 'org123' }, + ]; + instance.allByOrganizationId = stub().resolves(mockProjects); + + const result = await instance.allByOrganizationId('org123'); + + expect(result).to.deep.equal(mockProjects); + expect(instance.allByOrganizationId).to.have.been.calledOnceWithExactly('org123'); + }); + + it('returns empty array when no projects found for organization', async () => { + instance.allByOrganizationId = stub().resolves([]); + + const result = await instance.allByOrganizationId('nonexistent-org'); + + expect(result).to.deep.equal([]); + expect(instance.allByOrganizationId).to.have.been.calledOnceWithExactly('nonexistent-org'); + }); + + it('throws error for empty organization ID', async () => { + instance.allByOrganizationId = stub().rejects(new Error('organizationId is required')); + + await expect(instance.allByOrganizationId('')).to.be.rejectedWith('organizationId is required'); + await expect(instance.allByOrganizationId(null)).to.be.rejectedWith('organizationId is required'); + await expect(instance.allByOrganizationId(undefined)).to.be.rejectedWith('organizationId is required'); + }); + }); + + describe('allByProjectName', () => { + it('returns all projects with the specified name', async () => { + const mockProjects = [ + { getId: () => 'p12345', getProjectName: () => 'Test Project' }, + ]; + instance.allByProjectName = stub().resolves(mockProjects); + + const result = await instance.allByProjectName('Test Project'); + + expect(result).to.deep.equal(mockProjects); + expect(instance.allByProjectName).to.have.been.calledOnceWithExactly('Test Project'); + }); + + it('returns empty array when no projects found with the name', async () => { + instance.allByProjectName = stub().resolves([]); + + const result = await instance.allByProjectName('Nonexistent Project'); + + expect(result).to.deep.equal([]); + expect(instance.allByProjectName).to.have.been.calledOnceWithExactly('Nonexistent Project'); + }); + + it('throws error for empty project name', async () => { + instance.allByProjectName = stub().rejects(new Error('projectName is required')); + + await expect(instance.allByProjectName('')).to.be.rejectedWith('projectName is required'); + await expect(instance.allByProjectName(null)).to.be.rejectedWith('projectName is required'); + await expect(instance.allByProjectName(undefined)).to.be.rejectedWith('projectName is required'); + }); + }); + + describe('findByProjectName', () => { + it('returns a single project with the specified name', async () => { + const mockProject = { getId: () => 'p12345', getProjectName: () => 'Test Project' }; + instance.findByProjectName = stub().resolves(mockProject); + + const result = await instance.findByProjectName('Test Project'); + + expect(result).to.deep.equal(mockProject); + expect(instance.findByProjectName).to.have.been.calledOnceWithExactly('Test Project'); + }); + + it('returns null when no project found with the name', async () => { + instance.findByProjectName = stub().resolves(null); + + const result = await instance.findByProjectName('Nonexistent Project'); + + expect(result).to.be.null; + expect(instance.findByProjectName).to.have.been.calledOnceWithExactly('Nonexistent Project'); + }); + + it('throws error for empty project name', async () => { + instance.findByProjectName = stub().rejects(new Error('projectName is required')); + + await expect(instance.findByProjectName('')).to.be.rejectedWith('projectName is required'); + await expect(instance.findByProjectName(null)).to.be.rejectedWith('projectName is required'); + await expect(instance.findByProjectName(undefined)).to.be.rejectedWith('projectName is required'); + }); + }); + + describe('findByOrganizationIdAndProjectName', () => { + it('returns a project when both organization ID and project name match', async () => { + const mockProject = { + getId: () => 'p12345', + getOrganizationId: () => 'org123', + getProjectName: () => 'Test Project', + }; + instance.findByOrganizationIdAndProjectName = stub().resolves(mockProject); + + const result = await instance.findByOrganizationIdAndProjectName('org123', 'Test Project'); + + expect(result).to.deep.equal(mockProject); + expect(instance.findByOrganizationIdAndProjectName) + .to.have.been.calledOnceWithExactly('org123', 'Test Project'); + }); + + it('returns null when no project found with the criteria', async () => { + instance.findByOrganizationIdAndProjectName = stub().resolves(null); + + const result = await instance.findByOrganizationIdAndProjectName('org123', 'Nonexistent Project'); + + expect(result).to.be.null; + expect(instance.findByOrganizationIdAndProjectName) + .to.have.been.calledOnceWithExactly('org123', 'Nonexistent Project'); + }); + + it('throws error for empty organization ID', async () => { + instance.findByOrganizationIdAndProjectName = stub() + .rejects(new Error('organizationId is required')); + + await expect(instance.findByOrganizationIdAndProjectName('', 'Test Project')) + .to.be.rejectedWith('organizationId is required'); + }); + + it('throws error for empty project name', async () => { + instance.findByOrganizationIdAndProjectName = stub() + .rejects(new Error('projectName is required')); + + await expect(instance.findByOrganizationIdAndProjectName('org123', '')) + .to.be.rejectedWith('projectName is required'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/models/project/project.model.test.js b/packages/spacecat-shared-data-access/test/unit/models/project/project.model.test.js new file mode 100644 index 000000000..c8d73ddaa --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/models/project/project.model.test.js @@ -0,0 +1,101 @@ +/* + * Copyright 2024 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. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai'; + +import { stub } from 'sinon'; +import { createElectroMocks } from '../../util.js'; +import projectsFixture from '../../../fixtures/projects.fixture.js'; +import { Project } from '../../../../src/index.js'; + +describe('Project Model', () => { + let project; + const mockRecord = projectsFixture[0]; + + let mockElectroService; + + beforeEach(() => { + ({ + mockElectroService, + model: project, + } = createElectroMocks(Project, mockRecord)); + + mockElectroService.entities.patch = stub().returns({ set: stub() }); + }); + + describe('getProjectName', () => { + it('should return the project name', () => { + expect(project).to.be.an('object'); + expect(project.getProjectName()).to.equal('Test Project 1'); + }); + }); + + describe('getOrganizationId', () => { + it('should return the organization ID', () => { + expect(project.getOrganizationId()).to.equal('4854e75e-894b-4a74-92bf-d674abad1423'); + }); + }); + + describe('setProjectName', () => { + it('should set the project name and return the instance', () => { + const result = project.setProjectName('New Project Name'); + expect(result).to.equal(project); + expect(project.getProjectName()).to.equal('New Project Name'); + }); + }); + + describe('setOrganizationId', () => { + it('should set the organization ID and return the instance', () => { + const result = project.setOrganizationId('1e9c6f94-f226-41f3-9005-4bb766765ac2'); + expect(result).to.equal(project); + expect(project.getOrganizationId()).to.equal('1e9c6f94-f226-41f3-9005-4bb766765ac2'); + }); + }); + + describe('getPrimaryLocaleSites', () => { + it('should return an empty array when no sites exist', async () => { + project.getSites = stub().resolves([]); + + const result = await project.getPrimaryLocaleSites(); + + expect(result).to.be.an('array'); + expect(result).to.have.lengthOf(0); + }); + + it('should return only sites where getPrimaryLocale is true', async () => { + const mockSite1 = { + getIsPrimaryLocale: stub().returns(false), + getBaseURL: () => 'https://example1.com', + }; + const mockSite2 = { + getIsPrimaryLocale: stub().returns(true), + getBaseURL: () => 'https://example2.com', + }; + const mockSite3 = { + getIsPrimaryLocale: stub().returns(false), + getBaseURL: () => 'https://example3.com', + }; + const mockSite4 = { + getIsPrimaryLocale: stub().returns(undefined), + getBaseURL: () => 'https://example4.com', + }; + + project.getSites = stub().resolves([mockSite1, mockSite2, mockSite3, mockSite4]); + + const result = await project.getPrimaryLocaleSites(); + + expect(result).to.deep.equal([mockSite2]); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/models/site/site.collection.test.js b/packages/spacecat-shared-data-access/test/unit/models/site/site.collection.test.js index 3234ca218..a29a79c45 100755 --- a/packages/spacecat-shared-data-access/test/unit/models/site/site.collection.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/site/site.collection.test.js @@ -189,4 +189,208 @@ describe('SiteCollection', () => { .to.be.rejectedWith(`Unsupported preview URL: ${invalidUrl}`); }); }); + + describe('new project-related methods', () => { + let mockProjectCollection; + let mockProject; + let mockSites; + + beforeEach(() => { + mockProject = { + getId: () => 'project-123', + getOrganizationId: () => 'org-123', + getProjectName: () => 'Test Project', + }; + + mockSites = [ + { getId: () => 'site-1', getProjectId: () => 'project-123' }, + { getId: () => 'site-2', getProjectId: () => 'project-123' }, + ]; + + mockProjectCollection = { + findByProjectName: stub(), + findById: stub(), + allByOrganizationId: stub(), + }; + + mockEntityRegistry.getCollection = stub().returns(mockProjectCollection); + }); + + describe('allByProjectName', () => { + it('should return sites for a valid project name', async () => { + mockProjectCollection.findByProjectName.resolves(mockProject); + instance.allByProjectId = stub().resolves(mockSites); + + const result = await instance.allByProjectName('Test Project'); + + expect(result).to.deep.equal(mockSites); + expect(mockProjectCollection.findByProjectName).to.have.been.calledOnceWith('Test Project'); + expect(instance.allByProjectId).to.have.been.calledOnceWith('project-123'); + }); + + it('should return empty array when project is not found', async () => { + mockProjectCollection.findByProjectName.resolves(null); + + const result = await instance.allByProjectName('Non-existent Project'); + + expect(result).to.deep.equal([]); + expect(mockProjectCollection.findByProjectName).to.have.been.calledOnceWith('Non-existent Project'); + }); + + it('should throw error for empty project name', async () => { + await expect(instance.allByProjectName('')).to.be.rejectedWith('projectName is required'); + await expect(instance.allByProjectName(null)).to.be.rejectedWith('projectName is required'); + await expect(instance.allByProjectName(undefined)).to.be.rejectedWith('projectName is required'); + }); + }); + + describe('allByProjectId', () => { + it('should return sites for a valid project ID', async () => { + instance.allByProjectId = stub().resolves(mockSites); + + const result = await instance.allByProjectId('project-123'); + + expect(result).to.deep.equal(mockSites); + expect(instance.allByProjectId).to.have.been.calledOnceWith('project-123'); + }); + + it('should throw error for empty project ID', async () => { + await expect(instance.allByProjectId('')).to.be.rejectedWith('projectId is required'); + await expect(instance.allByProjectId(null)).to.be.rejectedWith('projectId is required'); + await expect(instance.allByProjectId(undefined)).to.be.rejectedWith('projectId is required'); + }); + }); + + describe('allByOrganizationIdAndProjectId', () => { + let mockOrganizationCollection; + let mockOrganization; + + beforeEach(() => { + mockOrganization = { + getId: () => 'org-123', + }; + mockOrganizationCollection = { + findById: stub(), + }; + instance.entityRegistry.getCollection.withArgs('OrganizationCollection').returns(mockOrganizationCollection); + }); + + it('should return sites when organization and project exist', async () => { + mockOrganizationCollection.findById.resolves(mockOrganization); + mockProjectCollection.allByOrganizationId.resolves([mockProject]); + instance.allByProjectId = stub().resolves(mockSites); + + const result = await instance.allByOrganizationIdAndProjectId('org-123', 'project-123'); + + expect(result).to.deep.equal(mockSites); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + expect(mockProjectCollection.allByOrganizationId).to.have.been.calledOnceWith('org-123'); + expect(instance.allByProjectId).to.have.been.calledOnceWith('project-123'); + }); + + it('should return empty array when organization does not exist', async () => { + mockOrganizationCollection.findById.resolves(null); + + const result = await instance.allByOrganizationIdAndProjectId('org-123', 'project-123'); + + expect(result).to.deep.equal([]); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + }); + + it('should return empty array when project is not found in organization', async () => { + mockOrganizationCollection.findById.resolves(mockOrganization); + mockProjectCollection.allByOrganizationId.resolves([]); + + const result = await instance.allByOrganizationIdAndProjectId('org-123', 'project-123'); + + expect(result).to.deep.equal([]); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + expect(mockProjectCollection.allByOrganizationId).to.have.been.calledOnceWith('org-123'); + }); + + it('should throw error for empty organization ID', async () => { + await expect(instance.allByOrganizationIdAndProjectId('', 'project-123')) + .to.be.rejectedWith('organizationId is required'); + }); + + it('should throw error for empty project ID', async () => { + await expect(instance.allByOrganizationIdAndProjectId('org-123', '')) + .to.be.rejectedWith('projectId is required'); + }); + }); + + describe('allByOrganizationIdAndProjectName', () => { + let mockOrganizationCollection; + let mockOrganization; + + beforeEach(() => { + mockOrganization = { + getId: () => 'org-123', + }; + mockOrganizationCollection = { + findById: stub(), + }; + instance.entityRegistry.getCollection.withArgs('OrganizationCollection').returns(mockOrganizationCollection); + }); + + it('should return sites when organization and project exist', async () => { + mockOrganizationCollection.findById.resolves(mockOrganization); + mockProjectCollection.allByOrganizationId.resolves([mockProject]); + instance.allByProjectId = stub().resolves(mockSites); + + const result = await instance.allByOrganizationIdAndProjectName('org-123', 'Test Project'); + + expect(result).to.deep.equal(mockSites); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + expect(mockProjectCollection.allByOrganizationId).to.have.been.calledOnceWith('org-123'); + expect(instance.allByProjectId).to.have.been.calledOnceWith('project-123'); + }); + + it('should return empty array when organization does not exist', async () => { + mockOrganizationCollection.findById.resolves(null); + + const result = await instance.allByOrganizationIdAndProjectName('org-123', 'Test Project'); + + expect(result).to.deep.equal([]); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + }); + + it('should return empty array when project is not found in organization', async () => { + mockOrganizationCollection.findById.resolves(mockOrganization); + mockProjectCollection.allByOrganizationId.resolves([]); + + const result = await instance.allByOrganizationIdAndProjectName('org-123', 'Non-existent Project'); + + expect(result).to.deep.equal([]); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + expect(mockProjectCollection.allByOrganizationId).to.have.been.calledOnceWith('org-123'); + }); + + it('should return empty array when project name does not match', async () => { + const differentProject = { + getId: () => 'project-456', + getOrganizationId: () => 'org-123', + getProjectName: () => 'Different Project', + }; + mockOrganizationCollection.findById.resolves(mockOrganization); + mockProjectCollection.allByOrganizationId.resolves([differentProject]); + + const result = await instance.allByOrganizationIdAndProjectName('org-123', 'Test Project'); + + expect(result).to.deep.equal([]); + expect(mockOrganizationCollection.findById).to.have.been.calledOnceWith('org-123'); + expect(mockProjectCollection.allByOrganizationId).to.have.been.calledOnceWith('org-123'); + }); + + it('should throw error for empty organization ID', async () => { + await expect(instance.allByOrganizationIdAndProjectName('', 'Test Project')) + .to.be.rejectedWith('organizationId is required'); + }); + + it('should throw error for empty project name', async () => { + await expect(instance.allByOrganizationIdAndProjectName('org-123', '')) + .to.be.rejectedWith('projectName is required'); + }); + }); + }); }); diff --git a/packages/spacecat-shared-data-access/test/unit/models/site/site.model.test.js b/packages/spacecat-shared-data-access/test/unit/models/site/site.model.test.js index db5a86a38..896578cef 100755 --- a/packages/spacecat-shared-data-access/test/unit/models/site/site.model.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/site/site.model.test.js @@ -394,4 +394,55 @@ describe('SiteModel', () => { expect(() => instance.setpageTypes(missingPattern)).to.throw(); }); }); + + describe('localization fields', () => { + describe('primaryLocale', () => { + it('gets primaryLocale', () => { + expect(instance.getIsPrimaryLocale()).to.equal(undefined); + }); + + it('sets primaryLocale', () => { + instance.setIsPrimaryLocale(false); + expect(instance.getIsPrimaryLocale()).to.equal(false); + }); + }); + + describe('language', () => { + it('gets language', () => { + expect(instance.getLanguage()).to.equal(undefined); + }); + + it('sets language (ISO 639-1)', () => { + instance.setLanguage('en'); + expect(instance.getLanguage()).to.equal('en'); + }); + }); + + describe('region', () => { + it('gets region', () => { + expect(instance.getRegion()).to.equal(undefined); + }); + + it('sets region', () => { + instance.setRegion('US'); + expect(instance.getRegion()).to.equal('US'); + }); + }); + + describe('projectId', () => { + it('gets projectId', () => { + expect(instance.getProjectId()).to.equal('f47ac10b-58cc-4372-a567-0e02b2c3d479'); + }); + + it('sets projectId', () => { + instance.setProjectId('1e9c6f94-f226-41f3-9005-4bb766765ac2'); + expect(instance.getProjectId()).to.equal('1e9c6f94-f226-41f3-9005-4bb766765ac2'); + }); + + it('throws for invalid projectId format', () => { + expect(() => instance.setProjectId('invalid-id')).to.throw(); + expect(() => instance.setProjectId('123')).to.throw(); + }); + }); + }); });