Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ packages/amplify-graphql-api-construct-tests/amplify-e2e-reports
packages/amplify-graphql-api-construct/README.md
packages/amplify-graphql-api-construct/tsconfig.json
packages/amplify-graphql-conversation-transformer/src/__tests__/schemas/*.graphql
packages/amplify-graphql-conversation-transformer/src/resolvers/*.js
packages/amplify-data-construct/README.md
packages/amplify-data-construct/tsconfig.json
packages/amplify-graphql-model-transformer/publish-notification-lambda/lib/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"access": "public"
},
"scripts": {
"build": "tsc",
"build": "tsc && yarn copy-js-resolver-templates",
"watch": "tsc -w",
"clean": "rimraf ./lib",
"copy-js-resolver-templates": "cp ./src/resolvers/*.js ./lib/resolvers",
"test": "jest",
"extract-api": "ts-node ../../scripts/extract-api.ts"
},
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,28 @@ describe('ConversationTransformer', () => {
});

const assertResolverSnapshot = (routeName: string, resources: DeploymentResources) => {
const resolverCode = resources.rootStack.Resources?.[`Mutation${routeName}Resolver`]?.['Properties']['Code'];
const resolverCode = getResolverResource(routeName, resources.rootStack.Resources)['Properties']['Code'];
expect(resolverCode).toBeDefined();
expect(resolverCode).toMatchSnapshot();

const resolverFnCode = getResolverFnResource(routeName, resources);
expect(resolverFnCode).toBeDefined();
expect(resolverFnCode).toMatchSnapshot();
};

const getResolverResource = (mutationName: string, resources?: Record<string, any>): Record<string, any> => {
const resolverName = `Mutation${mutationName}Resolver`;
return resources?.[resolverName];
};

const getResolverFnResource = (mutationName: string, resources: DeploymentResources): string => {
const resolverFnCode =
resources.rootStack.Resources &&
Object.entries(resources.rootStack.Resources).find(([key, _]) => key.startsWith(`Mutation${toUpper(routeName)}DataResolverFn`))?.[1][
Object.entries(resources.rootStack.Resources).find(([key, _]) => key.startsWith(`Mutation${toUpper(mutationName)}DataResolverFn`))?.[1][
'Properties'
]['Code'];

expect(resolverCode).toBeDefined();
expect(resolverCode).toMatchSnapshot();
expect(resolverFnCode).toBeDefined();
expect(resolverFnCode).toMatchSnapshot();
return resolverFnCode;
};

const defaultAuthConfig: AppSyncAuthConfiguration = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { util, extensions } from '@aws-appsync/utils';

export function request(ctx) {
ctx.stash.hasAuth = true;
const isAuthorized = false;

if (util.authType() === 'User Pool Authorization') {
if (!isAuthorized) {
const authFilter = [];
let ownerClaim0 = ctx.identity['claims']['sub'];
ctx.args.owner = ownerClaim0;
const currentClaim1 = ctx.identity['claims']['username'] ?? ctx.identity['claims']['cognito:username'];
if (ownerClaim0 && currentClaim1) {
ownerClaim0 = ownerClaim0 + '::' + currentClaim1;
authFilter.push({ owner: { eq: ownerClaim0 } });
}
const role0_0 = ctx.identity['claims']['sub'];
if (role0_0) {
authFilter.push({ owner: { eq: role0_0 } });
}
// we can just reuse currentClaim1 here, but doing this (for now) to mirror the existing
// vtl auth resolver.
const role0_1 = ctx.identity['claims']['username'] ?? ctx.identity['claims']['cognito:username'];
if (role0_1) {
authFilter.push({ owner: { eq: role0_1 } });
}
if (authFilter.length !== 0) {
ctx.stash.authFilter = { or: authFilter };
}
}
}
if (!isAuthorized && ctx.stash.authFilter.length === 0) {
util.unauthorized();
}
ctx.args.filter = { ...ctx.args.filter, and: [{ conversationId: { eq: ctx.args.conversationId } }] };
return { version: '2018-05-29', payload: {} };
}

export function response(ctx) {
const subscriptionFilter = util.transform.toSubscriptionFilter(ctx.args.filter);
extensions.setSubscriptionFilter(subscriptionFilter);
return null;
}
Original file line number Diff line number Diff line change
@@ -1,81 +1,17 @@
import { MappingTemplate } from '@aws-amplify/graphql-transformer-core';
import { MappingTemplateProvider } from '@aws-amplify/graphql-transformer-interfaces';
import { dedent } from 'ts-dedent';
import fs from 'fs';
import path from 'path';
import { ConversationDirectiveConfiguration } from '../grapqhl-conversation-transformer';

/**
* Creates and returns the mapping template for the conversation message subscription resolver.
* This includes both request and response functions.
*
* @returns {MappingTemplateProvider} An object containing request and response MappingTemplateProviders.
*/
export const conversationMessageSubscriptionMappingTamplate = (): MappingTemplateProvider => {
const req = createAssistantMessagesSubscriptionRequestFunction();
const res = createAssistantMessagesSubscriptionResponseFunction();
return MappingTemplate.inlineTemplateFromString(dedent(req + '\n' + res));
};

