Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/task/244054/recommendations-qa-w…
Browse files Browse the repository at this point in the history
…orkflow' into task/244054/recommendations-qa-workflow
  • Loading branch information
jag-nahl-airelogic committed Feb 19, 2025
2 parents 2394887 + c272b12 commit b59ceab
Show file tree
Hide file tree
Showing 39 changed files with 10,281 additions and 8,365 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -507,5 +507,7 @@ contentful/migration-scripts/contentful-export-*.json
# Secrets
.env
*.env
.env.dev
.env.master

StrykerOutput/
8 changes: 7 additions & 1 deletion contentful/content-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The project is currently separate from the content-migrations project because th

Note that the `contentful-management` library defaults to the `master` environment if there is a typo in the environment name. This is very undesirable so `validate-environment.js` will fetch all environments from contentful and ensure that `ENVIRONMENT` is one of them.

## Usage
### Usage

1. Add your content update script following the convention of `YYYYMMDD-HHMM-description-of-crud-operation.js`
2. run the update with
Expand Down Expand Up @@ -68,3 +68,9 @@ There is absolutely no confirmation before deletion or anything like that. Make
You don't want to accidentally delete everything somewhere else.
Highly recommend backing up your data first!
## Tests
The test suite can be found in `../tests/tests/`. To run them, navigate to /contentful in the terminal and type `npm run test`.
For details of how to run in debug mode, see the [README.md file](../tests/README.md) in `../tests/`.
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
const deleteEntry = require("../helpers/delete-entry");
const getClient = require("../helpers/get-client");
import deleteEntry from "../helpers/delete-entry.js";
import getClient from "../helpers/get-client.js";

