Skip to content

Commit

Permalink
feat(cli): add new action compare (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
oljekechoro authored Dec 19, 2021
1 parent cf2e9ab commit 667d6b2
Show file tree
Hide file tree
Showing 12 changed files with 3,349 additions and 19 deletions.
101 changes: 95 additions & 6 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,43 @@ Output in `nexus-downloads/nexus-cli-downloads-meta-2021-03-17T06:48:21.347Z.jso
}
]
```
### Compare
Options for this action should be given via config
```shell script
nexus-cli --config compare-config.json
```
Utility will write list of missing packages and list of extra packages for primary registry in comparison with secondary one.

Output in `${cwd}/missing.json`
```json
[
{
"group": "null",
"name": "buildstamp",
"version": "1.0.2"
},
{
"group": "null",
"name": "buildstamp",
"version": "1.2.2"
},
{
"group": "null",
"name": "buildstamp",
"version": "1.3.0"
}
]
```
Output in `${cwd}/extra.json`
```json
[
{
"group": "null",
"name": "buildstamp",
"version": "1.0.1",
}
]
```
### With config file:
```shell script
nexus-cli --config some/path/config.json
Expand All @@ -51,21 +88,21 @@ nexus-cli --config some/path/config.json --data.repo npm --data.name bat --data.
#### Common
| Option | Description |
|---------------------------------------------|--------------------------------------------------|
| `auth.username`, `auth.password` | Nexus API credentials |
| `url` | Nexus API URL |
| `auth.username`, `auth.password` | Nexus API credentials, optional if action is 'compare' |
| `url` | Nexus API URL, optional if action is 'compare' |
| `batch.rateLimit` | Components API `deleteComponent` method multiple call limit. If exists, limitation will be applied. See more at [push-it-to-the-limit](https://github.com/antongolub/push-it-to-the-limit). |
| `config` | path to config file
| `action` | one of `delete`, `download` |
| `data.repo` | name of package repository |
| `data.name` | package name |
| `data.group` | package group. To get packages outside of any group (scope) pass `null` |
| `data.range` | package versions range to be deleted |
By default `batch.rateLimit` is 3 requests per 1000 ms
### Delete

| Option | Description |
|---------------------------------------------|--------------------------------------------------|
| `data.no-prompt` | disable destructive action confirmation (delete) |
| `data.repo` | name of package repository |
| `data.name` | package name |
| `data.group` | package group. To get packages outside of any group (scope) pass `null` |
| `data.range` | package versions range to be deleted |
### Download

| Option | Description |
Expand All @@ -74,6 +111,20 @@ By default `batch.rateLimit` is 3 requests per 1000 ms
| `data.npmBatch.access` | make meta output as [@qiwi/npm-batch-cli](https://github.com/qiwi/npm-batch-action/tree/master/packages/cli) config with blank values & given access, one of `public`, `restricted` |
| `data.sortField` | one of `version`, `name`, `group`, `repository` |
| `data.sortDirection` | one of `asc`, `desc` |
| `data.repo` | name of package repository |
| `data.name` | package name |
| `data.group` | package group. To get packages outside of any group (scope) pass `null` |

### Compare

| Option | Description |
|---------------------------------------------|--------------------------------------------------|
| `data.packages[].name` | name of package to compare |
| `data.packages[].group` | group of package to compare. To get packages outside of any group (scope) pass `null` |
| `data.primaryRegistry.url` | url of primary repository to compare |
| `data.primaryRegistry.auth.username`, `data.primaryRegistry.auth.password` | credentials of primary repository |
| `data.secondaryRegistry.url` | url of secondary repository to compare |
| `data.secondaryRegistry.auth.username`, `data.secondaryRegistry.auth.password` | credentials of secondary repository |

All options except `--no-prompt` must be set through the CLI flags or `--config` JSON data.
Options from config file can be overridden.
Expand Down Expand Up @@ -117,3 +168,41 @@ If you want to use `--no-prompt` option in a config file, add it as `"prompt": f
}
}
```
#### Compare
```json
{
"action": "compare",
"data": {
"cwd": "temp-compare",
"primaryRegistry": {
"url": "http://foo.qiwi.com:8081/repository/npm-foo",
"auth": {
"username": "username",
"password": "password"
}
},
"secondaryRegistry": {
"url": "https://bar.qiwi.com/repository/npm-foo",
"auth": {
"username": "username",
"password": "password"
}
},
"packages": [
{
"name": "baz",
"group": "qiwi-bar"
},
{
"name": "foo-bar",
"group": "qiwi"
},
{
"name": "common",
"group": "null"
}
]
}
}

