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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions genstudio-mlr-claims-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ aio app deploy # Deploy to production
- ✅ **`utils/claimsValidation.ts`** - Claims checking algorithms
- ⚠️ **`app/`** - DO NOT modify (core registration logic)

## Claims Data Sources

This app supports multiple claims data sources:

### Local Claims Provider (Default)
Uses static claims data from `src/genstudiopem/actions/claims/provider/local/claims.js`. Perfect for development and testing.

### AEM Content Fragment Claims Provider (New!)
Fetches claims dynamically from AEM Content Fragments. Ideal for production content management workflows.


#### Edit .env with your AEM instance details and provider flag

AEM_HOST= # author-pxxxx-exxxx.adobeaemcloud.com
CF_FOLDER_PATH= # e.g. /content/dam/us/en/claims
CLAIM_PROVIDER_TYPE= # <aem | local> ## this is required filed with AEM_HOST configuration

## Migration Note

This app has been updated to use:
Expand Down
3 changes: 3 additions & 0 deletions genstudio-mlr-claims-app/app.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ extensions:
inputs:
LOG_LEVEL: info
actionType: getClaims
aemHost: $AEM_HOST
cfFolderPath: $CF_FOLDER_PATH
providerType: $CLAIM_PROVIDER_TYPE
annotations:
require-adobe-auth: true
final: false
1 change: 1 addition & 0 deletions genstudio-mlr-claims-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@adobe/aio-sdk": "^6.0.0",
"@react-spectrum/s2": "^0.9.0",
"core-js": "^3.6.4",
"node-fetch": "^2.6.0",
"number-to-words": "^1.2.4",
"p-retry": "^6.2.1",
"react": "^18.3.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,25 @@ governing permissions and limitations under the License.
const { Core } = require("@adobe/aio-sdk");
const { errorResponse, ValidationError } = require("../utils");
const LocalClaimProvider = require("./provider/local/LocalClaimProvider");
const AEMClaimProvider = require("./provider/aem/AEMClaimProvider");
const AEM = "aem";
const LOCAL = "local";

