Skip to content

Commit

Permalink
Merge branch 'main' into sorgh/splash_screen
Browse files Browse the repository at this point in the history
  • Loading branch information
GeoffCoxMSFT committed Sep 15, 2020
2 parents bd628a5 + f5c30f3 commit 69d7cc0
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,34 +187,47 @@ describe('generateDispatchModels', () => {
const dialogs: any = [{ id: 'test', content: { recognizer: 'test.lu' } }];
const selectedTriggers = [];
const luFiles = [];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles);
const qnaFiles = [];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles);
expect(result).toEqual({});
});

it("should return empty object if the schema doesn't include dispatchModels", () => {
const schema = { properties: {} };
const dialogs: any = [{ id: 'test', content: { recognizer: 'test.lu' } }];
const dialogs: any = [{ id: 'test', content: { recognizer: 'test.lu.qna' } }];
const selectedTriggers = [{ $kind: SDKKinds.OnIntent, intent: 'testIntent' }];
const luFiles: any = [{ id: 'test.en-us' }, { id: 'test.fr-FR' }];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles);
const luFiles: any = [
{ id: 'test.en-us', empty: false },
{ id: 'test.fr-FR', empty: false },
];
const qnaFiles = [];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles);
expect(result).toEqual({});
});

it('should return empty object if the recognizer type is not luis', () => {
const schema = { properties: {} };
const dialogs: any = [{ id: 'test', content: {} }];
const selectedTriggers = [{ $kind: SDKKinds.OnIntent, intent: 'testIntent' }];
const luFiles: any = [{ id: 'test.en-us' }, { id: 'test.fr-FR' }];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles);
const luFiles: any = [
{ id: 'test.en-us', empty: false },
{ id: 'test.fr-FR', empty: false },
];
const qnaFiles = [];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles);
expect(result).toEqual({});
});