export default async function () {
console.log(`Deleting unlinked headers`);

module.exports = async function () {
const client = await getClient();
const headers = await client.entry.getMany({
query: {
content_type: "header",
},
});

for (const header of headers.items) {
console.log(`Processing header ${header.sys.id}`);
const linked = await client.entry.getMany({
query: {
links_to_entry: header.sys.id,
},
});

if (!linked.items.length) {
console.log(
`Deleting header ${header.sys.id} as it has no links to it.`
);
await deleteEntry(header);
}
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* This script finds all entries which link to deleted/removed content and removes those references.
*/

const getClient = require("../helpers/get-client");
import getClient from "../helpers/get-client.js";

module.exports = async function () {
export default async function () {
const client = await getClient();
await removeInvalidReferences(client);
};
}

/**
* Goes through all entries removing invalid links within them
Expand All @@ -17,31 +17,31 @@ module.exports = async function () {
*/
async function removeInvalidReferences(client) {
const contentTypes = await client.contentType.getMany();
const entryIds = await getAllValidEntryIds(client);
const entryIds = await getAllValidEntryIds({ client, contentTypes });

for (const contentType of contentTypes.items) {
await removeInvalidReferencesForContentType({
client: client,
client,
validEntryIds: entryIds,
contentType: contentType.sys.id,
contentType,
});
}
}

/**
* Finds all valid entryIds within contentful. This has to be split up by content type due to a max limit of 1000 items
*
* @param {ClientAPI} client - Contentful management client instance.
* @param {{client: ClientAPI, contentTypes: object}} params
*/
async function getAllValidEntryIds(client) {
async function getAllValidEntryIds({ client, contentTypes }) {
console.log("Finding all valid entryIds");
const contentTypes = await client.contentType.getMany();
const entryIds = new Set();

for (const contentType of contentTypes.items) {
const entries = await client.entry.getMany({
query: { limit: 1000, content_type: contentType.sys.id },
});
contentType.entries = entries;
entries.items.map((entry) => entryIds.add(entry.sys.id));
}

Expand All @@ -62,10 +62,8 @@ async function removeInvalidReferencesForContentType({
validEntryIds,
contentType,
}) {
console.log(`Processing contentType: ${contentType}`);
const entries = await client.entry.getMany({
query: { limit: 1000, content_type: contentType },
});
console.log(`Processing contentType: ${contentType.sys.id}`);
const entries = contentType.entries;
for (const entry of entries.items) {
for (const [key, fieldValue] of Object.entries(entry.fields)) {
for (const [locale, value] of Object.entries(fieldValue)) {
Expand Down Expand Up @@ -99,22 +97,23 @@ async function processField({
locale,
fieldKey,
fieldValue,
} = {}) {
}) {
if (Array.isArray(fieldValue)) {
await removeMissingReferencesInArray({
client: client,
entry: entry,
validEntryIds: validEntryIds,
locale: locale,
fieldKey: fieldKey,
client,
entry,
validEntryIds,
locale,
fieldKey,
fieldItems: fieldValue,
});
} else if (fieldKey.sys?.type === "Link") {
} else if (fieldValue?.sys?.type === "Link") {
await removeLinkIfMissing({
client: client,
entry: entry,
validEntryIds: validEntryIds,
fieldKey: fieldKey,
client,
entry,
validEntryIds,
fieldKey,
fieldValue,
});
}
}
Expand All @@ -126,18 +125,20 @@ async function processField({
* @param {object} params.entry - The Contentful entry object being processed.
* @param {Set<string>} params.validEntryIds - All valid entry Ids.
* @param {string} params.fieldKey - Key of the field in the entry.
* @param {any} params.fieldValue - Value of the field in the entry.
*/
async function removeLinkIfMissing({
client,
entry,
validEntryIds,
fieldKey,
} = {}) {
if (!validEntryIds.has(fieldKey.sys?.id)) {
fieldValue,
}) {
if (!validEntryIds.has(fieldValue.sys?.id)) {
console.log(
`Removing invalid link in entry "${entry.sys.id}" field "${fieldKey}" value "${fieldKey.sys.id}}".`
`Removing invalid link in entry "${entry.sys.id}" field "${fieldKey}" value "${fieldValue.sys.id}}".`
);
delete entry.fields[fieldKey.sys?.id];
delete entry.fields[fieldKey];
await client.entry.update(
{ entryId: entry.sys.id },
{
Expand Down Expand Up @@ -165,7 +166,7 @@ async function removeMissingReferencesInArray({
locale,
fieldKey,
fieldItems,
} = {}) {
}) {
let updated = false;
let validReferences = [];
for (const reference of fieldItems) {
Expand Down
3 changes: 3 additions & 0 deletions contentful/content-management/changes/test-change.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { jest } from "@jest/globals";

export default jest.fn();
10 changes: 5 additions & 5 deletions contentful/content-management/helpers/delete-entry.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const getClient = require("../helpers/get-client");
import { getAndValidateClient } from "./get-client.js";

module.exports = async function deleteEntry(entry) {
const client = getClient();
if (!!entry.sys.publishedVersion) {
export default async function deleteEntry(entry) {
const client = await getAndValidateClient();
if (entry.sys.publishedVersion) {
console.log(`unpublishing ${entry.sys.id}`);
await client.entry.unpublish({
entryId: entry.sys.id,
Expand All @@ -12,4 +12,4 @@ module.exports = async function deleteEntry(entry) {
await client.entry.delete({
entryId: entry.sys.id,
});
};
}
66 changes: 61 additions & 5 deletions contentful/content-management/helpers/get-client.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,53 @@
const contentful = require("contentful-management");
const validateEnvironment = require("./validate-environment");
import contentfulManagement from "contentful-management";

const { createClient } = contentfulManagement;

module.exports = async function getClient() {
const client = contentful.createClient(
/**
* @typedef {Object} ClientAPI
* @property {function(string, Object): Promise<CursorPaginatedCollection>} getEnvironmentTemplates - Gets all environment templates for an organization
* @property {function(Object): Promise<EnvironmentTemplate>} getEnvironmentTemplate - Gets a specific environment template
* @property {function(string, Object): Promise<EnvironmentTemplate>} createEnvironmentTemplate - Creates an environment template
* @property {function(Object): Promise<Collection>} getSpaces - Gets all spaces
* @property {function(string): Promise<Space>} getSpace - Gets a specific space
* @property {function(Object, string): Promise<Space>} createSpace - Creates a new space
* @property {function(string): Promise<Organization>} getOrganization - Gets a specific organization
* @property {function(Object): Promise<Collection>} getOrganizations - Gets all organizations
* @property {function(Object): Promise<User>} getCurrentUser - Gets the authenticated user
* @property {function(Object): Promise<AppDefinition>} getAppDefinition - Gets an app definition
* @property {function(Object): Promise<PersonalAccessToken>} createPersonalAccessToken - Creates a personal access token
* @property {function(string): Promise<PersonalAccessToken>} getPersonalAccessToken - Gets a personal access token (deprecated)
* @property {function(): Promise<Collection>} getPersonalAccessTokens - Gets all personal access tokens (deprecated)
* @property {function(string): Promise<AccessToken>} getAccessToken - Gets a user's access token
* @property {function(): Promise<Collection>} getAccessTokens - Gets all user access tokens
* @property {function(string, Object): Promise<Collection>} getOrganizationAccessTokens - Gets all organization access tokens
* @property {function(string, Object): Promise<Collection>} getOrganizationUsage - Gets organization usage metrics
* @property {function(string, Object): Promise<Collection>} getSpaceUsage - Gets space usage metrics
* @property {function(Object): Promise<any>} rawRequest - Makes a custom request to the API
*/

/**
* Verifys that the environment specified in .env is a valid contentful environment
* This is important because if it isn't, the management api fails silently
* and falls back to using the master environment
*
* @param {ClientAPI} client
*/
export async function validateEnvironment(client) {
const environments = await client.environment.getMany({
spaceId: process.env.SPACE_ID,
});
const validNames = environments.items.map((env) => env.name);
if (!validNames.includes(process.env.ENVIRONMENT)) {
throw new Error(`Invalid Contentful environment`);
}
}

/**
* Creates client without validation
* @returns {ClientAPI}
*/
export function getClient() {
const client = createClient(
{
accessToken: process.env.MANAGEMENT_TOKEN,
},
Expand All @@ -15,6 +59,18 @@ module.exports = async function getClient() {
},
}
);

return client;
}

/**
* Gets Contentful Management ClientAPI and validates the environment from process.env
* @returns {Promise<ClientAPI>}
*/
export async function getAndValidateClient() {
const client = getClient();
await validateEnvironment(client);
return client;
};
}

export default getAndValidateClient;
30 changes: 30 additions & 0 deletions contentful/content-management/helpers/import-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import "dotenv/config";
import contentfulImport from "contentful-import";
import { existsSync } from "fs";
import { getAndValidateClient } from "./get-client.js";

export default async function importContentfulData() {
const options = {
contentFile: process.env.CONTENT_FILE,
spaceId: process.env.SPACE_ID,
managementToken: process.env.MANAGEMENT_TOKEN,
environmentId: process.env.ENVIRONMENT,
skipContentModel: process.env.SKIP_CONTENT_MODEL === "true",
};

await getAndValidateClient();

if (!existsSync(options.contentFile)) {
throw new Error(`File not found: ${options.contentFile}`);
}

try {
await contentfulImport(options);
console.log(
`Import completed successfully from ${options.contentFile}`
);
} catch (error) {
console.error("Error during import:", error);
throw error;
}
}
43 changes: 7 additions & 36 deletions contentful/content-management/import-content.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
require('dotenv').config();
import "dotenv/config";

const contentfulImport = require('contentful-import');
const fs = require('fs');
const getClient = require("./helpers/get-client");
const deleteContentfulContent = require("./helpers/delete-all-content-and-content-types");
import importContentfulData from "./helpers/import-content";

async function importContentfulData() {
const options = {
contentFile: process.env.CONTENT_FILE,
spaceId: process.env.SPACE_ID,
managementToken: process.env.MANAGEMENT_TOKEN,
environmentId: process.env.ENVIRONMENT,
skipContentModel: process.env.SKIP_CONTENT_MODEL === 'true' ? true : false
};
const client = await getClient();

if (!fs.existsSync(options.contentFile)) {
throw new Error(`File not found: ${options.contentFile}`);
}

if (process.env.DELETE_ALL_DATA == 'true') {
console.log(`Deleting all existing data from ${options.environmentId}`);
await deleteContentfulContent({ client: client });
}

console.log("Starting import with the following options:", options)

try {
await contentfulImport(options);
console.log(`Import completed successfully from ${options.contentFile}`);
} catch (error) {
console.error('Error during import:', error);
throw error;
}
}

importContentfulData()
importContentfulData()
.then(() => console.log("Import completed successfully"))
.catch((error) => {
console.error("Error during import:", error);
});
15 changes: 15 additions & 0 deletions contentful/content-management/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
transform: {
"^.+\\.ts$": [
"ts-jest",
{
useESM: true,
},
],
},
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1.js",
},
testEnvironment: "node",
preset: "ts-jest",
};
Loading

0 comments on commit b59ceab

Please sign in to comment.