From bc79ad38039df1a742648db1d36f4a6a1ba679e9 Mon Sep 17 00:00:00 2001
From: Casey Waldren <cwaldren@launchdarkly.com>
Date: Thu, 12 Dec 2024 11:13:33 -0800
Subject: [PATCH] feat: add releases product to api-js

---
 api-js/src/SDKMeta.ts    | 33 ++++++++++++++++++++++++++
 api-js/tests/e2e.test.ts | 51 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 81 insertions(+), 3 deletions(-)

diff --git a/api-js/src/SDKMeta.ts b/api-js/src/SDKMeta.ts
index 169c4fa..a97d2e7 100644
--- a/api-js/src/SDKMeta.ts
+++ b/api-js/src/SDKMeta.ts
@@ -3,6 +3,7 @@ import sdkRepos from './data/repos.json'
 import sdkNames from './data/names.json'
 import sdkTypes from './data/types.json'
 import sdkPopularity from './data/popularity.json'
+import sdkReleases from './data/releases.json'
 
 export enum Type {
     // ClientSide is an SDK that runs in a client scenario.
@@ -29,9 +30,41 @@ export const Languages: Record<string, string[]> = sdkLanguages;
 export const Names: Record<string, string> = sdkNames;
 export const Repos: Record<string, Repo> = sdkRepos;
 export const Popularity: Record<string, number> = sdkPopularity;
+export const Releases: ReleaseList = Object.fromEntries(
+    Object.entries(sdkReleases).map(([key, value]) => [
+      key,
+      value.map((release: any) => ({
+          Major: release["major"],
+          Minor: release["minor"],
+          Date: new Date(release["date"]),
+          EOL: release["eol"] ? new Date(release["eol"]) : null
+      }))
+    ]));
 
 export const Types: Record<string, Type> = Object.fromEntries(
     Object.entries(sdkTypes).map(([key, value]) => [
       key,
       isType(value) ? value : Type.Unknown
     ]));
+
+
+export interface Release {
+    Major: number;
+    Minor: number;
+    Date: Date;
+    EOL: Date | null;
+}
+
+export interface ReleaseList {
+    [key: string]: Release[];
+}
+
+export namespace ReleaseHelpers {
+    export const IsLatest = (release: Release) => release.EOL === null;
+    export const IsEOL = (release: Release, now: Date) => !IsLatest(release) && now > release.EOL!;
+    export const IsApproachingEOL = (release: Release, now: Date, thresholdPrior: number) =>
+        !IsLatest(release) && now.getTime() + thresholdPrior > release.EOL!.getTime();
+
+    export const Earliest = (releases: Release[]) => releases[releases.length - 1];
+    export const Latest = (releases: Release[]) => releases[0];
+}
diff --git a/api-js/tests/e2e.test.ts b/api-js/tests/e2e.test.ts
index bd231af..98ee044 100644
--- a/api-js/tests/e2e.test.ts
+++ b/api-js/tests/e2e.test.ts
@@ -1,4 +1,4 @@
-import { Names, Repos, Types, Type, Popularity, Languages } from '../src/SDKMeta';
+import { Names, Repos, Types, Type, Popularity, Languages, Releases, ReleaseHelpers } from '../src/SDKMeta';
 
 test('names', () => {
     expect(Names['node-server']).toBe('Node.js Server SDK');
@@ -17,8 +17,53 @@ test('types', () => {
     expect(Types['node-server']).toBe('server-side');
 });
 
-
-
 test('popularity', () => {
     expect(Popularity['node-server']).toBe(2);
 });
+
+test('releases', () => {
+    const firstNodeReleaseDate = new Date("2015-05-13T16:55:00Z");
+    const firstNodeReleaseEOL = new Date("2016-09-12T00:00:00Z");
+
+    expect(Releases['node-server'].length).toBeGreaterThanOrEqual(1);
+
+    const firstRelease = ReleaseHelpers.Earliest(Releases['node-server']);
+    expect(firstRelease.Major).toBe(1);
+    expect(firstRelease.Minor).toBe(0);
+    expect(ReleaseHelpers.IsLatest(firstRelease)).toBe(false);
+
+    expect(firstRelease.Date).toEqual(firstNodeReleaseDate);
+    expect(firstRelease.EOL).not.toBeNull();
+    expect(firstRelease.EOL).toEqual(firstNodeReleaseEOL);
+
+    const latestRelease = ReleaseHelpers.Latest(Releases['node-server']);
+    expect(latestRelease.Major).toBeGreaterThanOrEqual(9);
+    expect(latestRelease.Minor).toBeGreaterThanOrEqual(4);
+    expect(latestRelease.EOL).toBeNull();
+    expect(ReleaseHelpers.IsLatest(latestRelease)).toBe(true);
+})
+
+test('eol calculations', () => {
+  const releases = Releases['node-server'];
+    const earliest = ReleaseHelpers.Earliest(releases);
+    const latest = ReleaseHelpers.Latest(releases);
+    const earliestEOL = new Date("2016-09-12T00:00:00Z");
+
+    // Checking that the latest release is not yet EOL
+    expect(ReleaseHelpers.IsEOL(latest, new Date())).toBe(false);
+    // Checking that the earliest release becomes EOL if we pass in a "current" date of its EOL + 1 second
+    expect(ReleaseHelpers.IsEOL(earliest, new Date(earliestEOL.getTime() + 1000))).toBe(true);
+
+    // Check the "approaching EOL" logic for the earliest release by passing in different values of "current" date.
+    const minute = 60 * 1000;
+    const hour = 60 * minute;
+    const hour_and_1_minute = 61 * minute;
+    const fifty_nine_minutes = 59 * minute;
+    const thirty_minutes = 30 * minute;
+
+    expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - hour_and_1_minute), hour)).toBe(false);
+    expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - hour), hour)).toBe(false);
+    expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - fifty_nine_minutes), hour)).toBe(true);
+    expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - thirty_minutes), hour)).toBe(true);
+    expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - minute), hour)).toBe(true);
+})