diff --git a/lib/src/commands/pull.test.ts b/lib/src/commands/pull.test.ts index c8a8589..1970675 100644 --- a/lib/src/commands/pull.test.ts +++ b/lib/src/commands/pull.test.ts @@ -1,5 +1,5 @@ import { pull } from "./pull"; -import httpClient from "../http/client"; +import getHttpClient from "../http/client"; import { Component, TextItem } from "../http/types"; import appContext from "../utils/appContext"; import * as path from "path"; @@ -8,7 +8,13 @@ import * as os from "os"; jest.mock("../http/client"); -const mockHttpClient = httpClient as jest.Mocked; +// Create a mock client with a mock 'get' method +const mockHttpClient = { + get: jest.fn(), +}; + +// Make getHttpClient return the mock client +(getHttpClient as jest.Mock).mockReturnValue(mockHttpClient); // Test data factories const createMockTextItem = (overrides: Partial = {}) => ({ @@ -35,7 +41,7 @@ const createMockComponent = (overrides: Partial = {}) => ({ folderId: null, variantId: null, ...overrides, -}) +}); const createMockVariable = (overrides: any = {}) => ({ id: "var-1", @@ -49,8 +55,16 @@ const createMockVariable = (overrides: any = {}) => ({ }); // Helper functions -const setupMocks = ({ textItems = [], components = [], variables = [] }: { textItems: TextItem[]; components?: Component[]; variables?: any[] }) => { - mockHttpClient.get.mockImplementation((url: string) => { +const setupMocks = ({ + textItems = [], + components = [], + variables = [], +}: { + textItems: TextItem[]; + components?: Component[]; + variables?: any[]; +}) => { + mockHttpClient.get.mockImplementation((url: string, config?: any) => { if (url.includes("/v2/textItems")) { return Promise.resolve({ data: textItems }); } @@ -122,7 +136,7 @@ describe("pull command - end-to-end tests", () => { const mockTextItem = createMockTextItem(); const mockComponent = createMockComponent(); - setupMocks({ textItems: [mockTextItem], components: [mockComponent]}); + setupMocks({ textItems: [mockTextItem], components: [mockComponent] }); // Set up appContext - this is what actually drives the test appContext.setProjectConfig({ @@ -132,7 +146,7 @@ describe("pull command - end-to-end tests", () => { outputs: [{ format: "json", outDir: outputDir }], }); - await pull(); + await pull({}); // Verify rich text content was written assertFileContainsText( @@ -145,7 +159,7 @@ describe("pull command - end-to-end tests", () => { path.join(outputDir, "components___base.json"), "component-1", "

Rich HTML content

