From fe27a56473f76950baee8bad2269516773f89aeb Mon Sep 17 00:00:00 2001 From: Ioan Lucut Date: Tue, 8 Aug 2023 20:38:57 +0300 Subject: [PATCH] Add a first inventory snapshot feature (for CI/CD) --- src/core.ts | 37 ++++++++- src/index.ts | 1 + src/inventorySnapshotRunner.ts | 141 +++++++++++++++++++++++++++++++++ src/types.ts | 19 +++++ 4 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 src/inventorySnapshotRunner.ts diff --git a/src/core.ts b/src/core.ts index 810cd4d..2df324f 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,6 +1,15 @@ -import { first, last, parseInt, trim } from 'lodash'; import chalk from 'chalk'; -import { SequenceChar } from './types'; +import { + filter, + first, + includes, + last, + parseInt, + size, + trim, + uniq, +} from 'lodash'; +import { SequenceChar, SongMeta } from './types'; import { COLON, COMMA, @@ -10,6 +19,7 @@ import { NEW_LINE_TUPLE, TEST_ENV, } from './constants'; +import assert from 'node:assert'; const MISSING_SEQUENCE_NUMBER = 1; @@ -84,7 +94,7 @@ export const getMetaSectionsFromTitle = (titleContent: string) => { const [sequence, content] = entry.split(COLON); return { ...accumulator, [sequence]: trim(content) }; - }, {}); + }, {}) as Record; }; export const createSongMock = ( @@ -119,3 +129,24 @@ ${tuples export const convertSequenceToNumber = (sequenceOrderQualifier: string) => parseInt(sequenceOrderQualifier) || MISSING_SEQUENCE_NUMBER; + +export const getTodayAsDateString = () => { + const now = new Date(); + + return ( + ('0' + now.getDate()).slice(-2) + + '-' + + ('0' + (now.getMonth() + 1)).slice(-2) + + '-' + + now.getFullYear() + ); +}; + +export const assertUniqueness = (array: string[]) => + assert.equal( + size(uniq(array)), + size(array), + `There are duplicates: ${filter(array, (value, index, iteratee) => + includes(iteratee, value, index + 1), + ).join(COMMA)}`, + ); diff --git a/src/index.ts b/src/index.ts index f979d07..b137404 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export * from './inventorySnapshotRunner'; export * from './migratorRunner'; export * from './proPresenter7SongConverter'; export * from './songsParser'; diff --git a/src/inventorySnapshotRunner.ts b/src/inventorySnapshotRunner.ts new file mode 100644 index 0000000..a76b0a1 --- /dev/null +++ b/src/inventorySnapshotRunner.ts @@ -0,0 +1,141 @@ +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; +import recursive from 'recursive-readdir'; +import { isEqual, last, reject } from 'lodash'; +import { + assertUniqueness, + getMetaSectionsFromTitle, + getSongInSectionTuples, + getTitleWithoutMeta, + getTodayAsDateString, + logFileWithLinkInConsole, + logProcessingFile, +} from './core'; +import assert from 'node:assert'; +import { + SongInventoryChange, + SongMeta, + SongsInventory, + SongsInventoryEntry, +} from './types'; +import { TXT_EXTENSION } from './constants'; + +dotenv.config(); + +const inventoryChangelogFilename = path.join( + __dirname, + '..', + 'inventoryChangelog.json', +); + +const snapshotInventory = async (dir: string) => { + console.log(`"Reprocessing file names from ${dir} directory.."`); + + const currentInventory = JSON.parse( + fs.readFileSync(inventoryChangelogFilename).toString(), + ) as SongsInventory; + + const { songs } = currentInventory; + + const songsAsHashMap = songs.reduce( + (accumulator, entry) => ({ + ...accumulator, + [entry.id]: entry, + }), + {} as { + [id: string]: SongsInventoryEntry; + }, + ); + + const songsMeta = (await recursive(dir)) + .filter((filePath) => path.extname(filePath) === TXT_EXTENSION) + .map((filePath) => { + const filename = path.basename(filePath); + const fileContent = fs.readFileSync(filePath).toString(); + + logProcessingFile(filename, 'inventory generation'); + logFileWithLinkInConsole(filePath); + + const rawTitle = getSongInSectionTuples(fileContent)[1]; + + assert.ok( + rawTitle.includes(SongMeta.CONTENT_HASH), + `The ${SongMeta.CONTENT_HASH} should be defined.`, + ); + + return { + filename, + metaSections: getMetaSectionsFromTitle(rawTitle), + titleWithoutMetaSections: getTitleWithoutMeta(rawTitle), + }; + }); + + assertUniqueness( + songsMeta.map(({ metaSections }) => metaSections[SongMeta.ID]), + ); + + const dateAsString = getTodayAsDateString(); + const updatedSongsInventory = songsMeta.map(({ filename, metaSections }) => { + const id = metaSections[SongMeta.ID]; + const contentHash = metaSections[SongMeta.CONTENT_HASH]; + const currentInventory = songsAsHashMap[id]; + + if (!currentInventory) { + return { + id, + filename, + changes: [ + { + date: dateAsString, + contentHash, + }, + ], + } as SongsInventoryEntry; + } + + const { changes } = currentInventory; + const { + filename: lastChangeFilename, + contentHash: lastChangedContentHash, + date: lastChangedDate, + } = last(changes) as SongInventoryChange; + + if (isEqual(lastChangedContentHash, contentHash)) { + return currentInventory; + } + + const isFilenameChanged = !isEqual(lastChangeFilename, filename); + return { + id, + filename, + changes: [ + ...(isEqual(lastChangedDate, dateAsString) + ? reject(changes, { date: lastChangedDate }) + : changes), + { + date: dateAsString, + contentHash, + ...(isFilenameChanged ? { filename, isFilenameChanged } : {}), + }, + ], + } as SongsInventoryEntry; + }); + + assertUniqueness(Object.keys(updatedSongsInventory).map((id) => id)); + + const nextSongs = updatedSongsInventory.sort((a, b) => + a.filename.localeCompare(b.filename), + ); + fs.writeFileSync( + inventoryChangelogFilename, + JSON.stringify( + isEqual(songs, nextSongs) + ? currentInventory + : { + updatedOn: dateAsString, + songs: nextSongs, + }, + ), + ); +}; diff --git a/src/types.ts b/src/types.ts index f3c3bb6..0659454 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,3 +115,22 @@ export type Song = { verses: Section[]; version?: string; }; + +export type SongInventoryChange = { + date: string; + contentHash: string; + filename: string; + isFilenameChanged?: boolean; + isDeleted?: boolean; +}; + +export type SongsInventoryEntry = { + id: string; + filename: string; + changes: SongInventoryChange[]; +}; + +export type SongsInventory = { + updatedOn: string; + songs: SongsInventoryEntry[]; +};