/**
* Creates the request function for the conversation message subscription resolver.
* This function handles the authorization and filtering of the conversation messages for owner auth.
*
* @returns {MappingTemplateProvider} A MappingTemplateProvider for the request function.
*/
const createAssistantMessagesSubscriptionRequestFunction = (): string => {
const requestFunctionString = `
export function request(ctx) {
ctx.stash.hasAuth = true;
const isAuthorized = false;

if (util.authType() === 'User Pool Authorization') {
if (!isAuthorized) {
const authFilter = [];
let ownerClaim0 = ctx.identity['claims']['sub'];
ctx.args.owner = ownerClaim0;
const currentClaim1 = ctx.identity['claims']['username'] ?? ctx.identity['claims']['cognito:username'];
if (ownerClaim0 && currentClaim1) {
ownerClaim0 = ownerClaim0 + '::' + currentClaim1;
authFilter.push({ owner: { eq: ownerClaim0 } })
}
const role0_0 = ctx.identity['claims']['sub'];
if (role0_0) {
authFilter.push({ owner: { eq: role0_0 } });
}
// we can just reuse currentClaim1 here, but doing this (for now) to mirror the existing
// vtl auth resolver.
const role0_1 = ctx.identity['claims']['username'] ?? ctx.identity['claims']['cognito:username'];
if (role0_1) {
authFilter.push({ owner: { eq: role0_1 }});
}
if (authFilter.length !== 0) {
ctx.stash.authFilter = { or: authFilter };
}
}
}
if (!isAuthorized && ctx.stash.authFilter.length === 0) {
util.unauthorized();
}
ctx.args.filter = { ...ctx.args.filter, and: [{ conversationId: { eq: ctx.args.conversationId }}]};
return { version: '2018-05-29', payload: {} };
}`;

return requestFunctionString;
};

/**
* Creates the response function for the conversation message subscription resolver.
* This function handles the subscription filter and sets the subscription filter for the conversation messages.
*
* @returns {MappingTemplateProvider} A MappingTemplateProvider for the response function.
*/
const createAssistantMessagesSubscriptionResponseFunction = (): string => {
const responseFunctionString = `
import { util, extensions } from '@aws-appsync/utils';

export function response(ctx) {
const subscriptionFilter = util.transform.toSubscriptionFilter(ctx.args.filter);
extensions.setSubscriptionFilter(subscriptionFilter);
return null;
}`;

return responseFunctionString;
export const conversationMessageSubscriptionMappingTamplate = (config: ConversationDirectiveConfiguration): MappingTemplateProvider => {
const resolver = fs.readFileSync(path.join(__dirname, 'assistant-messages-subscription-resolver-fn.js'), 'utf8');
const templateName = `Subscription.${config.field.name.value}.assistant-message.js`;
return MappingTemplate.s3MappingFunctionCodeFromString(resolver, templateName);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { util } from '@aws-appsync/utils';

/**
* Sends a request to the attached data source
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the request
*/
export function request(ctx) {
const owner = ctx.identity['claims']['sub'];
ctx.stash.owner = owner;
const { conversationId, content, associatedUserMessageId } = ctx.args.input;
const updatedAt = util.time.nowISO8601();

const expression = 'SET #assistantContent = :assistantContent, #updatedAt = :updatedAt';
const expressionNames = { '#assistantContent': 'assistantContent', '#updatedAt': 'updatedAt' };
const expressionValues = { ':assistantContent': content, ':updatedAt': updatedAt };
const condition = JSON.parse(
util.transform.toDynamoDBConditionExpression({
owner: { eq: owner },
conversationId: { eq: conversationId },
}),
);
return {
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ id: associatedUserMessageId }),
condition,
update: {
expression,
expressionNames,
expressionValues: util.dynamodb.toMapValues(expressionValues),
},
};
}

/**
* Returns the resolver result
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the result
*/
export function response(ctx) {
// Update with response logic
if (ctx.error) {
util.error(ctx.error.message, ctx.error.type);
}

const { conversationId, content, associatedUserMessageId } = ctx.args.input;
const { createdAt, updatedAt } = ctx.result;

return {
id: associatedUserMessageId,
content,
conversationId,
role: 'assistant',
owner: ctx.stash.owner,
createdAt,
updatedAt,
};
}
Original file line number Diff line number Diff line change
@@ -1,96 +1,17 @@
import { MappingTemplate } from '@aws-amplify/graphql-transformer-core';
import { MappingTemplateProvider } from '@aws-amplify/graphql-transformer-interfaces';
import { dedent } from 'ts-dedent';
import fs from 'fs';
import path from 'path';
import { ConversationDirectiveConfiguration } from '../grapqhl-conversation-transformer';