" - ) + ); }); it("should use plain text when richText is disabled at output level", async () => { @@ -162,7 +176,7 @@ describe("pull command - end-to-end tests", () => { outputs: [{ format: "json", outDir: outputDir, richText: false }], }); - await pull(); + await pull({}); // Verify plain text content was written despite base config assertFileContainsText( @@ -189,7 +203,7 @@ describe("pull command - end-to-end tests", () => { outputs: [{ format: "json", outDir: outputDir, richText: "html" }], }); - await pull(); + await pull({}); // Verify rich text content was written assertFileContainsText( @@ -214,7 +228,7 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); // Verify correct API call with filtered params expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { @@ -238,7 +252,7 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); // Verify correct API call with filtered params expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { @@ -262,15 +276,14 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/components", { params: { - filter: - '{}', + filter: "{}", }, }); - }) + }); it("should filter components by folder at base level", async () => { fs.mkdirSync(outputDir, { recursive: true }); @@ -287,15 +300,14 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/components", { params: { - filter: - '{"folders":[{"id":"folder-1"}]}', + filter: '{"folders":[{"id":"folder-1"}]}', }, }); - }) + }); it("should filter components by folder and variants at base level", async () => { fs.mkdirSync(outputDir, { recursive: true }); @@ -313,7 +325,7 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/components", { params: { @@ -321,7 +333,7 @@ describe("pull command - end-to-end tests", () => { '{"folders":[{"id":"folder-1"}],"variants":[{"id":"variant-a"},{"id":"variant-b"}]}', }, }); - }) + }); it("should filter components by folder at output level", async () => { fs.mkdirSync(outputDir, { recursive: true }); @@ -335,21 +347,20 @@ describe("pull command - end-to-end tests", () => { format: "json", outDir: outputDir, components: { - folders: [{ id: "folder-3" }] - } + folders: [{ id: "folder-3" }], + }, }, ], }); - await pull(); + await pull({}); expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/components", { params: { - filter: - '{"folders":[{"id":"folder-3"}]}', + filter: '{"folders":[{"id":"folder-3"}]}', }, }); - }) + }); it("should filter components by folder and variants at output level", async () => { fs.mkdirSync(outputDir, { recursive: true }); @@ -363,14 +374,14 @@ describe("pull command - end-to-end tests", () => { format: "json", outDir: outputDir, components: { - folders: [{ id: "folder-3" }] + folders: [{ id: "folder-3" }], }, variants: [{ id: "variant-a" }, { id: "variant-b" }], }, ], }); - await pull(); + await pull({}); expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/components", { params: { @@ -378,7 +389,7 @@ describe("pull command - end-to-end tests", () => { '{"folders":[{"id":"folder-3"}],"variants":[{"id":"variant-a"},{"id":"variant-b"}]}', }, }); - }) + }); it("should filter projects at output level", async () => { fs.mkdirSync(outputDir, { recursive: true }); @@ -394,7 +405,7 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); // Verify correct API call with filtered params expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { @@ -419,7 +430,7 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); // Verify correct API call with filtered params expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { @@ -443,18 +454,18 @@ describe("pull command - end-to-end tests", () => { ], }); - await pull(); + await pull({}); // Verify correct API call with filtered params expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { params: { - filter: "{\"projects\":[]}", + filter: '{"projects":[]}', }, }); - expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/variables") + expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/variables"); // Components endpoint should not be called if not provided as source field - expect(mockHttpClient.get).toHaveBeenCalledTimes(2) + expect(mockHttpClient.get).toHaveBeenCalledTimes(2); }); }); @@ -536,7 +547,7 @@ describe("pull command - end-to-end tests", () => { variantId: null, folderId: "folder-2", }), - ] + ]; const componentsVariantA = [ createMockComponent({ @@ -549,7 +560,7 @@ describe("pull command - end-to-end tests", () => { variantId: "variant-a", folderId: "folder-1", }), - ] + ]; const componentsVariantB = [ createMockComponent({ @@ -562,14 +573,22 @@ describe("pull command - end-to-end tests", () => { variantId: "variant-b", folderId: "folder-1", }), - ] + ]; setupMocks({ - textItems: [...baseTextItems, ...variantATextItems, ...variantBTextItems], - components: [...componentsBase, ...componentsVariantA, ...componentsVariantB], + textItems: [ + ...baseTextItems, + ...variantATextItems, + ...variantBTextItems, + ], + components: [ + ...componentsBase, + ...componentsVariantA, + ...componentsVariantB, + ], }); - await pull(); + await pull({}); // Verify a file was created for each project and variant present in the (mocked) API response assertFilesCreated(outputDir, [ diff --git a/lib/src/commands/pull.ts b/lib/src/commands/pull.ts index 031a40f..56daea2 100644 --- a/lib/src/commands/pull.ts +++ b/lib/src/commands/pull.ts @@ -1,8 +1,9 @@ import appContext from "../utils/appContext"; import formatOutput from "../formatters"; +import { CommandMetaFlags } from "../http/types"; -export const pull = async () => { +export const pull = async (meta: CommandMetaFlags) => { for (const output of appContext.selectedProjectConfigOutputs) { - await formatOutput(output, appContext.projectConfig); + await formatOutput(output, appContext.projectConfig, meta); } }; diff --git a/lib/src/formatters/index.ts b/lib/src/formatters/index.ts index a2536fb..d02e8d8 100644 --- a/lib/src/formatters/index.ts +++ b/lib/src/formatters/index.ts @@ -1,14 +1,16 @@ +import { CommandMetaFlags } from "../http/types"; import { Output } from "../outputs"; import { ProjectConfigYAML } from "../services/projectConfig"; import JSONFormatter from "./json"; -export default function handleOutput( +export default function formatOutput( output: Output, - projectConfig: ProjectConfigYAML + projectConfig: ProjectConfigYAML, + meta: CommandMetaFlags ) { switch (output.format) { case "json": - return new JSONFormatter(output, projectConfig).format(); + return new JSONFormatter(output, projectConfig, meta).format(); default: throw new Error(`Unsupported output format: ${output}`); } diff --git a/lib/src/formatters/json.ts b/lib/src/formatters/json.ts index ec312bd..8c189f3 100644 --- a/lib/src/formatters/json.ts +++ b/lib/src/formatters/json.ts @@ -22,7 +22,7 @@ export default class JSONFormatter extends applyMixins( protected async fetchAPIData() { const textItems = await this.fetchTextItems(); const components = await this.fetchComponents(); - const variables = await fetchVariables(); + const variables = await this.fetchVariables(); const variablesById = variables.reduce((acc, variable) => { acc[variable.id] = variable; @@ -137,6 +137,7 @@ export default class JSONFormatter extends applyMixins( params.richText = this.output.richText; } + return params; } @@ -149,7 +150,7 @@ export default class JSONFormatter extends applyMixins( private async fetchTextItems() { if (!this.projectConfig.projects && !this.output.projects) return []; - return await fetchText(this.generateQueryParams("textItem")); + return await fetchText(this.generateQueryParams("textItem"), this.meta); } /** @@ -161,6 +162,10 @@ export default class JSONFormatter extends applyMixins( private async fetchComponents() { if (!this.projectConfig.components && !this.output.components) return []; - return await fetchComponents(this.generateQueryParams("component")); + return await fetchComponents(this.generateQueryParams("component"), this.meta); + } + + private async fetchVariables() { + return await fetchVariables(this.meta); } } diff --git a/lib/src/formatters/shared/base.ts b/lib/src/formatters/shared/base.ts index b3d4484..797ff0c 100644 --- a/lib/src/formatters/shared/base.ts +++ b/lib/src/formatters/shared/base.ts @@ -5,6 +5,7 @@ import { ProjectConfigYAML } from "../../services/projectConfig"; import OutputFile from "./fileTypes/OutputFile"; import appContext from "../../utils/appContext"; import JSONOutputFile from "./fileTypes/JSONOutputFile"; +import { CommandMetaFlags } from "../../http/types"; export default class BaseFormatter { protected output: Output; @@ -15,8 +16,13 @@ export default class BaseFormatter { JSONOutputFile<{ variantId: string }> >; protected variablesOutputFile: JSONOutputFile; + protected meta: CommandMetaFlags; - constructor(output: Output, projectConfig: ProjectConfigYAML) { + constructor( + output: Output, + projectConfig: ProjectConfigYAML, + meta: CommandMetaFlags + ) { this.output = output; this.projectConfig = projectConfig; this.outDir = output.outDir ?? appContext.outDir; @@ -25,6 +31,7 @@ export default class BaseFormatter { filename: "variables", path: this.outDir, }); + this.meta = meta; } protected async fetchAPIData(): Promise { diff --git a/lib/src/http/checkToken.ts b/lib/src/http/checkToken.ts index f3bf38f..cc01224 100644 --- a/lib/src/http/checkToken.ts +++ b/lib/src/http/checkToken.ts @@ -1,13 +1,11 @@ -import { defaultInterceptor } from "./client"; +import getHttpClient from "./client"; import logger from "../utils/logger"; -import axios, { AxiosError } from "axios"; +import { AxiosError } from "axios"; import appContext from "../utils/appContext"; export default async function checkToken(token: string) { try { - const httpClient = axios.create({}); - - httpClient.interceptors.request.use(defaultInterceptor(token)); + const httpClient = getHttpClient({ token }); const response = await httpClient.get("/token-check"); diff --git a/lib/src/http/client.test.ts b/lib/src/http/client.test.ts new file mode 100644 index 0000000..8a8df10 --- /dev/null +++ b/lib/src/http/client.test.ts @@ -0,0 +1,75 @@ +import { defaultInterceptor } from "./client"; +import appContext from "../utils/appContext"; +import { InternalAxiosRequestConfig } from "axios"; + +describe("defaultInterceptor", () => { + const HOST = "https://api.example.com"; + const CLIENT_ID = "test-client-id"; + const API_TOKEN = "test-token"; + const INTERCEPTOR_CONFIG = { headers: {} } as InternalAxiosRequestConfig; + + beforeEach(() => { + appContext.apiHost = HOST; + appContext.setClientId(CLIENT_ID); + appContext.setApiToken(API_TOKEN); + }); + + it("sets baseURL to appContext.apiHost", () => { + appContext.apiHost = HOST; + + const interceptor = defaultInterceptor(); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.baseURL).toBe(HOST); + }); + + it("sets x-ditto-client-id to appContext.clientId", () => { + appContext.setClientId(CLIENT_ID); + + const interceptor = defaultInterceptor(); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.headers["x-ditto-client-id"]).toBe(CLIENT_ID); + }); + + it("sets Authorization header to appContext.apiToken when no token is provided", () => { + const interceptor = defaultInterceptor(); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.headers.Authorization).toBe(API_TOKEN); + }); + + it("sets Authorization header to provided token", () => { + const CUSTOM_TOKEN = "custom-token"; + + const interceptor = defaultInterceptor({ token: CUSTOM_TOKEN }); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.headers.Authorization).toBe(CUSTOM_TOKEN); + }); + + it("sets x-ditto-app to github_action when githubActionRequest is true", () => { + const interceptor = defaultInterceptor({ + meta: { githubActionRequest: "true" }, + }); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.headers["x-ditto-app"]).toBe("github_action"); + }); + + it("sets x-ditto-app to cli when githubActionRequest is false", () => { + const interceptor = defaultInterceptor({ + meta: { githubActionRequest: "false" }, + }); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.headers["x-ditto-app"]).toBe("cli"); + }); + + it("sets x-ditto-app to cli when githubActionRequest is not present", () => { + const interceptor = defaultInterceptor({ meta: {} }); + const result = interceptor(INTERCEPTOR_CONFIG); + + expect(result.headers["x-ditto-app"]).toBe("cli"); + }); +}); diff --git a/lib/src/http/client.ts b/lib/src/http/client.ts index e3df3cf..ae149c8 100644 --- a/lib/src/http/client.ts +++ b/lib/src/http/client.ts @@ -1,18 +1,24 @@ import axios, { InternalAxiosRequestConfig } from "axios"; import appContext from "../utils/appContext"; +import { CommandMetaFlags } from "./types"; -export function defaultInterceptor(token?: string) { +type InterceptorParams = { token?: string; meta?: CommandMetaFlags }; + +export function defaultInterceptor({ token, meta }: InterceptorParams = {}) { return function (config: InternalAxiosRequestConfig) { config.baseURL = appContext.apiHost; config.headers["x-ditto-client-id"] = appContext.clientId; - config.headers["x-ditto-app"] = "cli"; + config.headers["x-ditto-app"] = + meta?.githubActionRequest === "true" ? "github_action" : "cli"; config.headers.Authorization = token || appContext.apiToken; return config; }; } -const httpClient = axios.create({}); - -httpClient.interceptors.request.use(defaultInterceptor()); +const getHttpClient = (params: InterceptorParams) => { + const httpClient = axios.create({}); + httpClient.interceptors.request.use(defaultInterceptor(params)); + return httpClient; +}; -export default httpClient; +export default getHttpClient; diff --git a/lib/src/http/components.test.ts b/lib/src/http/components.test.ts index 25229e3..acd3698 100644 --- a/lib/src/http/components.test.ts +++ b/lib/src/http/components.test.ts @@ -1,10 +1,16 @@ -import httpClient from "./client"; +import getHttpClient from "./client"; import fetchComponents from "./components"; jest.mock("./client"); describe("fetchComponents", () => { - const mockHttpClient = httpClient as jest.Mocked; + // Create a mock client with a mock 'get' method + const mockHttpClient = { + get: jest.fn(), + }; + + // Make getHttpClient return the mock client + (getHttpClient as jest.Mock).mockReturnValue(mockHttpClient); beforeEach(() => { jest.clearAllMocks(); @@ -30,10 +36,13 @@ describe("fetchComponents", () => { mockHttpClient.get.mockResolvedValue(mockResponse); - const result = await fetchComponents({ - filter: "", - richText: "html", - }); + const result = await fetchComponents( + { + filter: "", + richText: "html", + }, + {} + ); expect(result).toEqual([...mockResponse.data]); }); @@ -56,10 +65,13 @@ describe("fetchComponents", () => { mockHttpClient.get.mockResolvedValue(mockResponse); - const result = await fetchComponents({ - filter: "", - richText: "html", - }); + const result = await fetchComponents( + { + filter: "", + richText: "html", + }, + {} + ); expect(result).toEqual([...mockResponse.data]); }); diff --git a/lib/src/http/components.ts b/lib/src/http/components.ts index c603b7c..70c26cf 100644 --- a/lib/src/http/components.ts +++ b/lib/src/http/components.ts @@ -1,10 +1,20 @@ import { AxiosError } from "axios"; -import { ZComponentsResponse, PullQueryParams } from "./types"; -import httpClient from "./client"; +import { + ZComponentsResponse, + PullQueryParams, + CommandMetaFlags, +} from "./types"; +import getHttpClient from "./client"; -export default async function fetchComponents(params: PullQueryParams) { +export default async function fetchComponents( + params: PullQueryParams, + meta: CommandMetaFlags +) { try { - const response = await httpClient.get("/v2/components", { params }); + const httpClient = getHttpClient({ meta }); + const response = await httpClient.get("/v2/components", { + params, + }); return ZComponentsResponse.parse(response.data); } catch (e) { @@ -30,4 +40,4 @@ export default async function fetchComponents(params: PullQueryParams) { throw e; } -} \ No newline at end of file +} diff --git a/lib/src/http/textItems.test.ts b/lib/src/http/textItems.test.ts index 37f19da..d2a7e22 100644 --- a/lib/src/http/textItems.test.ts +++ b/lib/src/http/textItems.test.ts @@ -1,10 +1,16 @@ import fetchText from "./textItems"; -import httpClient from "./client"; +import getHttpClient from "./client"; jest.mock("./client"); describe("fetchText", () => { - const mockHttpClient = httpClient as jest.Mocked; + // Create a mock client with a mock 'get' method + const mockHttpClient = { + get: jest.fn(), + }; + + // Make getHttpClient return the mock client + (getHttpClient as jest.Mock).mockReturnValue(mockHttpClient); beforeEach(() => { jest.clearAllMocks(); @@ -30,10 +36,13 @@ describe("fetchText", () => { mockHttpClient.get.mockResolvedValue(mockResponse); - const result = await fetchText({ - filter: "", - richText: "html", - }); + const result = await fetchText( + { + filter: "", + richText: "html", + }, + {} + ); expect(result).toEqual([ { @@ -68,10 +77,13 @@ describe("fetchText", () => { mockHttpClient.get.mockResolvedValue(mockResponse); - const result = await fetchText({ - filter: "", - richText: "html", - }); + const result = await fetchText( + { + filter: "", + richText: "html", + }, + {} + ); expect(result).toEqual([ { @@ -88,4 +100,4 @@ describe("fetchText", () => { ]); }); }); -}); \ No newline at end of file +}); diff --git a/lib/src/http/textItems.ts b/lib/src/http/textItems.ts index d022922..57d4618 100644 --- a/lib/src/http/textItems.ts +++ b/lib/src/http/textItems.ts @@ -1,9 +1,14 @@ import httpClient from "./client"; import { AxiosError } from "axios"; -import { PullQueryParams, ZTextItemsResponse } from "./types"; +import { CommandMetaFlags, PullQueryParams, ZTextItemsResponse } from "./types"; +import getHttpClient from "./client"; -export default async function fetchText(params: PullQueryParams) { +export default async function fetchText( + params: PullQueryParams, + meta: CommandMetaFlags +) { try { + const httpClient = getHttpClient({ meta }); const response = await httpClient.get("/v2/textItems", { params }); return ZTextItemsResponse.parse(response.data); diff --git a/lib/src/http/types.ts b/lib/src/http/types.ts index 78886e1..bd11c07 100644 --- a/lib/src/http/types.ts +++ b/lib/src/http/types.ts @@ -23,11 +23,11 @@ const ZBaseTextEntity = z.object({ tags: z.array(z.string()), variableIds: z.array(z.string()), variantId: z.string().nullable(), -}) +}); const ZTextItem = ZBaseTextEntity.extend({ projectId: z.string(), -}) +}); export function isTextItem(item: TextItem | Component): item is TextItem { return "projectId" in item; @@ -45,7 +45,7 @@ export type TextItemsResponse = z.infer; const ZComponent = ZBaseTextEntity.extend({ folderId: z.string().nullable(), -}) +}); /** * Represents a single component, as returned from the /v2/components endpoint @@ -54,3 +54,12 @@ export type Component = z.infer; export const ZComponentsResponse = z.array(ZComponent); export type ComponentsResponse = z.infer; + +/** + * Contains metadata attached to CLI commands via -m or --meta flag + * Currently only used internally to identify requests from our GitHub Action + */ +export type CommandMetaFlags = { + githubActionRequest?: string; // Set to "true" if the request is from our GitHub Action + [key: string]: string | undefined; // Allow other arbitrary key-value pairs, but none of these values are used for anything at the moment +}; diff --git a/lib/src/http/variables.ts b/lib/src/http/variables.ts index da89480..ce9c2d4 100644 --- a/lib/src/http/variables.ts +++ b/lib/src/http/variables.ts @@ -1,6 +1,8 @@ import httpClient from "./client"; import { AxiosError } from "axios"; import { z } from "zod"; +import getHttpClient from "./client"; +import { CommandMetaFlags } from "./types"; const ZBaseVariable = z.object({ id: z.string(), @@ -65,8 +67,9 @@ const ZVariablesResponse = z.array(ZVariable); export type VariablesResponse = z.infer; -export default async function fetchVariables() { +export default async function fetchVariables(meta: CommandMetaFlags) { try { + const httpClient = getHttpClient({ meta }); const response = await httpClient.get("/v2/variables"); return ZVariablesResponse.parse(response.data); diff --git a/lib/src/index.ts b/lib/src/index.ts index 7fa5282..b197503 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -11,6 +11,7 @@ import { initProjectConfig } from "./services/projectConfig"; import appContext from "./utils/appContext"; import type commander from "commander"; import { ErrorType, isDittoError, isDittoErrorType } from "./utils/DittoError"; +import processCommandMetaFlag from "./utils/processCommandMetaFlag"; type Command = "pull"; @@ -63,6 +64,10 @@ const setupCommands = () => { const setupOptions = () => { program.option("--legacy", "Run in legacy mode"); + program.option( + "-m, --meta ", + "Include arbitrary data in requests to the Ditto API. Ex: -m githubActionRequest:true trigger:manual" + ); program.version(version, "-v, --version", "Output the current version"); }; @@ -77,10 +82,12 @@ const executeCommand = async ( await initProjectConfig(options); + const { meta } = program.opts(); + switch (commandName) { case "none": case "pull": { - return await pull(); + return await pull(processCommandMetaFlag(meta)); } default: { await quit(`Invalid command: ${commandName}. Exiting Ditto CLI...`); diff --git a/lib/src/utils/appContext.ts b/lib/src/utils/appContext.ts index 4093fef..05ee567 100644 --- a/lib/src/utils/appContext.ts +++ b/lib/src/utils/appContext.ts @@ -66,6 +66,10 @@ class AppContext { return this.#clientId; } + setClientId(value: string) { + this.#clientId = value; + } + setApiToken(value: string | undefined) { this.#apiToken = value; } diff --git a/lib/src/utils/processCommandMetaFlag.test.ts b/lib/src/utils/processCommandMetaFlag.test.ts new file mode 100644 index 0000000..776a7f5 --- /dev/null +++ b/lib/src/utils/processCommandMetaFlag.test.ts @@ -0,0 +1,29 @@ +import processCommandMetaFlag from "./processCommandMetaFlag"; + +describe("processCommandMetaFlag tests", () => { + it("It parses correctly", () => { + expect( + processCommandMetaFlag(["githubActionRequest:true", "trigger:manual"]) + ).toEqual({ + githubActionRequest: "true", + trigger: "manual", + }); + }); + + it("Successfully parses entries without : as key with undefined value", () => { + expect( + processCommandMetaFlag(["context:github-action", "trigger"]) + ).toEqual({ + context: "github-action", + trigger: undefined, + }); + }); + + it("Ignores entries with multiple : in them", () => { + expect( + processCommandMetaFlag(["context:github-action", "trigger:manual:ci"]) + ).toEqual({ + context: "github-action", + }); + }); +}); diff --git a/lib/src/utils/processCommandMetaFlag.ts b/lib/src/utils/processCommandMetaFlag.ts new file mode 100644 index 0000000..9abad35 --- /dev/null +++ b/lib/src/utils/processCommandMetaFlag.ts @@ -0,0 +1,31 @@ +import { CommandMetaFlags } from "../http/types"; + +/** + * Processes an array of strings in the format "key:value" and returns an object mapping keys to values. + * @param inputArr Array of strings in the format "key:value" + * @returns An object mapping keys to values + */ +const processCommandMetaFlag = ( + inputArr: string[] | null +): CommandMetaFlags => { + const res: CommandMetaFlags = {}; + + if (!Array.isArray(inputArr)) { + return res; + } + + inputArr.forEach((element) => { + const parts = element.split(":"); + // Skip entries with multiple ":" characters + // Entries with no ":" will result in key with undefined value, which is ok + if (parts.length > 2) { + return; + } + const [key, value] = parts; + res[key] = value; + }); + + return res; +}; + +export default processCommandMetaFlag; diff --git a/package.json b/package.json index f69cb84..6a210bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dittowords/cli", - "version": "5.0.0", + "version": "5.1.0", "description": "Command Line Interface for Ditto (dittowords.com).", "license": "MIT", "main": "bin/ditto.js",