Skip to content

Commit

Permalink
feat: add list command
Browse files Browse the repository at this point in the history
  • Loading branch information
pviti committed Jun 28, 2024
1 parent 17f75df commit 37463ba
Show file tree
Hide file tree
Showing 8 changed files with 707 additions and 524 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": [
"@commercelayer/eslint-config-ts"
],
"ignorePatterns": ["test/**/*.test.ts", "bin/**"],
"ignorePatterns": ["test/**/*.ts", "bin/**"],
"rules": {
"prettier/prettier": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# commercelayer-cli-plugin-links
# commercelayer-cli-plugin-links
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"build": "rm -rf lib && tsc -b",
"prepack": "pnpm build && oclif manifest && pnpm readme",
"test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
"test-local": "tsx test/spot.ts",
"readme": "cl-cli-dev readme --plugin --bin=commercelayer && git add README.md",
"lint": "eslint src --ext .ts --config .eslintrc",
"lint:fix": "eslint src --fix"
Expand All @@ -62,10 +63,10 @@
"@types/chai": "^4.3.16",
"@types/lodash.isempty": "^4.4.9",
"@types/mocha": "^10.0.7",
"@types/node": "^20.14.8",
"@types/node": "^20.14.9",
"chai": "^4.4.1",
"eslint": "^8.57.0",
"mocha": "^10.5.1",
"mocha": "^10.5.2",
"nyc": "^15.1.0",
"oclif": "^4.13.10",
"semantic-release": "^24.0.0",
Expand Down
945 changes: 472 additions & 473 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

35 changes: 18 additions & 17 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import commercelayer, { type CommerceLayerClient, CommerceLayerStatic, type Link
import { Command, Args, Flags, ux as cliux } from '@oclif/core'
import { clColor, clConfig, clOutput, clText, clToken, clUpdate, clUtil } from '@commercelayer/cli-core'
import type { CommandError } from '@oclif/core/lib/interfaces'
import { DOC_DATE_TIME_STRING_FORMAT } from './util'
import { DOC_DATE_TIME_STRING_FORMAT, fillUTCDate } from './util'



Expand Down Expand Up @@ -100,6 +100,21 @@ export abstract class BaseCommand extends Command {

}


protected checkDateValue(value: string): string {
try {
const parsed = Date.parse(value)
if (Number.isNaN(parsed)) throw new Error('Invalid date', { cause: 'PARSE' })
const isoDate = new Date(fillUTCDate(value))
return isoDate.toISOString()
} catch (err: any) {
const msg = (err.cause === 'PARSE') ? err.message : 'Error parsing date'
this.error(`${msg}: ${clColor.msg.error(value)}`, {
suggestions: [`Dates must be in standard ISO format (${DOC_DATE_TIME_STRING_FORMAT})`]
})
}
}

}


Expand Down Expand Up @@ -129,7 +144,8 @@ export abstract class BaseEditCommand extends BaseCommand {
char: 'S',
description: 'the scope of the link',
required: false,
multiple: true
multiple: true,
multipleNonGreedy: true
}),
name: Flags.string({
char: 'n',
Expand Down Expand Up @@ -218,21 +234,6 @@ export abstract class BaseEditCommand extends BaseCommand {
}


protected checkDateValue(value: string): string {
try {
const parsed = Date.parse(value)
if (Number.isNaN(parsed)) throw new Error('Invalid date', { cause: 'PARSE' })
const isoDate = new Date(value)
return isoDate.toISOString()
} catch (err: any) {
const msg = (err.cause === 'PARSE') ? err.message : 'Error parsing date'
this.error(`${msg}: ${clColor.msg.error(value)}`, {
suggestions: [`Dates must be in standard ISO format (${DOC_DATE_TIME_STRING_FORMAT})`]
})
}
}


protected checkScope(scopeFlags?: string[]): string {

const scope: string[] = []
Expand Down
201 changes: 173 additions & 28 deletions src/commands/links/list.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { BaseCommand, Flags, cliux } from '../../base'
import Table, { type HorizontalAlignment } from 'cli-table3'
import type { Link, QueryPageSize, QueryParamsList } from '@commercelayer/sdk'
import { clApi, clColor, clConfig, clOutput, clUtil } from '@commercelayer/cli-core'
import { linkStatus } from '../../util'
import type { Link, QueryArraySortable, QueryPageSize, QueryParamsList, QueryRecordSortable } from '@commercelayer/sdk'
import { type KeyValSort, clApi, clColor, clConfig, clOutput, clUtil } from '@commercelayer/cli-core'
import { fillUTCDate, linkStatus } from '../../util'


const MAX_LINKS = 1000

type ComparisonOperator = 'eq' | 'gt' | 'lt' | 'lteq' | 'gteq'
type ComparisonFilter = { op: ComparisonOperator, value: string }

const SORTABLE_FIELDS = ['name', 'starts_at', 'expires_at', 'created_at', 'updated_at', 'disabled_at']


export default class LinksList extends BaseCommand {

static description = 'list all the created links'
Expand All @@ -17,28 +23,53 @@ export default class LinksList extends BaseCommand {
'$ cl links --status=pending',
]

static help: 'help for command links:list'

static flags = {
all: Flags.boolean({
char: 'A',
description: `show all links instead of first ${clConfig.api.page_max_size} only`,
exclusive: ['limit']
}),
type: Flags.string({
char: 't',
description: 'the type of the item',
options: clConfig.links.linkable_resources as string[],
multiple: false
}),
status: Flags.string({
char: 's',
description: 'the link status',
options: ['active', 'disabled', 'pending', 'expired'],
multiple: false
}),
limit: Flags.integer({
char: 'l',
description: 'limit number of links in output',
exclusive: ['all']
}),
name: Flags.string({
char: 'n',
description: 'the name of the link'
}),
scope: Flags.string({
char: 'S',
description: 'the scope of the link',
required: false,
multiple: false
}),
starts: Flags.string({
char: 's',
summary: 'the link\'s start date and time',
description: 'Look at the description of flag \'expires\' for details',
required: false,
multiple: true
}),
expires: Flags.string({
char: 'e',
summary: 'the link\'s expiration date and time',
description: `Use the standard ISO format with operators [gt, gteq, eq, lt, lteq].
A maximum of 2 parameters can be used for date filters.
If the operator is omitted the default operator 'eq' will be used.\n
If only one parameter is defined without an operator, it is interpreted as a range of values
Examples:
-s 2024 will be translated into -s gteq=2024-01-01T00:00:00Z lt=2025-01-01T00:00:00Z
-s 2024-04-10 will be translated into -s gteq=2024-04-10T00:00:00Z lt=2024-04-11T00:00:00Z
-s 2024-04-10T13:15:00 will be translated into -s gteq 2024-04-10T13:15:00Z lt=2024-04-10T13:16:00Z`,
required: false,
multiple: true
}),
sort: Flags.string({
description: 'a comma separated list of fields to sort by',
multiple: true
})
}

Expand All @@ -49,6 +80,14 @@ export default class LinksList extends BaseCommand {

if (flags.limit && (flags.limit < 1)) this.error(clColor.italic('Limit') + ' must be a positive integer')

const startsFilter: ComparisonFilter[] = []
const expiresFilter: ComparisonFilter[] = []
if (flags.starts) startsFilter.push(...this.comparisonParam(flags.starts, 'starts'))
if (flags.expires) expiresFilter.push(...this.comparisonParam(flags.expires, 'expires'))

const sortBy = this.sortFlag(flags.sort)
const sort: QueryArraySortable<Link> | QueryRecordSortable<Link> = (sortBy && (Object.keys(sortBy).length > 0)) ? sortBy : ['-expires_at', '-starts_at']

this.commercelayerInit(flags)


Expand All @@ -70,17 +109,27 @@ export default class LinksList extends BaseCommand {
const params: QueryParamsList<Link> = {
pageSize,
pageNumber: ++currentPage,
sort: ['-created_at'],
sort,
filters: {},
include: ['item'],
fields: { orders: ['id'], sku_lists: ['id'], bundles: ['id'], skus: ['id'] }
fields: {
orders: ['id', 'number'],
sku_lists: ['id', 'name'],
bundles: ['id', 'name', 'code'],
skus: ['id', 'name', 'code']
},
}

if (params?.filters) {
if (flags.type) params.filters.resource_type_eq = flags.type
if (flags.status) params.filters.status_eq = flags.status
// if (flags.type) params.filters.item_type_in = flags.type.join(',')
// if (flags.status) params.filters.status_in = flags.status.join(',')
if (flags.name) params.filters.name_cont = flags.name
if (flags.scope) params.filters.scope_cont = flags.scope
if (startsFilter?.length > 0) for (const f of startsFilter) params.filters[`starts_at_${f.op}`] = f.value
if (expiresFilter?.length > 0) for (const f of expiresFilter) params.filters[`expires_at_${f.op}`] = f.value
}

console.log(params)

const links = await this.cl.links.list(params)

Expand All @@ -104,23 +153,23 @@ export default class LinksList extends BaseCommand {
if (tableData?.length) {

const table = new Table({
head: ['ID', 'Item type', 'Item ID', 'Status', 'Created at', 'Disabled at'],
head: ['ID', 'Name', 'Item type', /* 'Item ID', */'Status', 'Starts at', 'Expires at'],
// colWidths: [100, 200],
style: {
head: ['brightYellow'],
compact: false,
},
compact: false
}
})

// let index = 0

table.push(...tableData.map(i => [
// { content: ++index, hAlign: 'right' as HorizontalAlignment },
clColor.blueBright(i.id || ''),
i.item?.type || '',
i.item?.id || '',
i.name,
i.item?.type,
// i.item?.id || '',
{ content: linkStatus(i.status), hAlign: 'center' as HorizontalAlignment },
clOutput.localeDate(i.created_at || ''),
clOutput.localeDate(i.disabled_at || '')
clOutput.localeDate(i.starts_at || ''),
clOutput.localeDate(i.expires_at || '')
]))

this.log(table.toString())
Expand All @@ -140,6 +189,67 @@ export default class LinksList extends BaseCommand {
}


private comparisonParam(params: string[], name?: string): ComparisonFilter[] {

const filter: ComparisonFilter[] = []

if (params.length === 0) return filter
if ((params.length === 2) || ((params.length === 1) && (params[0].includes('=')))) {
for (const p of params) {
const opval = p.split('=')
const op: string = (opval.length > 1) ? opval[0] : 'eq'
const val: string = (opval.length > 1) ? this.checkDateValue(opval[1]) : this.checkDateValue(opval[0])
if (!['eq', 'lt', 'gt', 'lteq', 'gteq'].includes(op)) this.error(`${name ? `Flag ${name}: ` : ''}Invalid filter [${p}]`)
filter.push({ op: op as ComparisonOperator, value: val })
}
return filter
}
if (params.length > 2) this.error(`${name ? `Flag ${name}: ` : ''}Date filters cannot have more than 2 params [${params.join(', ')}]`)


// Only one filter param
const param = params[0]

const gteq = this.checkDateValue(param) // check iso format
let lt

const dateTime = param.replace('Z', '').split('T')
if (dateTime.length === 2) { // date and time
const time = dateTime[1]
const hhmmss = (time.includes('.') ? time.substring(0, time.indexOf('.')) : time).split(':')
const nextTime = new Date(fillUTCDate(param))
switch (hhmmss.length) {
// hour, minute and second
case 3: { nextTime.setUTCSeconds(nextTime.getUTCSeconds() + 1); break }
// hour and minute
case 2: { nextTime.setUTCMinutes(nextTime.getUTCMinutes() + 1); break }
// only hour
case 1: { nextTime.setUTCHours(nextTime.getUTCHours() + 1); break }
}
lt = nextTime.toISOString()
} else { // only date
const date = dateTime[0]
const yyyymmdd = date.split('-')
const nextDate = new Date(fillUTCDate(date))
switch (yyyymmdd.length) {
// year, month and day
case 3: { nextDate.setUTCDate(nextDate.getUTCDate() + 1); break }
// year and month
case 2: { nextDate.setUTCMonth(nextDate.getUTCMonth() + 1); break }
// only year
case 1: { nextDate.setUTCFullYear(nextDate.getUTCFullYear() + 1); break }
}
lt = nextDate.toISOString()
}

filter.push({ op: 'gteq', value: gteq })
if (lt) filter.push({ op: 'lt', value: lt })

return filter

}


private footerMessage(flags: any, itemCount: number, totalItems: number): void {

this.log()
Expand Down Expand Up @@ -176,4 +286,39 @@ export default class LinksList extends BaseCommand {

}


private sortFlag(flag: string[] | undefined): KeyValSort {

const sort: KeyValSort = {}

if (flag && (flag.length > 0)) {

flag.forEach(f => {

const ot = f.split(',')
if (ot.length > 2) this.error('Can be defined only one field for each sort flag',
{ suggestions: [`Split the value ${clColor.style.attribute(f)} into two or more sort flags`] }
)

const of = ot[0]
if (!SORTABLE_FIELDS.includes(of)) this.error(`Invalid sort field: ${clColor.msg.error(of)}`,
{ suggestions: [`Sort field must be one of [${SORTABLE_FIELDS.join(', ')}]`] })
if (of.startsWith('-')) this.error(`Invalid sort syntax: ${clColor.msg.error(of)}`,
{ suggestions: [`To sort records you can use only the syntax ${clColor.cli.value('<field>,<order>')} and not the syntax ${clColor.cli.value('[-]<field>')}`] }
)
const sd = ot[1] || 'asc'
if (!['asc', 'desc'].includes(sd)) this.error(`Invalid sort flag: ${clColor.msg.error(f)}`,
{ suggestions: [`Sort direction can assume only the values ${clColor.cli.value('asc')} or ${clColor.cli.value('desc')}`] }
)

sort[of] = sd as 'asc' | 'desc'

})

}

return sort

}

}
Loading

0 comments on commit 37463ba

Please sign in to comment.