diff --git a/src/commands/cache.js b/src/commands/cache.js index fa4e6494..62170ea2 100644 --- a/src/commands/cache.js +++ b/src/commands/cache.js @@ -5,7 +5,7 @@ import { setImmediate } from "node:timers/promises"; // Import Third-party Dependencies import prettyJson from "@topcli/pretty-json"; import * as i18n from "@nodesecure/i18n"; -import { cache } from "@nodesecure/server"; +import { cache, config } from "@nodesecure/server"; export async function main(options) { const { @@ -54,6 +54,7 @@ async function clearCache(full) { }); } + await config.setDefault(); await cache.initPayloadsList({ logging: false, reset: true }); console.log(styleText("green", i18n.getTokenSync("cli.commands.cache.cleared"))); diff --git a/workspaces/cache/README.md b/workspaces/cache/README.md index 49b0fb1b..07b174f5 100644 --- a/workspaces/cache/README.md +++ b/workspaces/cache/README.md @@ -1,6 +1,6 @@ # `cache` -[![version](https://img.shields.io/github/package-json/v/NodeSecure/Cli?filename=workspaces%cache%2Fpackage.json&style=for-the-badge)](https://www.npmjs.com/package/@nodesecure/cache) +[![version](https://img.shields.io/github/package-json/v/NodeSecure/Cli?filename=workspaces%2Fcache%2Fpackage.json&style=for-the-badge)](https://www.npmjs.com/package/@nodesecure/cache) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/cli/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/cli) [![mit](https://img.shields.io/github/license/NodeSecure/Cli?style=for-the-badge)](https://github.com/NodeSecure/cli/blob/master/LICENSE) @@ -11,7 +11,7 @@ Caching layer for NodeSecure CLI and server, handling configuration, analysis pa ## Requirements -- [Node.js](https://nodejs.org/en/) v20 or higher +- [Node.js](https://nodejs.org/en/) v24 or higher ## Getting Started @@ -43,121 +43,5 @@ await cache.setRootPayload(payload); ## API -### `updateConfig(config: AppConfig): Promise` - -Stores a new configuration object in the cache. - -### `getConfig(): Promise` - -Retrieves the current configuration object from the cache. - -### `updatePayload(packageName: string, payload: Payload): void` - -Saves an analysis payload for a given package. - -**Parameters**: -- `pkg` (`string`): Package name (e.g., `"@nodesecure/scanner@6.0.0"`). -- `payload` (`object`): The analysis result to store. - -> [!NOTE] -> Payloads are stored in the user's home directory under `~/.nsecure/payloads/` - -### `getPayload(packageName: string): Payload` - -Loads an analysis payload for a given package. - -**Parameters**: -`pkg` (`string`): Package name. - -### `availablePayloads(): string[]` - -Lists all available payloads (package names) in the cache. - -### `getPayloadOrNull(packageName: string): Payload | null` - -Loads an analysis payload for a given package, or returns `null` if not found. - -**Parameters**: - -- `pkg` (`string`): Package name. - -Returns `null` if not found. - -### `updatePayloadsList(payloadsList: PayloadsList): Promise` - -Updates the internal MRU/LRU and available payloads list. - -**Parameters**: - -- `payloadsList` (`object`): The new payloads list object. - -### `payloadsList(): Promise` - -Retrieves the current MRU/LRU and available payloads list. - -### `initPayloadsList(options: InitPayloadListOptions = {}): Promise` - -Initializes the payloads list, optionally resetting the cache. - -**Parameters**: - -- `options` (`object`, *optional*): - - `logging` (`boolean`, default: `true`): Enable logging. - - `reset` (`boolean`, default: `false`): If `true`, reset the cache before initializing. - -### `removePayload(packageName: string): void` - -Removes a payload for a given package from the cache. - -**Parameters**: -- `pkg` (`string`): Package name. - -### `removeLastMRU(): Promise` - -Removes the least recently used payload if the MRU exceeds the maximum allowed. - -### `setRootPayload(payload: Payload, options: SetRootPayloadOptions = {}): Promise` - -Sets a new root payload, updates MRU/LRU, and manages cache state. - -**Parameters**: - -- `payload` (`object`): The analysis result to set as root. -- `options` (`object`): - - `logging` (`boolean`, default: `true`): Enable logging. - - `local` (`boolean`, default: `false`): Mark the payload as local. - -## Interfaces - -```ts -interface AppConfig { - defaultPackageMenu: string; - ignore: { - flags: Flag[]; - warnings: WarningName[]; - }; - theme?: "light" | "dark"; - disableExternalRequests: boolean; -} - -interface PayloadsList { - mru: string[]; - lru: string[]; - current: string; - availables: string[]; - lastUsed: Record; - root: string | null; -} - -interface LoggingOption { - logging?: boolean; -} - -interface InitPayloadListOptions extends LoggingOption { - reset?: boolean; -} - -interface SetRootPayloadOptions extends LoggingOption { - local?: boolean; -} -``` +- [AppCache](./docs/AppCache.md) +- [FilePersistanceProvider](./docs/FilePersistanceProvider.md) diff --git a/workspaces/cache/docs/AppCache.md b/workspaces/cache/docs/AppCache.md new file mode 100644 index 00000000..12086295 --- /dev/null +++ b/workspaces/cache/docs/AppCache.md @@ -0,0 +1,104 @@ +# AppCache + +## API + +### `updatePayload(packageName: string, payload: Payload): void` + +Saves an analysis payload for a given package. + +**Parameters**: +- `packageName` (`string`): Package name (e.g., `"@nodesecure/scanner@6.0.0"`). +- `payload` (`object`): The analysis result to store. + +> [!NOTE] +> Payloads are stored in the user's home directory under `~/.nsecure/payloads/` + +### `getPayload(packageName: string): Payload` + +Loads an analysis payload for a given package. + +**Parameters**: +`packageName` (`string`): Package name. + +### `availablePayloads(): string[]` + +Lists all available payloads (package names) in the cache. + +### `getPayloadOrNull(packageName: string): Payload | null` + +Loads an analysis payload for a given package, or returns `null` if not found. + +**Parameters**: + +- `packageName` (`string`): Package name. + +Returns `null` if not found. + +### `updatePayloadsList(payloadsList: PayloadsList): Promise` + +Updates the internal MRU/LRU and available payloads list. + +**Parameters**: + +- `payloadsList` (`object`): The new payloads list object. + +### `payloadsList(): Promise` + +Retrieves the current MRU/LRU and available payloads list. + +### `initPayloadsList(options: InitPayloadListOptions = {}): Promise` + +Initializes the payloads list, optionally resetting the cache. + +**Parameters**: + +- `options` (`object`, *optional*): + - `logging` (`boolean`, default: `true`): Enable logging. + - `reset` (`boolean`, default: `false`): If `true`, reset the cache before initializing. + +### `removePayload(packageName: string): void` + +Removes a payload for a given package from the cache. + +**Parameters**: +- `packageName` (`string`): Package name. + +### `removeLastMRU(): Promise` + +Removes the least recently used payload if the MRU exceeds the maximum allowed. + +### `setRootPayload(payload: Payload, options: SetRootPayloadOptions = {}): Promise` + +Sets a new root payload, updates MRU/LRU, and manages cache state. + +**Parameters**: + +- `payload` (`object`): The analysis result to set as root. +- `options` (`object`): + - `logging` (`boolean`, default: `true`): Enable logging. + - `local` (`boolean`, default: `false`): Mark the payload as local. + +## Interfaces + +```ts +interface PayloadsList { + mru: string[]; + lru: string[]; + current: string; + availables: string[]; + lastUsed: Record; + root: string | null; +} + +interface LoggingOption { + logging?: boolean; +} + +interface InitPayloadListOptions extends LoggingOption { + reset?: boolean; +} + +interface SetRootPayloadOptions extends LoggingOption { + local?: boolean; +} +``` diff --git a/workspaces/cache/docs/FilePersistanceProvider.md b/workspaces/cache/docs/FilePersistanceProvider.md new file mode 100644 index 00000000..593b1a2b --- /dev/null +++ b/workspaces/cache/docs/FilePersistanceProvider.md @@ -0,0 +1,77 @@ +# FilePersistanceProvider + +A generic file-based cache provider using [cacache](https://www.npmjs.com/package/cacache) for persistent storage. + +## Usage Example + +```ts +import { FilePersistanceProvider } from "@nodesecure/cache"; + +interface MyData { + name: string; + value: number; +} + +const cache = new FilePersistanceProvider("my-cache-key"); + +// Store data +await cache.set({ name: "example", value: 42 }); + +// Retrieve data +const data = await cache.get(); +console.log(data); // { name: "example", value: 42 } + +// Remove data +await cache.remove(); +``` + +## Interfaces + +```ts +interface BasePersistanceProvider { + get(): Promise; + set(value: T): Promise; + remove(): Promise; +} +``` + +## API + +### `static PATH` + +The base path for the cache storage. + +**Default**: `os.tmpdir()/nsecure-cli` + +### `constructor(cacheKey: string)` + +Creates a new instance of the persistence provider. + +**Parameters**: + +- `cacheKey` (`string`): A unique key to identify the cached data. + +### `get(): Promise` + +Retrieves a cached value by its key. + +**Returns**: + +- `Promise`: The cached value parsed from JSON, or `null` if not found or on error. + +### `set(value: T): Promise` + +Stores a value in the cache. + +**Parameters**: + +- `value` (`T`): The value to cache (will be JSON stringified). + +**Returns**: + +- `Promise`: `true` if the value was successfully stored, `false` on error. + +### `remove(): Promise` + +Removes the cached entry associated with the key. + diff --git a/workspaces/cache/package.json b/workspaces/cache/package.json index b7345d2b..fe499450 100644 --- a/workspaces/cache/package.json +++ b/workspaces/cache/package.json @@ -12,7 +12,7 @@ "build": "tsc", "prepublishOnly": "npm run build", "lint": "eslint src test", - "test": "node --test test/index.test.ts", + "test": "node --test test/**.test.ts", "test:c8": "c8 npm run test" }, "author": "GENTILHOMME Thomas ", diff --git a/workspaces/cache/src/AppCache.ts b/workspaces/cache/src/AppCache.ts new file mode 100644 index 00000000..3610f0ab --- /dev/null +++ b/workspaces/cache/src/AppCache.ts @@ -0,0 +1,240 @@ +// Import Node.js Dependencies +import os from "node:os"; +import path from "node:path"; +import fs from "node:fs"; + +// Import Third-party Dependencies +import cacache from "cacache"; +import type { Payload } from "@nodesecure/scanner"; + +// Import Internal Dependencies +import { type AbstractLogger, createNoopLogger } from "./abstract-logging.ts"; + +// CONSTANTS +const kPayloadsCache = "___payloads"; +const kPayloadsPath = path.join(os.homedir(), ".nsecure", "payloads"); +const kMaxPayloads = 3; +const kSlashReplaceToken = "______"; + +export const CACHE_PATH = path.join(os.tmpdir(), "nsecure-cli"); +export const DEFAULT_PAYLOAD_PATH = path.join(process.cwd(), "nsecure-result.json"); + +export interface PayloadsList { + mru: string[]; + lru: string[]; + current: string; + availables: string[]; + lastUsed: Record; + root: string | null; +} + +export interface LoggingOption { + logging?: boolean; +} + +export interface InitPayloadListOptions extends LoggingOption { + reset?: boolean; +} + +export interface SetRootPayloadOptions extends LoggingOption { + local?: boolean; +} + +export class AppCache { + #logger: AbstractLogger; + + prefix = ""; + startFromZero = false; + + constructor( + logger: AbstractLogger = createNoopLogger() + ) { + this.#logger = logger; + fs.mkdirSync(kPayloadsPath, { recursive: true }); + } + + updatePayload(packageName: string, payload: Payload) { + if (packageName.includes(kSlashReplaceToken)) { + throw new Error(`Invalid package name: ${packageName}`); + } + + const filePath = path.join(kPayloadsPath, packageName.replaceAll("/", kSlashReplaceToken)); + fs.writeFileSync(filePath, JSON.stringify(payload)); + } + + getPayload(packageName: string): Payload { + const filePath = path.join(kPayloadsPath, packageName.replaceAll("/", kSlashReplaceToken)); + + try { + return JSON.parse(fs.readFileSync(filePath, "utf-8")); + } + catch (err) { + this.#logger.error(`[cache|get](pkg: ${packageName}|cache: not found)`); + + throw err; + } + } + + availablePayloads() { + return fs + .readdirSync(kPayloadsPath) + .map((filename) => filename.replaceAll(kSlashReplaceToken, "/")); + } + + getPayloadOrNull(packageName: string): Payload | null { + try { + return this.getPayload(packageName); + } + catch { + return null; + } + } + + async updatePayloadsList(payloadsList: PayloadsList) { + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); + } + + async payloadsList(): Promise { + try { + const { data } = await cacache.get(CACHE_PATH, `${this.prefix}${kPayloadsCache}`); + + return JSON.parse(data.toString()); + } + catch (err) { + this.#logger.error("[cache|get](cache: not found)"); + + throw err; + } + } + + async #initDefaultPayloadsList(options: LoggingOption = {}) { + const { logging = true } = options; + + if (this.startFromZero) { + const payloadsList = { + mru: [], + lru: [], + current: null, + availables: [], + lastUsed: {}, + root: null + }; + + if (logging) { + this.#logger.info("[cache|init](startFromZero)"); + } + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); + + return; + } + + const payload = JSON.parse(fs.readFileSync(DEFAULT_PAYLOAD_PATH, "utf-8")); + const { name, version } = payload.rootDependency; + + const spec = `${name}@${version}`; + const payloadsList = { + mru: [spec], + lru: [], + current: spec, + availables: [], + lastUsed: { + [spec]: Date.now() + }, + root: spec + }; + + if (logging) { + this.#logger.info(`[cache|init](dep: ${spec})`); + } + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); + this.updatePayload(spec, payload); + } + + async initPayloadsList(options: InitPayloadListOptions = {}) { + const { + logging = true, + reset = false + } = options; + + if (reset) { + await cacache.rm.all(CACHE_PATH); + } + + try { + // prevent re-initialization of the cache + await cacache.get(CACHE_PATH, `${this.prefix}${kPayloadsCache}`); + + return; + } + catch { + // Do nothing. + } + const packagesInFolder = this.availablePayloads(); + if (packagesInFolder.length === 0) { + await this.#initDefaultPayloadsList({ logging }); + + return; + } + + if (logging) { + this.#logger.info(`[cache|init](packagesInFolder: ${packagesInFolder})`); + } + + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify({ + availables: packagesInFolder, + current: null, + mru: [], + lru: [] + })); + } + + removePayload(packageName: string) { + const filePath = path.join(kPayloadsPath, packageName.replaceAll("/", kSlashReplaceToken)); + fs.rmSync(filePath, { force: true }); + } + + async removeLastMRU(): Promise { + const { mru, lastUsed, lru, ...cache } = await this.payloadsList(); + if (mru.length < kMaxPayloads) { + return { + ...cache, + mru, + lru, + lastUsed + }; + } + const packageToBeRemoved = Object.keys(lastUsed) + .filter((key) => mru.includes(key)) + .sort((a, b) => lastUsed[a] - lastUsed[b])[0]; + + return { + ...cache, + mru: mru.filter((pkg) => pkg !== packageToBeRemoved), + lru: [...lru, packageToBeRemoved], + lastUsed + }; + } + + async setRootPayload(payload: Payload, options: SetRootPayloadOptions = {}) { + const { logging = true, local = false } = options; + + const { name, version } = payload.rootDependency; + + const pkg = `${name}@${version}${local ? "#local" : ""}`; + this.updatePayload(pkg, payload); + + await this.initPayloadsList({ logging }); + + const { mru, lru, availables, lastUsed } = await this.removeLastMRU(); + + const updatedPayloadsCache = { + mru: [...new Set([...mru, pkg])], + lru, + availables, + lastUsed: { ...lastUsed, [pkg]: Date.now() }, + current: pkg, + root: pkg + }; + await this.updatePayloadsList(updatedPayloadsCache); + } +} diff --git a/workspaces/cache/src/FilePersistanceProvider.ts b/workspaces/cache/src/FilePersistanceProvider.ts new file mode 100644 index 00000000..dadf528e --- /dev/null +++ b/workspaces/cache/src/FilePersistanceProvider.ts @@ -0,0 +1,62 @@ +// Import Node.js Dependencies +import os from "node:os"; +import path from "node:path"; + +// Import Third-party Dependencies +import cacache from "cacache"; + +export interface BasePersistanceProvider { + get(): Promise; + set(value: T): Promise; + remove(): Promise; +} + +export class FilePersistanceProvider implements BasePersistanceProvider { + static PATH = path.join(os.tmpdir(), "nsecure-cli"); + + #cacheKey: string; + + constructor( + cacheKey: string + ) { + this.#cacheKey = cacheKey; + } + + async get(): Promise { + try { + const { data } = await cacache.get( + FilePersistanceProvider.PATH, + this.#cacheKey + ); + + return JSON.parse(data.toString()); + } + catch { + return null; + } + } + + async set( + value: T + ): Promise { + try { + await cacache.put( + FilePersistanceProvider.PATH, + this.#cacheKey, + JSON.stringify(value) + ); + + return true; + } + catch { + return false; + } + } + + async remove() { + await cacache.rm( + FilePersistanceProvider.PATH, + this.#cacheKey + ); + } +} diff --git a/workspaces/cache/src/index.ts b/workspaces/cache/src/index.ts index eb47465c..90d28c3f 100644 --- a/workspaces/cache/src/index.ts +++ b/workspaces/cache/src/index.ts @@ -1,263 +1,2 @@ -// Import Node.js Dependencies -import os from "node:os"; -import path from "node:path"; -import fs from "node:fs"; - -// Import Third-party Dependencies -import cacache from "cacache"; -import type { Flag } from "@nodesecure/flags"; -import type { WarningName } from "@nodesecure/js-x-ray"; -import type { Payload } from "@nodesecure/scanner"; - -// Import Internal Dependencies -import { type AbstractLogger, createNoopLogger } from "./abstract-logging.ts"; - -// CONSTANTS -const kConfigCache = "___config"; -const kPayloadsCache = "___payloads"; -const kPayloadsPath = path.join(os.homedir(), ".nsecure", "payloads"); -const kMaxPayloads = 3; -const kSlashReplaceToken = "______"; - -export const CACHE_PATH = path.join(os.tmpdir(), "nsecure-cli"); -export const DEFAULT_PAYLOAD_PATH = path.join(process.cwd(), "nsecure-result.json"); - -export interface AppConfig { - defaultPackageMenu: string; - ignore: { - flags: Flag[]; - warnings: WarningName[]; - }; - theme?: "light" | "dark"; - disableExternalRequests: boolean; -} - -export interface PayloadsList { - mru: string[]; - lru: string[]; - current: string; - availables: string[]; - lastUsed: Record; - root: string | null; -} - -export interface LoggingOption { - logging?: boolean; -} - -export interface InitPayloadListOptions extends LoggingOption { - reset?: boolean; -} - -export interface SetRootPayloadOptions extends LoggingOption { - local?: boolean; -} - -export class AppCache { - #logger: AbstractLogger; - - prefix = ""; - startFromZero = false; - - constructor( - logger: AbstractLogger = createNoopLogger() - ) { - this.#logger = logger; - fs.mkdirSync(kPayloadsPath, { recursive: true }); - } - - async updateConfig(newValue: AppConfig) { - await cacache.put(CACHE_PATH, kConfigCache, JSON.stringify(newValue)); - } - - async getConfig(): Promise { - const { data } = await cacache.get(CACHE_PATH, kConfigCache); - - return JSON.parse(data.toString()); - } - - updatePayload(packageName: string, payload: Payload) { - if (packageName.includes(kSlashReplaceToken)) { - throw new Error(`Invalid package name: ${packageName}`); - } - - const filePath = path.join(kPayloadsPath, packageName.replaceAll("/", kSlashReplaceToken)); - fs.writeFileSync(filePath, JSON.stringify(payload)); - } - - getPayload(packageName: string): Payload { - const filePath = path.join(kPayloadsPath, packageName.replaceAll("/", kSlashReplaceToken)); - - try { - return JSON.parse(fs.readFileSync(filePath, "utf-8")); - } - catch (err) { - this.#logger.error(`[cache|get](pkg: ${packageName}|cache: not found)`); - - throw err; - } - } - - availablePayloads() { - return fs - .readdirSync(kPayloadsPath) - .map((filename) => filename.replaceAll(kSlashReplaceToken, "/")); - } - - getPayloadOrNull(packageName: string): Payload | null { - try { - return this.getPayload(packageName); - } - catch { - return null; - } - } - - async updatePayloadsList(payloadsList: PayloadsList) { - await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); - } - - async payloadsList(): Promise { - try { - const { data } = await cacache.get(CACHE_PATH, `${this.prefix}${kPayloadsCache}`); - - return JSON.parse(data.toString()); - } - catch (err) { - this.#logger.error("[cache|get](cache: not found)"); - - throw err; - } - } - - async #initDefaultPayloadsList(options: LoggingOption = {}) { - const { logging = true } = options; - - if (this.startFromZero) { - const payloadsList = { - mru: [], - lru: [], - current: null, - availables: [], - lastUsed: {}, - root: null - }; - - if (logging) { - this.#logger.info("[cache|init](startFromZero)"); - } - await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); - - return; - } - - const payload = JSON.parse(fs.readFileSync(DEFAULT_PAYLOAD_PATH, "utf-8")); - const { name, version } = payload.rootDependency; - - const spec = `${name}@${version}`; - const payloadsList = { - mru: [spec], - lru: [], - current: spec, - availables: [], - lastUsed: { - [spec]: Date.now() - }, - root: spec - }; - - if (logging) { - this.#logger.info(`[cache|init](dep: ${spec})`); - } - await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); - this.updatePayload(spec, payload); - } - - async initPayloadsList(options: InitPayloadListOptions = {}) { - const { - logging = true, - reset = false - } = options; - - if (reset) { - await cacache.rm.all(CACHE_PATH); - } - - try { - // prevent re-initialization of the cache - await cacache.get(CACHE_PATH, `${this.prefix}${kPayloadsCache}`); - - return; - } - catch { - // Do nothing. - } - const packagesInFolder = this.availablePayloads(); - if (packagesInFolder.length === 0) { - await this.#initDefaultPayloadsList({ logging }); - - return; - } - - if (logging) { - this.#logger.info(`[cache|init](packagesInFolder: ${packagesInFolder})`); - } - - await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify({ - availables: packagesInFolder, - current: null, - mru: [], - lru: [] - })); - } - - removePayload(packageName: string) { - const filePath = path.join(kPayloadsPath, packageName.replaceAll("/", kSlashReplaceToken)); - fs.rmSync(filePath, { force: true }); - } - - async removeLastMRU(): Promise { - const { mru, lastUsed, lru, ...cache } = await this.payloadsList(); - if (mru.length < kMaxPayloads) { - return { - ...cache, - mru, - lru, - lastUsed - }; - } - const packageToBeRemoved = Object.keys(lastUsed) - .filter((key) => mru.includes(key)) - .sort((a, b) => lastUsed[a] - lastUsed[b])[0]; - - return { - ...cache, - mru: mru.filter((pkg) => pkg !== packageToBeRemoved), - lru: [...lru, packageToBeRemoved], - lastUsed - }; - } - - async setRootPayload(payload: Payload, options: SetRootPayloadOptions = {}) { - const { logging = true, local = false } = options; - - const { name, version } = payload.rootDependency; - - const pkg = `${name}@${version}${local ? "#local" : ""}`; - this.updatePayload(pkg, payload); - - await this.initPayloadsList({ logging }); - - const { mru, lru, availables, lastUsed } = await this.removeLastMRU(); - - const updatedPayloadsCache = { - mru: [...new Set([...mru, pkg])], - lru, - availables, - lastUsed: { ...lastUsed, [pkg]: Date.now() }, - current: pkg, - root: pkg - }; - await this.updatePayloadsList(updatedPayloadsCache); - } -} +export * from "./AppCache.ts"; +export * from "./FilePersistanceProvider.ts"; diff --git a/workspaces/cache/test/FilePersistanceProvider.test.ts b/workspaces/cache/test/FilePersistanceProvider.test.ts new file mode 100644 index 00000000..467899c0 --- /dev/null +++ b/workspaces/cache/test/FilePersistanceProvider.test.ts @@ -0,0 +1,98 @@ +// Import Node.js Dependencies +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import os from "node:os"; +import path from "node:path"; + +// Import Third-party Dependencies +import cacache from "cacache"; + +// Import Internal Dependencies +import { FilePersistanceProvider } from "../src/FilePersistanceProvider.ts"; + +describe("FilePersistanceProvider", () => { + it("PATH should be in os.tmpdir()", () => { + assert.equal( + FilePersistanceProvider.PATH, + path.join(os.tmpdir(), "nsecure-cli") + ); + }); + + describe("get", () => { + it("should return parsed data on success", async(t) => { + const expected = { foo: "bar" }; + t.mock.method(cacache, "get", () => { + return { + data: Buffer.from(JSON.stringify(expected)) + }; + }); + + const provider = new FilePersistanceProvider("test-key"); + const result = await provider.get(); + + assert.deepEqual(result, expected); + }); + + it("should return null on error", async(t) => { + t.mock.method(cacache, "get", () => { + throw new Error("cache miss"); + }); + + const provider = new FilePersistanceProvider("test-key"); + const result = await provider.get(); + + assert.equal(result, null); + }); + }); + + describe("set", () => { + it("should return true on success", async(t) => { + t.mock.method(cacache, "put", () => Promise.resolve()); + + const provider = new FilePersistanceProvider("test-key"); + const result = await provider.set({ foo: "bar" }); + + assert.equal(result, true); + }); + + it("should call cacache.put with correct arguments", async(t) => { + const putMock = t.mock.method(cacache, "put", () => Promise.resolve()); + + const provider = new FilePersistanceProvider<{ foo: string; }>("my-key"); + await provider.set({ foo: "bar" }); + + assert.equal(putMock.mock.calls.length, 1); + assert.deepEqual(putMock.mock.calls[0].arguments, [ + FilePersistanceProvider.PATH, + "my-key", + JSON.stringify({ foo: "bar" }) + ]); + }); + + it("should return false on error", async(t) => { + t.mock.method(cacache, "put", () => { + throw new Error("write error"); + }); + + const provider = new FilePersistanceProvider("test-key"); + const result = await provider.set({ foo: "bar" }); + + assert.equal(result, false); + }); + }); + + describe("remove", () => { + it("should call cacache.rm with correct arguments", async(t) => { + const rmMock = t.mock.method(cacache, "rm", () => Promise.resolve()); + + const provider = new FilePersistanceProvider("remove-key"); + await provider.remove(); + + assert.equal(rmMock.mock.calls.length, 1); + assert.deepEqual(rmMock.mock.calls[0].arguments, [ + FilePersistanceProvider.PATH, + "remove-key" + ]); + }); + }); +}); diff --git a/workspaces/cache/test/index.test.ts b/workspaces/cache/test/index.test.ts index 7e72a95f..bcdfac73 100644 --- a/workspaces/cache/test/index.test.ts +++ b/workspaces/cache/test/index.test.ts @@ -28,13 +28,6 @@ describe("appCache", () => { }); }); - it("should update and get config", async() => { - await appCache.updateConfig({ foo: "bar" } as any); - - const updated = await appCache.getConfig(); - assert.deepEqual(updated, { foo: "bar" }); - }); - it("should write payload into ~/.nsecure/payloads", (t) => { let writePath = ""; let writeValue = ""; diff --git a/workspaces/server/src/cache.ts b/workspaces/server/src/cache.ts index f8fc489a..7cef4c42 100644 --- a/workspaces/server/src/cache.ts +++ b/workspaces/server/src/cache.ts @@ -1,7 +1,6 @@ // Import Third-party Dependencies import { - AppCache, - type AppConfig + AppCache } from "@nodesecure/cache"; // Import Internal Dependencies @@ -10,7 +9,3 @@ import { logger } from "./logger.ts"; export const cache = new AppCache( logger ); - -export type { - AppConfig -}; diff --git a/workspaces/server/src/config.ts b/workspaces/server/src/config.ts index a0461ff4..46e026b9 100644 --- a/workspaces/server/src/config.ts +++ b/workspaces/server/src/config.ts @@ -1,8 +1,11 @@ // Import Third-party Dependencies import { warnings, type WarningName } from "@nodesecure/js-x-ray"; +import type { Flag } from "@nodesecure/flags"; +import { + FilePersistanceProvider +} from "@nodesecure/cache"; // Import Internal Dependencies -import { cache, type AppConfig } from "./cache.ts"; import { logger } from "./logger.ts"; const experimentalWarnings = Object.entries(warnings) @@ -11,59 +14,55 @@ const experimentalWarnings = Object.entries(warnings) // CONSTANTS const kDefaultConfig = { defaultPackageMenu: "info", - ignore: { flags: [], warnings: experimentalWarnings }, + ignore: { + flags: [], + warnings: experimentalWarnings + }, disableExternalRequests: false }; -export async function get(): Promise { - try { - const config = await cache.getConfig(); - - const { - defaultPackageMenu, - ignore: { - flags = [], - warnings = [] - } = {}, - theme, - disableExternalRequests = false - } = config; - logger.info( - // eslint-disable-next-line @stylistic/max-len - `[config|get](defaultPackageMenu: ${defaultPackageMenu}|ignore-flag: ${flags}|ignore-warnings: ${warnings}|theme: ${theme}|disableExternalRequests${disableExternalRequests})` - ); +export interface WebUISettings { + defaultPackageMenu: string; + ignore: { + flags: Flag[]; + warnings: WarningName[]; + }; + theme?: "light" | "dark"; + disableExternalRequests: boolean; +} - return { - defaultPackageMenu, - ignore: { - flags, - warnings - }, - theme, - disableExternalRequests - }; - } - catch (err: any) { - logger.error(`[config|get](error: ${err.message})`); +export function getProvider(): FilePersistanceProvider { + return new FilePersistanceProvider( + "web-ui-settings" + ); +} - await cache.updateConfig(kDefaultConfig); +export async function get(): Promise { + const cache = getProvider(); - logger.info(`[config|get](fallback to default: ${JSON.stringify(kDefaultConfig)})`); + const config = await cache.get(); + logger.info(`[config|get](cache: ${config === null ? "miss" : "hit"})`); + if (config) { + logger.info(`[config|get](${JSON.stringify(config)})`); - return kDefaultConfig; + return config; } + + await setDefault(); + + return kDefaultConfig; } -export async function set(newValue: AppConfig) { +export async function set( + newValue: WebUISettings +) { logger.info(`[config|set](config: ${JSON.stringify(newValue)})`); - try { - await cache.updateConfig(newValue); - logger.info("[config|set](sucess)"); - } - catch (err: any) { - logger.error(`[config|set](error: ${err.message})`); + const cache = getProvider(); + await cache.set(newValue); +} - throw err; - } +export async function setDefault() { + const cache = getProvider(); + await cache.set(kDefaultConfig); } diff --git a/workspaces/server/src/endpoints/config.ts b/workspaces/server/src/endpoints/config.ts index 04722226..bf689e0e 100644 --- a/workspaces/server/src/endpoints/config.ts +++ b/workspaces/server/src/endpoints/config.ts @@ -4,9 +4,6 @@ import type { ServerResponse } from "node:http"; -// Import Third-party Dependencies -import type { AppConfig } from "@nodesecure/cache"; - // Import Internal Dependencies import * as config from "../config.ts"; import { bodyParser } from "./util/bodyParser.ts"; @@ -25,7 +22,7 @@ export async function save( req: IncomingMessage, res: ServerResponse ) { - const data = await bodyParser(req); + const data = await bodyParser(req); await config.set(data); res.statusCode = 204; diff --git a/workspaces/server/src/index.ts b/workspaces/server/src/index.ts index 669052a5..84b7d56f 100644 --- a/workspaces/server/src/index.ts +++ b/workspaces/server/src/index.ts @@ -73,6 +73,7 @@ export function buildServer( export { WebSocketServerInstanciator } from "./websocket/index.ts"; export { logger } from "./logger.ts"; +export * as config from "./config.ts"; export { cache diff --git a/workspaces/server/src/websocket/commands/remove.ts b/workspaces/server/src/websocket/commands/remove.ts index d0ad9d8f..2cd45b5e 100644 --- a/workspaces/server/src/websocket/commands/remove.ts +++ b/workspaces/server/src/websocket/commands/remove.ts @@ -1,5 +1,5 @@ // Import Third-party Dependencies -import type { PayloadsList } from "@nodesecure/cache"; +import type { PayloadsList } from "@nodesecure/cache/dist/AppCache.ts"; // Import Internal Dependencies import { context } from "../websocket.als.ts"; diff --git a/workspaces/server/src/websocket/commands/search.ts b/workspaces/server/src/websocket/commands/search.ts index 5fba6923..6212894e 100644 --- a/workspaces/server/src/websocket/commands/search.ts +++ b/workspaces/server/src/websocket/commands/search.ts @@ -1,6 +1,6 @@ // Import Third-party Dependencies import * as scanner from "@nodesecure/scanner"; -import type { PayloadsList, AppCache } from "@nodesecure/cache"; +import type { PayloadsList, AppCache } from "@nodesecure/cache/dist/AppCache.ts"; // Import Internal Dependencies import { context } from "../websocket.als.ts"; diff --git a/workspaces/server/src/websocket/index.ts b/workspaces/server/src/websocket/index.ts index 139c1f0d..d2edca7a 100644 --- a/workspaces/server/src/websocket/index.ts +++ b/workspaces/server/src/websocket/index.ts @@ -2,7 +2,7 @@ import { WebSocketServer, type WebSocket } from "ws"; import { match } from "ts-pattern"; import type { Logger } from "pino"; -import type { AppCache } from "@nodesecure/cache"; +import type { AppCache } from "@nodesecure/cache/dist/AppCache.ts"; // Import Internal Dependencies import { search } from "./commands/search.ts"; diff --git a/workspaces/server/src/websocket/websocket.types.ts b/workspaces/server/src/websocket/websocket.types.ts index b39827cc..435f0a3d 100644 --- a/workspaces/server/src/websocket/websocket.types.ts +++ b/workspaces/server/src/websocket/websocket.types.ts @@ -1,6 +1,6 @@ // Import Third-party Dependencies import type { WebSocket } from "ws"; -import type { AppCache, PayloadsList } from "@nodesecure/cache"; +import type { AppCache, PayloadsList } from "@nodesecure/cache/dist/AppCache.ts"; import type { Payload } from "@nodesecure/scanner"; // Import Internal Dependencies diff --git a/workspaces/server/test/config.test.ts b/workspaces/server/test/config.test.ts index 477711df..fa9ab9dc 100644 --- a/workspaces/server/test/config.test.ts +++ b/workspaces/server/test/config.test.ts @@ -1,31 +1,39 @@ // Import Node.js Dependencies -import { describe, it, before, after } from "node:test"; +import { describe, it, before, after, beforeEach } from "node:test"; import assert from "node:assert"; // Import Third-party Dependencies -import cacache from "cacache"; import { warnings, type Warning } from "@nodesecure/js-x-ray"; -import { type AppConfig, CACHE_PATH } from "@nodesecure/cache"; +import { + FilePersistanceProvider +} from "@nodesecure/cache"; // Import Internal Dependencies -import { get, set } from "../src/config.ts"; - -// CONSTANTS -const kConfigKey = "___config"; +import { + getProvider, + get, + set, + type WebUISettings +} from "../src/config.ts"; describe("config", () => { - let actualConfig: AppConfig; - + let currentConfig: WebUISettings; + let filePersistance: FilePersistanceProvider; before(async() => { - actualConfig = await get(); + currentConfig = await get(); + }); + + beforeEach(async() => { + filePersistance = getProvider(); + await filePersistance.remove(); }); after(async() => { - await set(actualConfig); + await filePersistance.remove(); + await set(currentConfig); }); it("should get default config from empty cache", async() => { - await cacache.rm(CACHE_PATH, kConfigKey); const value = await get(); assert.deepStrictEqual(value, { @@ -41,32 +49,33 @@ describe("config", () => { }); it("should get config from cache", async() => { - const expectedConfig = { + const expectedConfig: WebUISettings = { defaultPackageMenu: "foo", ignore: { flags: ["foo"], - warnings: ["bar"] + warnings: [] }, - theme: "galaxy", + theme: "light", disableExternalRequests: true }; - await cacache.put(CACHE_PATH, kConfigKey, JSON.stringify(expectedConfig)); + + await filePersistance.set(expectedConfig); const value = await get(); assert.deepStrictEqual(value, expectedConfig); }); it("should set config in cache", async() => { - const expectedConfig = { - defaultPackageMenu: "foz", + const expectedConfig: WebUISettings = { + defaultPackageMenu: "foo", ignore: { flags: ["foz"], - warnings: ["baz"] + warnings: [] }, - theme: "galactic", + theme: "light", disableExternalRequests: true }; - await set(expectedConfig as any); + await set(expectedConfig); const value = await get(); assert.deepStrictEqual(value, expectedConfig); diff --git a/workspaces/server/test/httpServer.test.ts b/workspaces/server/test/httpServer.test.ts index 4e9f6aab..abcffa46 100644 --- a/workspaces/server/test/httpServer.test.ts +++ b/workspaces/server/test/httpServer.test.ts @@ -9,14 +9,13 @@ import stream from "node:stream"; // Import Third-party Dependencies import { get, post, MockAgent, getGlobalDispatcher, setGlobalDispatcher } from "@openally/httpie"; -import { CACHE_PATH } from "@nodesecure/cache"; import * as i18n from "@nodesecure/i18n"; import * as flags from "@nodesecure/flags"; -import cacache from "cacache"; import enableDestroy from "server-destroy"; // Import Internal Dependencies import { buildServer } from "../src/index.ts"; +import * as config from "../src/config.ts"; import * as flagsEndpoint from "../src/endpoints/flags.ts"; // CONSTANTS @@ -26,7 +25,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const JSON_PATH = path.join(__dirname, "fixtures", "httpServer.json"); -const kConfigKey = "___config"; const kGlobalDispatcher = getGlobalDispatcher(); const kMockAgent = new MockAgent(); const kBundlephobiaPool = kMockAgent.get("https://bundlephobia.com"); @@ -186,17 +184,17 @@ describe("httpServer", { concurrency: 1 }, () => { test("GET '/config' should return the config", async() => { const { data: actualConfig } = await get(new URL("/config", kHttpURL)); - const expectedConfig = { + const expectedConfig: config.WebUISettings = { defaultPackageMenu: "foo", ignore: { flags: ["foo"], - warnings: ["bar"] + warnings: [] }, - theme: "galaxy", + theme: "light", disableExternalRequests: true }; - await cacache.put(CACHE_PATH, kConfigKey, JSON.stringify(expectedConfig)); + await config.set(expectedConfig); const result = await get(new URL("/config", kHttpURL)); assert.deepEqual(result.data, expectedConfig); @@ -220,8 +218,8 @@ describe("httpServer", { concurrency: 1 }, () => { assert.equal(status, 204); - const inCache = await cacache.get(CACHE_PATH, kConfigKey); - assert.deepEqual(JSON.parse(inCache.data.toString()), { fooz: "baz" }); + const inCache = await config.get(); + assert.deepEqual(inCache, { fooz: "baz" }); await fetch(new URL("/config", kHttpURL), { method: "PUT",