Skip to content

Commit 8f48f5b

Browse files
MathijsVerbeeckplamber
authored andcommitted
Adds 'search externalconnection schema add' command. Closes pnp#3171
1 parent 11542c0 commit 8f48f5b

File tree

5 files changed

+326
-1
lines changed

5 files changed

+326
-1
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# search externalconnection schema add
2+
3+
This command allows the administrator to add a schema to a specific external connection for use in Microsoft Search.
4+
5+
## Usage
6+
7+
```sh
8+
m365 search externalconnection schema add [options]
9+
```
10+
11+
## Options
12+
13+
`-i, --externalConnectionId <externalConnectionId>`
14+
: ID of the External Connection.
15+
16+
`-s, --schema [schema]`
17+
: The schema object to be added.
18+
19+
--8<-- "docs/cmd/_global.md"
20+
21+
## Examples
22+
23+
Adds a new schema to a specific external connection.
24+
25+
```sh
26+
m365 search externalconnection schema add --externalConnectionId 'CliConnectionId' --schema '{"baseType":"microsoft.graph.externalItem","properties":[{"name":"ticketTitle","type":"String","isSearchable":"true","isRetrievable":"true","labels":["title"]},{"name":"priority","type":"String","isQueryable":"true","isRetrievable":"true","isSearchable":"false"},{"name":"assignee","type":"String","isRetrievable":"true"}]}'
27+
```
28+
29+
## Response
30+
31+
The command won't return a response on success.

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ nav:
308308
- externalconnection get: cmd/search/externalconnection/externalconnection-get.md
309309
- externalconnection list: cmd/search/externalconnection/externalconnection-list.md
310310
- externalconnection remove: cmd/search/externalconnection/externalconnection-remove.md
311+
- externalconnection schema add: cmd/search/externalconnection/externalconnection-schema-add.md
311312
- Skype (skype):
312313
- report:
313314
- report activitycounts: cmd/skype/report/report-activitycounts.md

