Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: re-structured language command with sub commands #148

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"request": "launch",
"name": "Debug Pull languages",
"program": "${workspaceFolder}/dist/index.mjs",
"args": ["pull-languages", "--space", "2950182323", "--path", ".storyblok"],
"args": ["languages", "pull", "--space", "2950182323", "--path", ".storyblok"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { vol } from 'memfs'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { pullLanguages, saveLanguagesToFile } from './actions'
import { fetchLanguages, saveLanguagesToFile } from './actions'

const handlers = [
http.get('https://api.storyblok.com/v1/spaces/12345', async ({ request }) => {
Expand Down Expand Up @@ -44,7 +44,7 @@ describe('pull languages actions', () => {
vol.reset()
})

describe('pullLanguages', () => {
describe('fetchLanguages', () => {
it('should pull languages successfully with a valid token', async () => {
const mockResponse = {
default_lang_name: 'en',
Expand All @@ -59,12 +59,12 @@ describe('pull languages actions', () => {
},
],
}
const result = await pullLanguages('12345', 'valid-token', 'eu')
const result = await fetchLanguages('12345', 'valid-token', 'eu')
expect(result).toEqual(mockResponse)
})
})
it('should throw an masked error for invalid token', async () => {
await expect(pullLanguages('12345', 'invalid-token', 'eu')).rejects.toThrow(
await expect(fetchLanguages('12345', 'invalid-token', 'eu')).rejects.toThrow(
new Error(`The user is not authorized to access the API`),
)
})
Expand All @@ -90,7 +90,7 @@ describe('pull languages actions', () => {
verbose: false,
space: '12345',
})
const content = vol.readFileSync('/temp/languages.12345.json', 'utf8')
const content = vol.readFileSync('/temp/languages.json', 'utf8')
expect(content).toBe(JSON.stringify(mockResponse, null, 2))
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { RegionCode } from '../../constants'
import type { SpaceInternationalization } from '../../types'
import { getStoryblokUrl } from '../../utils/api-routes'

export const pullLanguages = async (space: string, token: string, region: RegionCode): Promise<SpaceInternationalization | undefined> => {
export const fetchLanguages = async (space: string, token: string, region: RegionCode): Promise<SpaceInternationalization | undefined> => {
try {
const url = getStoryblokUrl(region)
const response = await customFetch<{
Expand All @@ -31,10 +31,10 @@ export const pullLanguages = async (space: string, token: string, region: Region

export const saveLanguagesToFile = async (space: string, internationalizationOptions: SpaceInternationalization, options: PullLanguagesOptions) => {
try {
const { filename = 'languages', suffix = space, path } = options
const { filename = 'languages', suffix, path } = options
const data = JSON.stringify(internationalizationOptions, null, 2)
const name = `${filename}.${suffix}.json`
const resolvedPath = resolvePath(path, 'languages')
const name = suffix ? `${filename}.${suffix}.json` : `${filename}.json`
const resolvedPath = resolvePath(path, `languages/${space}/`)
const filePath = join(resolvedPath, name)

await saveToFile(filePath, data)
Expand Down
234 changes: 234 additions & 0 deletions src/commands/languages/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import chalk from 'chalk'
import { languagesCommand } from '.'
import { session } from '../../session'
import { CommandError, konsola } from '../../utils'
import { fetchLanguages, saveLanguagesToFile } from './actions'
import { colorPalette } from '../../constants'

vi.mock('./actions', () => ({
fetchLanguages: vi.fn(),
saveLanguagesToFile: vi.fn(),
}))

vi.mock('../../creds', () => ({
getCredentials: vi.fn(),
addCredentials: vi.fn(),
removeCredentials: vi.fn(),
removeAllCredentials: vi.fn(),
}))

// Mocking the session module
vi.mock('../../session', () => {
let _cache: Record<string, any> | null = null
const session = () => {
if (!_cache) {
_cache = {
state: {
isLoggedIn: false,
},
updateSession: vi.fn(),
persistCredentials: vi.fn(),
initializeSession: vi.fn(),
}
}
return _cache
}

return {
session,
}
})

vi.mock('../../utils', async () => {
const actualUtils = await vi.importActual('../../utils')
return {
...actualUtils,
konsola: {
ok: vi.fn(),
title: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
handleError: (error: Error, header = false) => {
konsola.error(error, header)
// Optionally, prevent process.exit during tests
},
}
})

describe('languagesCommand', () => {
describe('pull', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.clearAllMocks()
// Reset the option values
languagesCommand._optionValues = {}
for (const command of languagesCommand.commands) {
command._optionValues = {}
}
})
describe('default mode', () => {
it('should prompt the user if operation was sucessfull', async () => {
const mockResponse = {
default_lang_name: 'en',
languages: [
{
code: 'ca',
name: 'Catalan',
},
{
code: 'fr',
name: 'French',
},
],
}
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

vi.mocked(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
path: undefined,
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/12345/languages.json`)}`)
})

it('should throw an error if the user is not logged in', async () => {
session().state = {
isLoggedIn: false,
}
const mockError = new CommandError(`You are currently not logged in. Please login first to get your user info.`)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345'])
expect(konsola.error).toHaveBeenCalledWith(mockError, false)
})

it('should throw an error if the space is not provided', async () => {
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

const mockError = new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`)
await languagesCommand.parseAsync(['node', 'test', 'pull'])
expect(konsola.error).toHaveBeenCalledWith(mockError, false)
})

it('should prompt a warning the user if no languages are found', async () => {
const mockResponse = {
default_lang_name: 'en',
languages: [],
}
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

vi.mocked(fetchLanguages).mockResolvedValue(mockResponse)

await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '24568'])
expect(konsola.warn).toHaveBeenCalledWith(`No languages found in the space 24568`)
})
})

describe('--path option', () => {
it('should save the file at the provided path', async () => {
const mockResponse = {
default_lang_name: 'en',
languages: [
{
code: 'ca',
name: 'Catalan',
},
{
code: 'fr',
name: 'French',
},
],
}
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

vi.mocked(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--path', '/tmp'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
path: '/tmp',
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`/tmp/languages.json`)}`)
})
})

describe('--filename option', () => {
it('should save the file with the provided filename', async () => {
const mockResponse = {
default_lang_name: 'en',
languages: [
{
code: 'ca',
name: 'Catalan',
},
{
code: 'fr',
name: 'French',
},
],
}
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

vi.mocked(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--filename', 'custom-languages'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
filename: 'custom-languages',
path: undefined,
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/12345/custom-languages.json`)}`)
})
})

describe('--suffix option', () => {
it('should save the file with the provided suffix', async () => {
const mockResponse = {
default_lang_name: 'en',
languages: [
{
code: 'ca',
name: 'Catalan',
},
{
code: 'fr',
name: 'French',
},
],
}
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

vi.mocked(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--suffix', 'custom-suffix'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
suffix: 'custom-suffix',
path: undefined,
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/12345/languages.custom-suffix.json`)}`)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ import { colorPalette, commands } from '../../constants'
import { CommandError, handleError, konsola } from '../../utils'
import { getProgram } from '../../program'
import { session } from '../../session'
import { pullLanguages, saveLanguagesToFile } from './actions'
import { fetchLanguages, saveLanguagesToFile } from './actions'
import chalk from 'chalk'
import type { PullLanguagesOptions } from './constants'

const program = getProgram() // Get the shared singleton instance

export const pullLanguagesCommand = program
.command('pull-languages')
.description(`Download your space's languages schema as json`)
export const languagesCommand = program
.command('languages')
edodusi marked this conversation as resolved.
Show resolved Hide resolved
.description(`Manage your space's languages`)
.option('-s, --space <space>', 'space ID')
.option('-p, --path <path>', 'path to save the file. Default is .storyblok/languages')

languagesCommand
.command('pull')
.description(`Download your space's languages schema as json`)
.option('-f, --filename <filename>', 'filename to save the file as <filename>.<suffix>.json')
.option('--su, --suffix <suffix>', 'suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.')
.action(async (options: PullLanguagesOptions) => {
konsola.title(` ${commands.PULL_LANGUAGES} `, colorPalette.PULL_LANGUAGES, 'Pulling languages...')
konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES, 'Pulling languages...')
// Global options
const verbose = program.opts().verbose
// Command options
const { space, path, filename = 'languages', suffix = options.space } = options
const { space, path } = languagesCommand.opts()
const { filename = 'languages', suffix = options.space } = options

const { state, initializeSession } = session()
await initializeSession()
Expand All @@ -35,14 +40,19 @@ export const pullLanguagesCommand = program
}

try {
const internationalization = await pullLanguages(space, state.password, state.region)
const internationalization = await fetchLanguages(space, state.password, state.region)

if (!internationalization || internationalization.languages?.length === 0) {
konsola.warn(`No languages found in the space ${space}`)
return
}
await saveLanguagesToFile(space, internationalization, options)
konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/${filename}.${suffix}.json` : `.storyblok/languages/${filename}.${suffix}.json`)}`)
await saveLanguagesToFile(space, internationalization, {
...options,
path,
})
const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json`
const filePath = path ? `${path}/${fileName}` : `.storyblok/languages/${space}/${fileName}`
konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(filePath)}`)
}
catch (error) {
handleError(error as Error, verbose)
Expand Down
Loading
Loading