Skip to content

Commit

Permalink
Merge pull request #2 from cagov/jsdoc
Browse files Browse the repository at this point in the history
Jsdoc
  • Loading branch information
xjensen authored Oct 24, 2023
2 parents ce3cd8a + 40c9ed0 commit 345f48c
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 23 deletions.
3 changes: 1 addition & 2 deletions src/http/get-benefits/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { getDefinitions } = require("@architect/shared/s3");

// Definitions will be loaded from benefits-recs-defs.json in S3.
// We keep it outside the handler to cache it between Lambda runs.
/** @type {import('./node_modules/@architect/shared/s3.js').Definitions} */
let definitions = {};

/** Core function for get-benefits. */
Expand All @@ -19,9 +20,7 @@ exports.handler = arc.http.async(async (req) => {
const language = req.query.language || "en";

const allLinks = assembleLinks(definitions, language, host);
console.log("All:" + allLinks.length);
const links = await applyRules(definitions, allLinks, host);
console.log("Filtered:" + links.length);
const data = {
header: "Apply for more benefits!",
tagline: "You might be able to get:",
Expand Down
44 changes: 35 additions & 9 deletions src/shared/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ const AnalyticEngines = {

/**
* Find the host definition for a given host url.
* We want to match up the origin of the request (host) with our own metadata (hostDefs).
* @param {string} host
* The host page, as a URL (string), from which the widget sent this request.
* @param {import('./s3.js').Host[]} hostDefs
* A list of host objects from the Airtable-derived definitions.
* @returns {import('./s3.js').Host|undefined}
* A matching host definition, or undefined if none are found.
*/
const findHostDef = (host, hostDefs) => {
const pHostUrl = url.parse(host);
Expand All @@ -28,8 +34,13 @@ const findHostDef = (host, hostDefs) => {
/**
* Add query string parameters for the target site's analytics engine.
* @param {string} linkUrl
* The target link's URL as string.
* @param {string} analytics
* @param {any} hostDef
* The target site's analytics engine, represented by a short code. @see AnalyticEngines
* @param {import('./s3.js').Host} hostDef
* A host object from the Airtable-derived definitions.
* @returns {string}
* A modified target link URL, including analytics query parameters if relevant.
*/
const addAnalytics = (linkUrl, analytics, hostDef) => {
const pUrl = url.parse(linkUrl);
Expand All @@ -47,12 +58,33 @@ const addAnalytics = (linkUrl, analytics, hostDef) => {
}
};

/**
* @typedef TargetLink
* @type {object}
* @property {string} id
* The id of this target link.
* @property {string} language
* The language for this target link.
* @property {string} lead
* The "lead text" of this target link.
* @property {string} catalyst
* The "call to action" text of this target link.
* @property {string} url
* The URL of this target link, as a string.
* @property {string} graphic
* The SVG icon for this target link, as a string.
*/

/**
* Construct link objects for the given language and host.
* @param {object} definitions
* @param {import('./s3.js').Definitions} definitions
* A parsed object representing the Airtable-derived `benefits-recs-defs.json` file.
* @param {string} language
* The language for this request, as an ISO 639-1 code.
* @param {string} host
* @returns
* The host page, as a URL (string), from which the widget sent this request.
* @returns {TargetLink[]}
* A processed list of target links in the requested language.
*/
exports.assembleLinks = (definitions, language, host) => {
const { targets, hosts: hostDefs } = definitions;
Expand All @@ -76,9 +108,6 @@ exports.assembleLinks = (definitions, language, host) => {
const catalyst =
translations[langKey]?.catalyst || translations.en.catalyst || "";

const description =
translations[langKey]?.description || translations.en.description || "";

const linkUrl = translations[langKey]?.url || translations.en.url || "";

const analytics =
Expand All @@ -87,11 +116,8 @@ exports.assembleLinks = (definitions, language, host) => {
const urlWithAnalytics = addAnalytics(linkUrl, analytics, hostDef);

return {
linktext: lead, // linktext depreciated
program: catalyst, // program depreciated
lead,
catalyst,
description, // description possibly depreciated
url: urlWithAnalytics,
graphic,
language: langKey,
Expand Down
54 changes: 46 additions & 8 deletions src/shared/rules.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const url = require("./url");
const { getThrottles } = require("./throttles");

/**
* rules.js
* Welcome to rules.
Expand All @@ -8,10 +11,23 @@
* Rules are processed in a specific order.
*/

const url = require("./url");
const { getThrottles } = require("./throttles");
/**
* @callback Rule
* @param {import('./links.js').TargetLink[]} links
* A processed list of all target links in the user's requested language.
* @param {object} params
* A collection of parameters from the request to help inform the rule.
* @param {string} [params.host]
* The host page, as a URL (string), from which the widget sent this request.
* @param {import('./s3.js').Throttle[]} [params.throttles]
* A processed list of target link throttles.
* @returns {import('./links.js').TargetLink[]}
*/

/** Remove links that have exceeded daily throttles. */
/**
* Remove links that have exceeded daily throttles.
* @type {Rule}
*/
const removeThrottledLinks = (links, { throttles }) =>
links.filter((link) => {
const blockingThrottles = throttles.filter(
Expand All @@ -22,29 +38,51 @@ const removeThrottledLinks = (links, { throttles }) =>
return blockingThrottles.length < 1;
});

/** Remove links that point back to the same host site as the widget. */
/**
* Remove links that point back to the same host site as the widget.
* @type {Rule}
*/
const removeLinkBacks = (links, { host }) =>
links.filter((link) => !url.matchHosts(host, link.url));

/** Randomize the order of the links. */
/**
* Randomize the order of the links.
* @type {Rule}
*/
const randomizeOrder = (links) =>
links
.map((link) => ({ link, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ link }) => link);

/** Reduce the list of links to just the top three. */
/**
* Reduce the list of links to just the top three.
* @type {Rule}
*/
const pickTopThree = (links) => links.slice(0, 3);

/** Active rules, in order. */
/**
* Active rules, in order.
* @type {Rule[]}
*/
const rules = [
removeThrottledLinks,
removeLinkBacks,
randomizeOrder,
pickTopThree,
];

/** Apply the rules to the list of links. */
/**
* Apply the rules to the given list of links.
* @param {import('./s3.js').Definitions} definitions
* A parsed object representing the Airtable-derived `benefits-recs-defs.json` file.
* @param {import('./links.js').TargetLink[]} allLinks
* A processed list of all target links in the user's requested language.
* @param {string} host
* The host page, as a URL (string), from which the widget sent this request.
* @returns {Promise<import('./links.js').TargetLink[]>}
* A targetted list of target links.
*/
const applyRules = async (definitions, allLinks, host) => {
const throttles = await getThrottles(definitions);

Expand Down
84 changes: 83 additions & 1 deletion src/shared/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,89 @@ const { GetObjectCommand, S3Client } = require("@aws-sdk/client-s3");

const s3 = new S3Client({});

/** Grab the definitions file from S3. */
/**
* *Definitions* represents the latest content for the benefits recommender.
* It's derived from Airtable, and lives in a file on S3 called `benefits-recs-defs.json`.
* See the schedule/airtable job to understand how definitions are generated.
* @typedef {object} Definitions
* @property {string} name
* The name of the definitions file.
* @property {string} description
* The description of the definitions file.
* @property {string} updated
* The date and time of last update for the definitions file.
* @property {Target[]} targets
* A list of active target links.
* @property {Throttle[]} throttles
* A list of active target link throttles.
* @property {Host[]} hosts
* A list of hosts where the widget is placed.
*/

/**
* *Target* represents a target link in the widget.
* Coming from the Airtable-derived definitons, this object includes all metadata and translations.
* See shared/links.js to understand how these targets are processed by the API.
* @typedef {object} Target
* @property {string} id
* The id of the target.
* @property {object.<string, Translation>} translations
* A list of translations to different languages for this target.
* @property {string[]} throttle_ids
* A list of IDs for applicable throttles.
*/

/**
* The *Translation* provides a *Target* in a different language.
* English language defaults are also included in a *Translation*.
* @typedef {object} Translation
* @property {string} id
* The id of the translation.
* @property {string} url
* The URL of the target for the given language.
* @property {string} icon
* The icon code of the target for the given language. See shared/icons.js.
* @property {string} analytics
* A code for analytics engine on the target site. See shared/links.js.
* @property {string} lead
* The "lead text" for the target in the given language.
* @property {string} catalyst
* The "call to action" text for the target in the given language.
*/

/**
* *Throttle* defines conditions for limiting display of a target.
* @typedef {object} Throttle
* @property {string} id
* The id for the throttle.
* @property {number} [limit]
* A daily clickthrough limit for the target.
* @property {string} [start]
* A start date for the target.
* @property {string} [end]
* An end date for the target.
* @property {string[]} urls
* The URLs to be throttled.
* @property {boolean} [exceeded]
* Determines if the throttle has been exceeded, and the target should be throttled.
* Unlink the other properties here, *exceeded* does not come from the definitions file.
* We define it here in the API, based on the other properties, in real time.
* See shared/throttles.js.
*/

/**
* A *Host* is a collection of related pages where the widget is located.
* @typedef {object} Host
* @property {string} id
* The id of the host.
* @property {string[]} urls
* The URLs associated with this host.
*/

/**
* Grab the definitions file from S3.
* @returns {Promise<Definitions>}
*/
exports.getDefinitions = async () => {
const command = new GetObjectCommand({
Bucket: "cdn.innovation.ca.gov",
Expand Down
11 changes: 10 additions & 1 deletion src/shared/throttles.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
const arc = require("@architect/functions");

/** Retrieve throttles. */
/**
* Based upon the Airtable-derived definitons, this function evaluates the throttles.
* We check DynamoDB to see if each throttle is still within daily clickthrough limits.
* We also look at other parameters, like start and end dates.
* The goal is to see which throttles need to be activated for this request.
* @param {import('./s3.js').Definitions} definitions
* A parsed object representing the Airtable-derived `benefits-recs-defs.json` file.
* @returns {Promise<import('./s3.js').Throttle[]>}
* A hydrated list of target link throttles.
*/
exports.getThrottles = async (definitions) => {
const { throttles: throttleDefs } = definitions;

Expand Down
10 changes: 8 additions & 2 deletions src/shared/url.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const { URL } = require("url");

/**
* url.js
* A collection of functions for comparing URLs.
* We'll be doing a lot of that when targetting links to send to the widget.
*/

const { URL } = require("url");

/**
* Get a URL object from a string, if possible.
* @param {string} input A string representation of the URL.
Expand Down Expand Up @@ -60,6 +60,12 @@ const comparePaths = (pUrl1, pUrl2) => {
return normalizePath(pUrl1) === normalizePath(pUrl2);
};

/**
* See if two URLs have matching hostnames and pathnames.
* @param {URL} pUrl1 A URL object for the first URL.
* @param {URL} pUrl2 A URL object for the second URL.
* @returns {boolean}
*/
const compare = (pUrl1, pUrl2) => {
return compareHosts(pUrl1, pUrl2) && comparePaths(pUrl1, pUrl2);
};
Expand Down

0 comments on commit 345f48c

Please sign in to comment.