src/m365/search/commands.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export default {
44
EXTERNALCONNECTION_ADD: `${prefix} externalconnection add`,
55
EXTERNALCONNECTION_GET: `${prefix} externalconnection get`,
66
EXTERNALCONNECTION_LIST: `${prefix} externalconnection list`,
7-
EXTERNALCONNECTION_REMOVE: `${prefix} externalconnection remove`
7+
EXTERNALCONNECTION_REMOVE: `${prefix} externalconnection remove`,
8+
EXTERNALCONNECTION_SCHEMA_ADD: `${prefix} externalconnection schema add`
89
};
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import auth from '../../../../Auth';
4+
import { Cli } from '../../../../cli/Cli';
5+
import { CommandInfo } from '../../../../cli/CommandInfo';
6+
import { Logger } from '../../../../cli/Logger';
7+
import Command, { CommandError } from '../../../../Command';
8+
import request from '../../../../request';
9+
import { telemetry } from '../../../../telemetry';
10+
import { pid } from '../../../../utils/pid';
11+
import { sinonUtil } from '../../../../utils/sinonUtil';
12+
import commands from '../../commands';
13+
const command: Command = require('./externalconnection-schema-add');
14+
15+
describe(commands.EXTERNALCONNECTION_SCHEMA_ADD, () => {
16+
const externalConnectionId = 'TestConnectionForCLI';
17+
const schema = '{"baseType": "microsoft.graph.externalItem","properties": [{"name": "ticketTitle","type": "String"}]}';
18+
19+
let log: string[];
20+
let logger: Logger;
21+
let commandInfo: CommandInfo;
22+
23+
before(() => {
24+
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
25+
sinon.stub(telemetry, 'trackEvent').callsFake(() => { });
26+
sinon.stub(pid, 'getProcessName').callsFake(() => '');
27+
auth.service.connected = true;
28+
commandInfo = Cli.getCommandInfo(command);
29+
});
30+
31+
beforeEach(() => {
32+
log = [];
33+
logger = {
34+
log: (msg: string) => {
35+
log.push(msg);
36+
},
37+
logRaw: (msg: string) => {
38+
log.push(msg);
39+
},
40+
logToStderr: (msg: string) => {
41+
log.push(msg);
42+
}
43+
};
44+
});
45+
46+
afterEach(() => {
47+
sinonUtil.restore([
48+
request.post
49+
]);
50+
});
51+
52+
after(() => {
53+
sinonUtil.restore([
54+
auth.restoreAuth,
55+
telemetry.trackEvent,
56+
pid.getProcessName
57+
]);
58+
auth.service.connected = false;
59+
});
60+
61+
it('has correct name', () => {
62+
assert.strictEqual(command.name, commands.EXTERNALCONNECTION_SCHEMA_ADD);
63+
});
64+
65+
it('has a description', () => {
66+
assert.notStrictEqual(command.description, null);
67+
});
68+
69+
it('adds an external connection schema', async () => {
70+
sinon.stub(request, 'post').callsFake(async (opts: any) => {
71+
if (opts.url === `https://graph.microsoft.com/v1.0/external/connections/${externalConnectionId}/schema`) {
72+
return;
73+
}
74+
throw 'Invalid request';
75+
});
76+
await command.action(logger, { options: { schema: schema, externalConnectionId: externalConnectionId, verbose: true } } as any);
77+
});
78+
79+
it('correctly handles error when request is malformed or schema already exists', async () => {
80+
const errorMessage = 'Error: The request is malformed or incorrect.';
81+
sinon.stub(request, 'post').callsFake(async (opts: any) => {
82+
if (opts.url === `https://graph.microsoft.com/v1.0/external/connections/${externalConnectionId}/schema`) {
83+
throw errorMessage;
84+
}
85+
throw 'Invalid request';
86+
});
87+
await assert.rejects(command.action(logger, { options: { schema: schema, externalConnectionId: externalConnectionId } } as any),
88+
new CommandError(errorMessage));
89+
});
90+
91+
it('fails validation if id is less than 3 characters', async () => {
92+
const actual = await command.validate({
93+
options: {
94+
externalConnectionId: 'T',
95+
schema: schema
96+
}
97+
}, commandInfo);
98+
assert.notStrictEqual(actual, false);
99+
});
100+
101+
it('fails validation if id is more than 32 characters', async () => {
102+
const actual = await command.validate({
103+
options: {
104+
externalConnectionId: externalConnectionId + 'zzzzzzzzzzzzzzzzzz',
105+
schema: schema
106+
}
107+
}, commandInfo);
108+
assert.notStrictEqual(actual, false);
109+
});
110+
111+
it('fails validation if id is not alphanumeric', async () => {
112+
const actual = await command.validate({
113+
options: {
114+
externalConnectionId: externalConnectionId + '!',
115+
schema: schema
116+
}
117+
}, commandInfo);
118+
assert.notStrictEqual(actual, false);
119+
});
120+
121+
it('fails validation if id starts with Microsoft', async () => {
122+
const actual = await command.validate({
123+
options: {
124+
externalConnectionId: 'Microsoft' + externalConnectionId,
125+
schema: schema
126+
}
127+
}, commandInfo);
128+
assert.notStrictEqual(actual, false);
129+
});
130+
131+
it('fails validation if schema does not contain baseType', async () => {
132+
const actual = await command.validate({
133+
options: {
134+
externalConnectionId: externalConnectionId,
135+
schema: '{"properties": [{"name": "ticketTitle","type": "String"}]}'
136+
}
137+
}, commandInfo);
138+
assert.notStrictEqual(actual, false);
139+
});
140+
141+
it('fails validation if schema does not contain properties', async () => {
142+
const actual = await command.validate({
143+
options: {
144+
externalConnectionId: externalConnectionId,
145+
schema: '{"baseType": "microsoft.graph.externalItem"}'
146+
}
147+
}, commandInfo);
148+
assert.notStrictEqual(actual, false);
149+
});
150+
151+
it('fails validation if schema does contain more than 128 properties', async () => {
152+
const schemaObject = JSON.parse(schema);
153+
for (let i = 0; i < 128; i++) {
154+
schemaObject.properties.push({
155+
name: `Test${i}`,
156+
type: 'String'
157+
});
158+
}
159+
const actual = await command.validate({
160+
options: {
161+
externalConnectionId: externalConnectionId,
162+
schema: JSON.stringify(schemaObject)
163+
}
164+
}, commandInfo);
165+
assert.notStrictEqual(actual, false);
166+
});
167+
168+
it('passes validation with a correct schema and external connection id', async () => {
169+
const actual = await command.validate({
170+
options: {
171+
externalConnectionId: externalConnectionId,
172+
schema: schema
173+
}
174+
}, commandInfo);
175+
assert.strictEqual(actual, true);
176+
});
177+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { AxiosRequestConfig } from 'axios';
2+
import { Logger } from '../../../../cli/Logger';
3+
import GlobalOptions from '../../../../GlobalOptions';
4+
import request from '../../../../request';
5+
import GraphCommand from '../../../base/GraphCommand';
6+
import commands from '../../commands';
7+
8+
interface CommandArgs {
9+
options: Options;
10+
}
11+
12+
interface Options extends GlobalOptions {
13+
externalConnectionId: string;
14+
schema: string;
15+
}
16+
17+
interface SearchExternalItem {
18+
baseType: string;
19+
properties: Property[];
20+
}
21+
22+
interface Property {
23+
aliases?: string[];
24+
isQueryable?: boolean;
25+
isRefinable?: boolean;
26+
isRetrievable?: boolean;
27+
isSearchable?: boolean;
28+
labels?: string[];
29+
name: string;
30+
type: string;
31+
}
32+
33+
class SearchExternalConnectionSchemaAddCommand extends GraphCommand {
34+
public get name(): string {
35+
return commands.EXTERNALCONNECTION_SCHEMA_ADD;
36+
}
37+
38+
public get description(): string {
39+
return 'This command allows the administrator to add a schema to a specific external connection for use in Microsoft Search.';
40+
}
41+
42+
constructor() {
43+
super();
44+
45+
this.#initOptions();
46+
this.#initValidators();
47+
}
48+
49+
#initOptions(): void {
50+
this.options.unshift(
51+
{
52+
option: '-i, --externalConnectionId <externalConnectionId>'
53+
},
54+
{
55+
option: '-s, --schema <schema>'
56+
}
57+
);
58+
}
59+
60+
#initValidators(): void {
61+
this.validators.push(
62+
async (args: CommandArgs) => {
63+
if (args.options.externalConnectionId.length < 3 || args.options.externalConnectionId.length > 32) {
64+
return 'externalConnectionId must be between 3 and 32 characters in length.';
65+
}
66+
67+
const alphaNumericRegEx = /[^\w]|_/g;
68+
69+
if (alphaNumericRegEx.test(args.options.externalConnectionId)) {
70+
return 'externalConnectionId must only contain alphanumeric characters.';
71+
}
72+
73+
if (args.options.externalConnectionId.length > 9 &&
74+
args.options.externalConnectionId.startsWith('Microsoft')) {
75+
return 'ID cannot begin with Microsoft';
76+
}
77+
78+
const schemaObject: SearchExternalItem = JSON.parse(args.options.schema);
79+
if (schemaObject.baseType === undefined || schemaObject.baseType !== 'microsoft.graph.externalItem') {
80+
return `The schema needs a required property 'baseType' with value 'microsoft.graph.externalItem'`;
81+
}
82+
83+
if (!schemaObject.properties || schemaObject.properties.length > 128) {
84+
return `We need atleast one property and a maximum of 128 properties in the schema object`;
85+
}
86+
87+
return true;
88+
}
89+
);
90+
}
91+
92+
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
93+
if (this.verbose) {
94+
logger.logToStderr(`Adding schema to external connection with id ${args.options.externalConnectionId}`);
95+
}
96+
97+
const requestOptions: AxiosRequestConfig = {
98+
url: `${this.resource}/v1.0/external/connections/${args.options.externalConnectionId}/schema`,
99+
headers: {
100+
accept: 'application/json;odata.metadata=none'
101+
},
102+
responseType: 'json',
103+
data: args.options.schema
104+
};
105+
106+
try {
107+
await request.post(requestOptions);
108+
}
109+
catch (err: any) {
110+
this.handleRejectedODataJsonPromise(err);
111+
}
112+
}
113+
}
114+
115+
module.exports = new SearchExternalConnectionSchemaAddCommand();

0 commit comments

Comments
 (0)