/**
* Creates and returns the mapping template for the assistant mutation resolver.
* This includes both request and response functions.
*
* @returns {MappingTemplateProvider} An object containing request and response MappingTemplateProviders.
*/
export const assistantMutationResolver = (): MappingTemplateProvider => {
const req = createAssistantMutationRequestFunction();
const res = createAssistantMutationResponseFunction();
return MappingTemplate.inlineTemplateFromString(dedent(req + '\n' + res));
};

/**
* Creates the request function for the assistant mutation resolver.
* This function handles the update of the assistant's response in the conversation.
*
* @returns {MappingTemplateProvider} A MappingTemplateProvider for the request function.
*/
const createAssistantMutationRequestFunction = (): string => {
const requestFunctionString = `
import { util } from '@aws-appsync/utils';

/**
* Sends a request to the attached data source
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the request
*/
export function request(ctx) {
const owner = ctx.identity['claims']['sub'];
ctx.stash.owner = owner;
const { conversationId, content, associatedUserMessageId } = ctx.args.input;
const updatedAt = util.time.nowISO8601();

const expression = 'SET #assistantContent = :assistantContent, #updatedAt = :updatedAt';
const expressionNames = { '#assistantContent': 'assistantContent', '#updatedAt': 'updatedAt' };
const expressionValues = { ':assistantContent': content, ':updatedAt': updatedAt };
const condition = JSON.parse(
util.transform.toDynamoDBConditionExpression({
owner: { eq: owner },
conversationId: { eq: conversationId }
})
);
return {
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ id: associatedUserMessageId }),
condition,
update: {
expression,
expressionNames,
expressionValues: util.dynamodb.toMapValues(expressionValues),
}
};
}`;

return requestFunctionString;
};

/**
* Creates the response function for the assistant mutation resolver.
* This function handles the processing of the response after the mutation.
*
* @returns {MappingTemplateProvider} A MappingTemplateProvider for the response function.
*/
const createAssistantMutationResponseFunction = (): string => {
const responseFunctionString = `
/**
* Returns the resolver result
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the result
*/
export function response(ctx) {
// Update with response logic
if (ctx.error) {
util.error(ctx.error.message, ctx.error.type);
}

const { conversationId, content, associatedUserMessageId } = ctx.args.input;
const { createdAt, updatedAt } = ctx.result;

return {
id: associatedUserMessageId,
content,
conversationId,
role: 'assistant',
owner: ctx.stash.owner,
createdAt,
updatedAt,
};
}`;

return responseFunctionString;
export const assistantMutationResolver = (config: ConversationDirectiveConfiguration): MappingTemplateProvider => {
const resolver = fs.readFileSync(path.join(__dirname, 'assistant-mutation-resolver-fn.js'), 'utf8');
const templateName = `Mutation.${config.field.name.value}.assistant-response.js`;
return MappingTemplate.s3MappingFunctionCodeFromString(resolver, templateName);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export function request(ctx) {
ctx.stash.hasAuth = true;
const isAuthorized = false;

if (util.authType() === 'User Pool Authorization') {
if (!isAuthorized) {
const authFilter = [];
let ownerClaim0 = ctx.identity['claims']['sub'];
ctx.args.owner = ownerClaim0;
const currentClaim1 = ctx.identity['claims']['username'] ?? ctx.identity['claims']['cognito:username'];
if (ownerClaim0 && currentClaim1) {
ownerClaim0 = ownerClaim0 + '::' + currentClaim1;
authFilter.push({ owner: { eq: ownerClaim0 } });
}
const role0_0 = ctx.identity['claims']['sub'];
if (role0_0) {
authFilter.push({ owner: { eq: role0_0 } });
}
// we can just reuse currentClaim1 here, but doing this (for now) to mirror the existing
// vtl auth resolver.
const role0_1 = ctx.identity['claims']['username'] ?? ctx.identity['claims']['cognito:username'];
if (role0_1) {
authFilter.push({ owner: { eq: role0_1 } });
}
if (authFilter.length !== 0) {
ctx.stash.authFilter = { or: authFilter };
}
}
}
if (!isAuthorized && ctx.stash.authFilter.length === 0) {
util.unauthorized();
}
return { version: '2018-05-29', payload: {} };
}

export function response(ctx) {
return {};
}
Loading