exports.main = async (params) => {
const logger = Core.Logger("main", { level: params.LOG_LEVEL || "info" });
const actionType = params.actionType || "getClaims";
const claimProvider = new LocalClaimProvider(params, logger);

// Choose provider based on parameters
logger.debug("Action parameters:", JSON.stringify(params, null, 2));
logger.debug("AEM host parameter:", params.aemHost);
logger.debug("Provider type parameter:", params.providerType);

const providerType = params.providerType || (params.providerType === AEM && params.aemHost ? AEM : LOCAL);

const claimProvider = providerType === AEM
? new AEMClaimProvider(params, logger)
: new LocalClaimProvider(params, logger);

try {
switch (actionType) {
case "getClaims":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
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.
*/

const ClaimProvider = require("../ClaimProvider");
const { ValidationError, getBearerToken } = require("../../../utils");
const fetch = require("node-fetch");

/**
* @class AEMClaimProvider
* @description
* AEM Content Fragment implementation of ClaimProvider that fetches raw content fragments from AEM.
* Claims extraction and transformation logic is handled client-side in React components.
*/
class AEMClaimProvider extends ClaimProvider {
constructor(params, logger) {
super(params, logger);
this.aemHost = params.aemHost;
this.cfFolderPath = params.cfFolderPath || "";
}

/**
* Gets raw content fragments from AEM.
* @param {Object} params
* @param {string} params.aemHost - AEM host URL
* @param {string} [params.cfFolderPath] - Content fragment folder path
* @returns {Promise<{statusCode: number, body: {fragments: Array}}>}
*/
async getClaims(params) {
try {
this.validateParams(params);

// Fetch content fragments from AEM
const fragments = await this.fetchContentFragments(params);

return {
statusCode: 200,
body: { fragments: fragments || [] },
};
} catch (error) {
this.logger.error("Error fetching fragments from AEM:", error);
if (error instanceof ValidationError) {
throw error;
}
throw new Error(`Failed to fetch fragments from AEM: ${error.message}`);
}
}

/**
* Validates required parameters for AEM operations.
* @param {Object} params
* @throws {ValidationError}
*/
validateParams(params) {
if (!params.aemHost) {
throw new ValidationError("aemHost parameter is required");
}

if (!params.__ow_headers?.authorization) {
throw new ValidationError("Authorization header is required");
}
}

/**
* Fetches content fragments from AEM.
* @param {Object} params
* @returns {Promise<Array>}
*/
async fetchContentFragments(params) {
const imsToken = getBearerToken(params);
const authHeaders = {
Authorization: `Bearer ${imsToken}`,
"Content-Type": "application/json",
};

let url = `https://${params.aemHost}/adobe/sites/cf/fragments`;
if (this.cfFolderPath.length > 0) {
url = url + "?path=" + this.cfFolderPath;
}

try {
const response = await fetch(url, {
method: "GET",
headers: authHeaders,
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`AEM API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
if(response && typeof response === "object") {
const data = await response.json();
return data.items;
} else {
this.logger.warn("Received no fragments from AEM or unexpected response format");
return [];
}
} catch (error) {
this.logger.error("Error during AEM API request:", error.message);
throw new Error(`Failed to fetch content fragments from AEM: ${error.message}`);
}
}
}

module.exports = AEMClaimProvider;
20 changes: 20 additions & 0 deletions genstudio-mlr-claims-app/src/genstudiopem/actions/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,28 @@ function errorResponse(statusCode, message, logger) {
},
};
}
/**
*
* Extracts the bearer token string from the Authorization header in the request parameters.
*
* @param {object} params action input parameters.
*
* @returns {string|undefined} the token string or undefined if not set in request headers.
*
*/
function getBearerToken(params) {
if (
params.__ow_headers &&
params.__ow_headers.authorization &&
params.__ow_headers.authorization.startsWith("Bearer ")
) {
return params.__ow_headers.authorization.substring("Bearer ".length);
}
return undefined;
}

module.exports = {
ValidationError,
errorResponse,
getBearerToken,
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const APP_METADATA: AppMetadata = {
};

export const GET_CLAIMS_ACTION = "genstudio-mlr-claims-app/get-claims";
export const CF_DISCRIPTION_FIELD = "textAssetMatchText";
export const CLAIM_PROVIDER_TYPE = process.env.CLAIM_PROVIDER_TYPE;

export const VIOLATION_STATUS = {
Valid: "valid",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,107 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/

import { Picker, PickerItem } from "@react-spectrum/s2";
import { Picker, PickerItem, Text, ProgressCircle } from "@react-spectrum/s2";
import React, { Key } from "react";
import { ClaimLibrary } from "../../types";

interface ClaimsLibraryPickerProps {
// eslint-disable-next-line
handleSelectionChange: (key: Key | null) => void;
claimLibraries: ClaimLibrary[];
claimLibraries: any;
selectedKey?: string;
isLoading?: boolean;
error?: string | null;
hasAttemptedFetch?: boolean;
}
const renderWaitingForFragments = () => {
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "12px",
gridArea: "claims"
}}
>
<ProgressCircle aria-label="Loading" isIndeterminate />
<Text>Waiting for fragments to be ready...</Text>
</div>
);
};

const renderNoFragments = () => {
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "12px",
gridArea: "claims"
}}
>
<Text>No AEM fragment present or something went wrong.</Text>
</div>
);
};
export const ClaimsLibraryPicker: React.FC<ClaimsLibraryPickerProps> = ({
handleSelectionChange,
claimLibraries,
selectedKey,
isLoading = false,
error = null,
hasAttemptedFetch = false,
}) => {

// Show loading state when fetching claims
if (isLoading) {
return renderWaitingForFragments();
}

// Show error message if there's an error
if (error && hasAttemptedFetch) {
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "12px",
gridArea: "claims"
}}
>
<Text>{error}</Text>
</div>
);
}

// Show no fragments message when no claims available
if (hasAttemptedFetch && (!claimLibraries || claimLibraries.length === 0)) {
return renderNoFragments();
}

// Don't render anything if we haven't tried fetching yet
if (!hasAttemptedFetch) {
return null;
}

// Show the picker when we have claim libraries
return (
<Picker
placeholder="Select Claims Category..."
selectedKey={selectedKey ?? null}
onSelectionChange={handleSelectionChange}
>
{claimLibraries?.map((library: ClaimLibrary) => (
<PickerItem key={library.id} id={library.id}>
{claimLibraries?.map((library: ClaimLibrary, index: number) => (
<PickerItem key={library.id || `library-${index}`} id={library.id}>
{library.name}
</PickerItem>
))}
Expand Down
Loading