```
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@qiwi/nexus-client": "1.1.5",
"@qiwi/nexus-helper": "2.2.3",
"@qiwi/nexus-utils": "1.0.3",
"@qiwi/npm-batch-client": "^2.1.0",
"@types/semver": "^7.3.9",
"axios": "^0.24.0",
"blork": "^9.3.0",
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/main/ts/executor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { performCompare } from './executors/compare'
import { performDelete } from './executors/delete'
import { performDownload } from './executors/download'
import { IBaseConfig } from './interfaces'
Expand All @@ -14,8 +15,11 @@ export const runExecutor = (
case 'delete': {
return performDelete(config.data, helper)
}
case 'download':{
case 'download': {
return performDownload(config.data, helper)
}
case 'compare': {
return performCompare(config.data)
}
}
}
64 changes: 64 additions & 0 deletions packages/cli/src/main/ts/executors/compare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { NpmRegClientWrapper } from '@qiwi/npm-batch-client'
import { join } from 'path'

import { TCompareConfigData, TCompareRegistryOpts, TDownloadListItem } from '../interfaces'
import { writeJson } from '../utils'

const clientFactory = (opts: TCompareRegistryOpts) => new NpmRegClientWrapper(
opts.url,
{
...opts.auth,
email: '',
}
)

export const performCompare = async (config: TCompareConfigData): Promise<void> => {
const {
secondaryRegistry,
primaryRegistry,
packages,
cwd,
} = config

const primaryRegClient = clientFactory(primaryRegistry)
const secondaryRegClient = clientFactory(secondaryRegistry)

const downloadListMissing: TDownloadListItem[] = []
const downloadListExtra: TDownloadListItem[] = []

for (const item of packages) {
const name = item.group && item.group !== 'null' ? `@${item.group}/${item.name}` : item.name
try {
const [
primaryPackument,
secondaryPackument,
] = await Promise.all([
primaryRegClient.getPackument(name),
secondaryRegClient.getPackument(name)
])

const primaryVersions = Object.keys(primaryPackument.versions)
const secondaryVersions = Object.keys(secondaryPackument.versions)

secondaryVersions
.filter(item => !primaryVersions.includes(item))
.forEach(version => downloadListMissing.push({ version, name: item.name, group: item.group }))

primaryVersions
.filter(item => !secondaryVersions.includes(item))
.forEach(version => downloadListExtra.push({ version, name: item.name, group: item.group }))
} catch (e) {
console.error(`Could not get data packuments for ${name}:`, e)
}
}

const missingResultsPath = join(cwd, 'missing.json')
const extraResultsPath = join(cwd, 'extra.json')

writeJson(downloadListMissing, missingResultsPath)
writeJson(downloadListExtra, extraResultsPath)

console.log(`Done.`)
console.log(`${downloadListMissing.length} packages are missing, ${downloadListExtra.length} packages are extra in primary registry`)
console.log(`Metadata is written to ${missingResultsPath} and ${extraResultsPath}`)
}
15 changes: 14 additions & 1 deletion packages/cli/src/main/ts/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface IPackageOpts {
repo: string
}

export type TAction = 'delete' | 'download'
export type TAction = 'delete' | 'download' | 'compare'

export interface IBaseConfig<TA = TAction, T = any> {
url: string
Expand All @@ -28,6 +28,19 @@ export type TDeleteConfigData = IPackageOpts & { prompt?: boolean }

export type TDeleteConfig = IBaseConfig<'delete', TDeleteConfigData>

export type TCompareRegistryOpts = Pick<IBaseConfig, 'url' | 'auth'>

export type TCompareConfigData = {
primaryRegistry: TCompareRegistryOpts
secondaryRegistry: TCompareRegistryOpts
packages: Array<Pick<IPackageOpts, 'name' | 'group'>>
cwd: string
}

export type TCompareConfig = IBaseConfig<'compare', TCompareConfigData>

export type TDownloadListItem = Pick<TGetPackageAssetsOpts, 'group' | 'name' | 'version'>

export type TPackageAccess = 'public' | 'restricted'

export type TNpmBatchOpts = {
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/src/main/ts/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IBaseConfig, TDeleteConfig, TDownloadConfig, TDownloadConfigStrict } from '../interfaces'
import { IBaseConfig, TCompareConfig, TDeleteConfig, TDownloadConfig, TDownloadConfigStrict } from '../interfaces'
import { DeepPartial, readFileToString } from './misc'
import { validateConfig, validateDeleteConfig, validateDownloadConfig } from './validators'
import { validateCompareConfig, validateConfig, validateDeleteConfig, validateDownloadConfig } from './validators'

export const defaultLimit = {
period: 1000,
Expand Down Expand Up @@ -51,7 +51,7 @@ export const resolveDeleteConfig = (config: TDeleteConfig): TDeleteConfig => {
}
}

export const getConfig = (opts: IBaseConfig, configPath?: string): TDownloadConfigStrict | TDeleteConfig => {
export const getConfig = (opts: IBaseConfig, configPath?: string): TDownloadConfigStrict | TDeleteConfig | TCompareConfig => {
const config = validateConfig(
configPath
? resolveConfig(JSON.parse(readFileToString(configPath)), opts as any) as any
Expand All @@ -66,5 +66,9 @@ export const getConfig = (opts: IBaseConfig, configPath?: string): TDownloadConf
return resolveDownloadConfig(validateDownloadConfig(config))
}

if (config.action === 'compare') {
return validateCompareConfig(config)
}

throw new Error('Unsupported action in config')
}
13 changes: 11 additions & 2 deletions packages/cli/src/main/ts/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentsApi, SearchApi } from '@qiwi/nexus-client'
import { INexusHelper, NexusComponentsHelper } from '@qiwi/nexus-helper'
import { readFileSync, writeFileSync } from 'fs'
import { mkdirSync,readFileSync, writeFileSync } from 'fs'
import { sep } from 'path'
import { createInterface } from 'readline'

import { IBaseConfig } from '../interfaces'
Expand All @@ -18,8 +19,16 @@ export const question = (message: string): Promise<string> => {

export const readFileToString = (path: string): string => readFileSync(path).toString()

export const writeJson = (obj: Record<string, any>, path: string): void =>
export const writeJson = (obj: Record<string, any>, path: string): void => {
const dirPath = path
.split(sep)
.slice(0, -1)
.join(sep)

mkdirSync(dirPath, { recursive: true})

writeFileSync(path, JSON.stringify(obj, null, '\t')) // eslint-disable-line unicorn/no-null
}

export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
Expand Down
30 changes: 25 additions & 5 deletions packages/cli/src/main/ts/utils/validators.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { check } from 'blork'

import { IBaseConfig, TDeleteConfig, TDownloadConfig } from '../interfaces'
import { IBaseConfig, TCompareConfig, TDeleteConfig, TDownloadConfig } from '../interfaces'
import { DeepPartial } from './misc'

export const validateConfig = (config: DeepPartial<IBaseConfig>): IBaseConfig => {
const { url, auth, action, batch } = config
check(action, 'config.action: "download" | "delete" | "compare"')

check(url, 'config.url: string')
check(auth, 'config.auth: { "username": str, "password": str }')
check(action, 'config.action: "download" | "delete"')
check(batch, 'config.batch: { "skipErrors": bool? }')
if (action !== 'compare') {
check(url, 'config.url: string')
check(auth, 'config.auth: { "username": str, "password": str }')
check(batch, 'config.batch: { "skipErrors": bool? }')
}

return config as IBaseConfig
}
Expand Down Expand Up @@ -40,3 +42,21 @@ export const validateDownloadConfig = (config: IBaseConfig): TDownloadConfig =>

return config as TDownloadConfig
}

export const validateCompareConfig = (config: IBaseConfig): TCompareConfig => {
check(config.data.cwd, 'config.data.cwd: str')

check(config.data.packages, 'config.data.packages: arr+')
config.data.packages.forEach((item: any, i: number) =>
check(item,`config.data.packages[${i}]: { "name": str, "group": str | "null" | undefined }`)
)
check(config.data.primaryRegistry.repo, 'config.data.primaryRegistry.repo: str')
check(config.data.primaryRegistry.url, 'config.data.primaryRegistry.url: string')
check(config.data.primaryRegistry.auth, 'config.data.primaryRegistry.auth: { "username": str, "password": str }')

check(config.data.secondaryRegistry.repo, 'config.data.primaryRegistry.repo: str')
check(config.data.secondaryRegistry.url, 'config.data.primaryRegistry.url: string')
check(config.data.secondaryRegistry.auth, 'config.data.primaryRegistry.auth: { "username": str, "password": str }')

return config as TCompareConfig
}
Loading

0 comments on commit 667d6b2

Please sign in to comment.