Skip to content

Commit

Permalink
feat: add entities:create command (#28)
Browse files Browse the repository at this point in the history
* feat: add entities:create command
  • Loading branch information
grof committed Jul 18, 2022
1 parent 2a25abe commit 8a31f40
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 13 deletions.
187 changes: 187 additions & 0 deletions src/commands/entities/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright 2022, Nuance, Inc. and its contributors.
* All rights reserved.
*
* This source code is licensed under the Apache-2.0 license found in
* the LICENSE file in the root directory of this source tree.
*/

import chalk from 'chalk'
import {flags} from '@oclif/command'
import makeDebug from 'debug'

import * as EntitiesAPI from '../../mix/api/entities'
import * as MixFlags from '../../utils/flags'
import {DomainOption} from '../../utils/validations'
import {EntitiesCreateParams, MixClient, MixResponse, MixResult} from '../../mix/types'
import MixCommand from '../../utils/base/mix-command'
import {validateRegexEntityParams, validateRuleBasedEntityParams} from '../../utils/validations'

const debug = makeDebug('mix:commands:entities:create')

export default class EntitiesCreate extends MixCommand {
static description = `create a new entity
Use this command to create a new entity in a project.
Mix supports several types of entities: freeform, list, regex,
relational and rule-based. There are many attributes that can
be passed for the creation of an entity and a good number of
these attributes are common to all entity types. However, certain
attributes are mandatory and apply to specific entity types only.
Regex entities make use of regular expressions specific to a single
locale. The --pattern and --locale flags must be set when creating
an entity of type "regex".
Relationial entities can have zero or one isA relation and
zero or many hasA relations. One --is-A or --has-A flag must be
set at a minimum when creating an entity of type "relational".
The --entity-type, --name and --project flags are mandatory for
the creation of any entity type.
The examples below show how to create each type of entity.
In each example, every allowed or mandatory flag is used.
Note that many flags have default values and do not need to be
explicitly provided.
`

static examples = [
'Create a freeform entity',
'$ mix entities:create -P 1922 --entity-type=freeform --name MESSAGE \\',
' --sensitive --no-canonicalize --data-type not-set',
'',
'Create a list entity',
'$ mix entities:create -P 1922 --entity-type=list --name DRINK_SIZE \\',
' --dynamic --sensitive --no-canonicalize --anaphora-type not-set \\',
' --data-type not-set',
'',
'Create a regex entity',
'$ mix entities:create -P 1922 --entity-type=regex --name PHONE_NUMBER \\',
' --locale en-US --pattern \\d{10} --sensitive --no-canonicalize \\',
' --anaphora-type not-set --data-type digits',
'',
'Create a relational entity',
'$ mix entities:create -P 1922 --entity-type=relational --name ARRIVAL_CITY \\',
' --is-a CITY --sensitive --no-canonicalize --anaphora-type not-set \\',
' --data-type not-set',
'',
'Create a rule-based entity',
'$ mix entities:create -P 1922 --entity-type=rule-based --name CARD_TYPE \\',
' --sensitive --no-canonicalize --anaphora-type not-set --data-type not-set',
]

static flags = {
'anaphora-type': MixFlags.anaphoraTypeFlag,
'data-type': MixFlags.dataTypeFlag,
dynamic: MixFlags.dynamicFlag,
'entity-type': MixFlags.withEntityTypeFlag,
'has-a': MixFlags.hasAFlag,
'is-a': MixFlags.isAFlag,
locale: MixFlags.regexLocaleFlag,
name: MixFlags.entityNameFlag,
'no-canonicalize': MixFlags.noCanonicalizeFlag,
pattern: MixFlags.patternFlag,
project: MixFlags.projectFlag,
sensitive: MixFlags.sensitiveUserDataFlag,
// output flags
json: MixFlags.jsonFlag,
yaml: MixFlags.yamlFlag,
}

get domainOptions(): DomainOption[] {
debug('get domainOptions()')
return ['locale', 'project']
}

async buildRequestParameters(options: Partial<flags.Output>): Promise<EntitiesCreateParams> {
debug('buildRequestParameters()')
const {
'anaphora-type': anaphora,
'no-canonicalize': noCanonicalize,
'data-type': dataType,
'has-a': hasA,
'is-a': isA,
dynamic: isDynamic,
'entity-type': entityType,
sensitive: isSensitive,
locale,
name,
pattern,
project: projectId,
} = options

return {
anaphora: `ANAPHORA_${anaphora.toUpperCase().replace('-', '_')}`,
canonicalize: !noCanonicalize,
dataType: dataType.toUpperCase().replace('-', '_'),
entityType: entityType,
hasA,
isA,
isDynamic,
isSensitive,
locale,
name,
pattern,
projectId,
}
}

doRequest(client: MixClient, params: EntitiesCreateParams): Promise<MixResponse> {
debug('doRequest()')
return EntitiesAPI.createEntity(client, params)
}

outputHumanReadable(transformedData: any) {
debug('outputHumanReadable()')
const {entityId, name} = transformedData
this.log(`Entity ${chalk.cyan(name)} with ID ${chalk.cyan(entityId)} created.`)
}

setRequestActionMessage(options: any) {
debug('setRequestActionMessage()')
this.requestActionMessage = `Creating entity ${options.name} in project ${options.project}`
}

transformResponse(result: MixResult) {
debug('transformResponse()')
const data = result.data as any
const [type] = Object.keys(data?.entity)
const entity = data?.entity[type] ?? {}

const {
name,
id: entityId,
} = entity

return {
entityId,
name,
}
}

tryDomainOptionsValidation(options: Partial<flags.Output>, domainOptions: DomainOption[]) {
debug('tryDomainOptionsValidation()')
super.tryDomainOptionsValidation(options, domainOptions)

const {
'entity-type': entityType,
'has-a': hasA,
'is-a': isA,
locale,
pattern,
} = options

// Entity types 'regex' and 'relational' require mandatory flags.
// Command bails out if mandatory parameters are missing.
// Extraneous parameters are ignored.
if (entityType === 'regex') {
validateRegexEntityParams(locale, pattern)
}

if (entityType === 'relational') {
validateRuleBasedEntityParams(hasA, isA)
}
}
}
90 changes: 82 additions & 8 deletions src/mix/api/entities-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,85 @@

import {Expand} from './shared-types'

export const AnaphoraDefault = 'not-set'
export const Anaphoras = {
[AnaphoraDefault]: 'ANAPHORA_NOT_SET',
'ref-moment': 'ANAPHORA_REF_MOMENT',
'ref-person': 'ANAPHONRA_REF_PERSON',
'ref-place': 'ANAPHORA_REF_PLACE',
'ref-thing': 'ANAPHORA_REF_THING',
}

export type Anaphora = typeof Anaphoras[keyof typeof Anaphoras]

export const DataTypeDefault = 'not-set'
export const DataTypes = {
alphanum: 'ALPHANUM',
amount: 'AMOUNT',
boolean: 'BOOLEAN',
date: 'DATE',
digits: 'DIGITS',
distance: 'DISTANCE',
'no-format': 'NO_FORMAT',
[DataTypeDefault]: 'NOT_SET',
number: 'NUMBER',
temperature: 'TEMPERATURE',
time: 'TIME',
'yes-no': 'YES_NO',
}

export type DataType = typeof DataTypes[keyof typeof DataTypes]

/** Entity type */
export type Entity =
| 'UNSPECIFIED'
| 'BASE'
| 'RELATIONAL'
| 'LIST'
| 'FREEFORM'
| 'REGEX'
| 'RULE_BASED'
export const Entities = {
base: 'BASE',
freeform: 'FREEFORM',
list: 'LIST',
regex: 'REGEX',
relational: 'RELATIONAL',
'rule-based': 'RULE_BASED',
}

export type Entity = typeof Entities[keyof typeof Entities]

/** @hidden */
export type EntitiesConfigureBodyParams = {
/** Name of the entity that this entity has an `isA` relationship with */
isA?: string,

/** Names of the entities that this entity has a `hasA` relationship with */
hasA?: string[],

/** Specifies the referrer for this entity */
anaphora?: Anaphora

/** Data type for the entity */
dataType?: DataType

/** Data type for the entity */
entityType?: Entity

/** When set to true, indicates that the entity is dynamic */
isDynamic?: boolean

/** When set to true, indicates that the entity is sensitive */
isSensitive?: boolean

/** When set to true, indicates that the entity is canonicalized */
canonicalize?: boolean

/** Locale for the pattern */
locale?: string

/** Regular expression pattern for this entity */
pattern?: string
}

/** @hidden */
export type EntitiesCreateBodyParams = EntitiesConfigureBodyParams & {
/** New entity name */
name: string,
}

/** @hidden */
export type EntitiesGetPathParams = {
Expand Down Expand Up @@ -46,13 +116,17 @@ export type EntitiesRenameBodyParams = {
}

/** @hidden */
export type EntitiesConfigureParams = Expand<EntitiesGetPathParams & EntitiesConfigureBodyParams>
export type EntitiesCreateParams = Expand<EntitiesListPathParams & EntitiesCreateBodyParams>
export type EntitiesDeleteParams = Expand<EntitiesGetPathParams>
export type EntitiesGetParams = Expand<EntitiesGetPathParams>
export type EntitiesListParams = Expand<EntitiesListPathParams & EntitiesListSearchParams>
export type EntitiesRenameParams = Expand<EntitiesGetPathParams & EntitiesRenameBodyParams>

/** @hidden */
export type EntitiesParams =
| EntitiesConfigureParams
| EntitiesCreateParams
| EntitiesDeleteParams
| EntitiesGetParams
| EntitiesListParams
Expand Down
21 changes: 21 additions & 0 deletions src/mix/api/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

import makeDebug from 'debug'

import {buildCreateOrUpdateEntityBody} from './utils/entities-helpers'
import buildURL from './utils/build-url'
import {
EntitiesCreateParams,
EntitiesDeleteParams,
EntitiesGetParams,
EntitiesListParams,
Expand All @@ -20,6 +22,25 @@ import {MixClient, MixResponse} from '../types'

const debug = makeDebug('mix:api:entities')

/**
* Create a new entity in a project.
*
* @category entities
*/
export async function createEntity(client: MixClient, params: EntitiesCreateParams): Promise<MixResponse> {
debug('createEntity()')
const {projectId, ...bodyParams} = params
const body = buildCreateOrUpdateEntityBody(bodyParams)

debug('body: %O', body)

return client.request({
method: 'post',
url: buildURL(client.getServer(), `/v4/projects/${projectId}/entities`),
data: body,
})
}

/**
* Delete an entity from a project.
*
Expand Down
51 changes: 51 additions & 0 deletions src/mix/api/utils/entities-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2022, Nuance, Inc. and its contributors.
* All rights reserved.
*
* This source code is licensed under the Apache-2.0 license found in
* the LICENSE file in the root directory of this source tree.
*/

import makeDebug from 'debug'

const debug = makeDebug.debug('mix:utils:entities-helpers')

export const buildCreateOrUpdateEntityBody = (params: any) => {
debug('buildCreateOrUpdateEntityBody()')

const {
anaphora,
canonicalize,
dataType,
entityType,
hasA,
isA,
isDynamic,
isSensitive,
locale,
name,
pattern,
} = params

let entityTypeKey
entityTypeKey = entityType === 'rule-based' ? 'ruleBased' : entityType
entityTypeKey = `${entityTypeKey}Entity`

return {
[entityTypeKey]: {
...(anaphora !== undefined && {anaphora}),
...(entityType === 'list') && {data: {}},
...(dataType !== undefined && {dataType}),
...(hasA !== undefined && {hasA: {entities: hasA}}),
...(isA !== undefined && {isA}),
...(isDynamic !== undefined && {isDynamic}),
...(locale !== undefined && {locale}),
...(name !== undefined && {name}),
...(pattern !== undefined && {pattern}),
settings: {
...(canonicalize !== undefined && {canonicalize}),
...(isSensitive !== undefined && {isSensitive}),
},
},
}
}
Loading

0 comments on commit 8a31f40

Please sign in to comment.