it('should return dispatch models', () => {
const schema = { properties: { dispatchModels: {} } };
const dialogs: any = [{ id: 'test', content: { recognizer: 'test.lu' }, isRoot: true }];
const dialogs: any = [{ id: 'test', content: { recognizer: 'test.lu.qna' }, isRoot: true }];
const selectedTriggers = [{ $kind: SDKKinds.OnIntent, intent: 'testIntent' }];
const luFiles: any = [{ id: 'test.en-us' }, { id: 'test.fr-FR' }];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles);
const luFiles: any = [
{ id: 'test.en-us', empty: false },
{ id: 'test.fr-FR', empty: false },
];
const qnaFiles: any = [{ id: 'test.es-es', empty: false }];
const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles);
expect(result).toEqual(
expect.objectContaining({
dispatchModels: {
Expand All @@ -223,15 +236,23 @@ describe('generateDispatchModels', () => {
{
name: 'test',
contentType: 'application/lu',
url: `<test.en-us url>`,
url: `<test.en-us.lu url>`,
description: '<description>',
},
],
'fr-FR': [
{
name: 'test',
contentType: 'application/lu',
url: `<test.fr-FR url>`,
url: `<test.fr-FR.lu url>`,
description: '<description>',
},
],
'es-es': [
{
name: 'test',
contentType: 'application/qna',
url: `<test.es-es.qna url>`,
description: '<description>',
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import get from 'lodash/get';
import { DialogInfo, DialogSchemaFile, ITrigger, SDKKinds, SkillManifest, LuFile } from '@bfc/shared';
import { DialogInfo, DialogSchemaFile, ITrigger, SDKKinds, SkillManifest, LuFile, QnAFile } from '@bfc/shared';
import { JSONSchema7 } from '@bfc/extension-client';

import { Activities, Activity, activityHandlerMap, ActivityTypes, DispatchModels } from './constants';
Expand All @@ -17,6 +17,7 @@ export const generateSkillManifest = (
dialogs: DialogInfo[],
dialogSchemas: DialogSchemaFile[],
luFiles: LuFile[],
qnaFiles: QnAFile[],
selectedTriggers: ITrigger[],
selectedDialogs: Partial<DialogInfo>[]
) => {
Expand All @@ -41,7 +42,7 @@ export const generateSkillManifest = (
}, []);

const activities = generateActivities(dialogSchemas, triggers, resolvedDialogs);
const dispatchModels = generateDispatchModels(schema, dialogs, triggers, luFiles);
const dispatchModels = generateDispatchModels(schema, dialogs, triggers, luFiles, qnaFiles);
const definitions = getDefinitions(dialogSchemas, resolvedDialogs);

return {
Expand Down Expand Up @@ -104,7 +105,8 @@ export const generateDispatchModels = (
schema: JSONSchema7,
dialogs: DialogInfo[],
selectedTriggers: any[],
luFiles: LuFile[]
luFiles: LuFile[],
qnaFiles: QnAFile[]
): { dispatchModels?: DispatchModels } => {
const intents = selectedTriggers.filter(({ $kind }) => $kind === SDKKinds.OnIntent).map(({ intent }) => intent);
const { id: rootId } = dialogs.find((dialog) => dialog?.isRoot) || {};
Expand All @@ -114,16 +116,46 @@ export const generateDispatchModels = (
return luId === rootId;
});

if (!intents.length || !schema.properties?.dispatchModels) {
const rootQnAFiles = qnaFiles.filter(({ id: qnaFileId }) => {
const [qnaId] = qnaFileId.split('.');
return qnaId === rootId;
});

if (!schema.properties?.dispatchModels) {
return {};
}

const languages = rootLuFiles.reduce((acc, { id }) => {
const luLanguages = intents.length
? rootLuFiles.reduce((acc, { empty, id }) => {
const [name, locale] = id.split('.');
const { content = {} } = dialogs.find(({ id }) => id === name) || {};
const { recognizer = '' } = content;

if (!recognizer.includes('.lu') || empty) {
return acc;
}

return {
...acc,
[locale]: [
...(acc[locale] ?? []),
{
name,
contentType: 'application/lu',
url: `<${id}.lu url>`,
description: '<description>',
},
],
};
}, {})
: {};

const languages = rootQnAFiles.reduce((acc, { empty, id }) => {
const [name, locale] = id.split('.');
const { content = {} } = dialogs.find(({ id }) => id === name) || {};
const { recognizer = '' } = content;

if (!''.endsWith.call(recognizer, '.lu')) {
if (!recognizer.includes('.qna') || empty) {
return acc;
}

Expand All @@ -133,19 +165,21 @@ export const generateDispatchModels = (
...(acc[locale] ?? []),
{
name,
contentType: 'application/lu',
url: `<${id} url>`,
contentType: 'application/qna',
url: `<${id}.qna url>`,
description: '<description>',
},
],
};
}, {});
}, luLanguages);

const dispatchModels = {
...(Object.keys(languages).length ? { languages } : {}),
...(intents.length ? { intents } : {}),
};

return {
dispatchModels: {
...(Object.keys(languages).length ? { languages } : {}),
...(intents.length ? { intents } : {}),
},
...(Object.keys(dispatchModels).length ? { dispatchModels } : {}),
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
dispatcherState,
luFilesState,
skillManifestsState,
qnaFilesState,
} from '../../../recoilModel';

import { editorSteps, ManifestEditorSteps, order } from './constants';
Expand All @@ -34,6 +35,7 @@ const ExportSkillModal: React.FC<ExportSkillModalProps> = ({ onSubmit, onDismiss
const dialogs = useRecoilValue(dialogsState);
const dialogSchemas = useRecoilValue(dialogSchemasState);
const luFiles = useRecoilValue(luFilesState);
const qnaFiles = useRecoilValue(qnaFilesState);
const skillManifests = useRecoilValue(skillManifestsState);
const { updateSkillManifest } = useRecoilValue(dispatcherState);

Expand All @@ -59,6 +61,7 @@ const ExportSkillModal: React.FC<ExportSkillModalProps> = ({ onSubmit, onDismiss
dialogs,
dialogSchemas,
luFiles,
qnaFiles,
selectedTriggers,
selectedDialogs
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe('dialog operation', () => {
const mockReq = {
params: { projectId },
query: {},
body: { name: 'bot1.dialog', content: '' },
body: { name: 'bot1.dialog', content: JSON.stringify({ $kind: 'aaa' }) },
} as Request;
await ProjectController.updateFile(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
Expand All @@ -196,7 +196,7 @@ describe('dialog operation', () => {
const mockReq = {
params: { projectId },
query: {},
body: { name: 'test2.dialog', content: '' },
body: { name: 'test2.dialog', content: JSON.stringify({ $kind: 'aaa' }) },
} as Request;
await ProjectController.createFile(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
Expand Down
24 changes: 22 additions & 2 deletions Composer/packages/server/src/models/bot/botProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export class BotProject implements IBotProject {
}

const relativePath = file.relativePath;
this._validateFileContent(name, content);
const lastModified = await this._updateFile(relativePath, content);
return lastModified;
};
Expand Down Expand Up @@ -414,6 +415,7 @@ export class BotProject implements IBotProject {
public createFile = async (name: string, content = '') => {
const filename = name.trim();
this.validateFileName(filename);
this._validateFileContent(name, content);
const botName = this.name;
const defaultLocale = this.settings?.defaultLanguage || defaultLanguage;
const relativePath = defaultFilePath(botName, defaultLocale, filename);
Expand Down Expand Up @@ -610,6 +612,9 @@ export class BotProject implements IBotProject {
// to root dir instead of dataDir dataDir is not aware at this layer
private _createFile = async (relativePath: string, content: string) => {
const absolutePath = Path.resolve(this.dir, relativePath);
if (!absolutePath.startsWith(this.dir)) {
throw new Error('Cannot create file outside of current project folder');
}
await this.ensureDirExists(Path.dirname(absolutePath));
debug('Creating file: %s', absolutePath);
await this.fileStorage.writeFile(absolutePath, content);
Expand Down Expand Up @@ -640,7 +645,9 @@ export class BotProject implements IBotProject {
}

const absolutePath = `${this.dir}/${relativePath}`;

if (!absolutePath.startsWith(this.dir)) {
throw new Error('Cannot update file outside of current project folder');
}
// only write if the file has actually changed
if (file.content !== content) {
file.content = content;
Expand All @@ -666,7 +673,6 @@ export class BotProject implements IBotProject {
const absolutePath = `${this.dir}/${relativePath}`;
await this.fileStorage.removeFile(absolutePath);
};

// ensure dir exist, dir is a absolute dir path
private ensureDirExists = async (dir: string) => {
if (!dir || dir === '.') {
Expand Down Expand Up @@ -764,4 +770,18 @@ export class BotProject implements IBotProject {
};
}
};

private _validateFileContent = (name: string, content: string) => {
const extension = Path.extname(name);
if (extension === '.dialog' || name === 'appsettings.json') {
try {
const parsedContent = JSON.parse(content);
if (typeof parsedContent !== 'object' || Array.isArray(parsedContent)) {
throw new Error('Invalid file content');
}
} catch (e) {
throw new Error('Invalid file content');
}
}
};
}

0 comments on commit 69d7cc0

Please sign in to comment.