From 2d1aa930e5931f95cfedbfe51323122b7c1f84b6 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Tue, 26 Nov 2024 10:53:58 -0700 Subject: [PATCH 1/8] feat: add api id and amplify environment name to stash --- .changeset/strong-toes-sniff.md | 5 ++ .../src/convert_js_resolvers.test.ts | 50 +++++++++++++++++- .../backend-data/src/convert_js_resolvers.ts | 52 ++++++++++--------- 3 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 .changeset/strong-toes-sniff.md diff --git a/.changeset/strong-toes-sniff.md b/.changeset/strong-toes-sniff.md new file mode 100644 index 0000000000..75cce3dfa6 --- /dev/null +++ b/.changeset/strong-toes-sniff.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-data': minor +--- + +Add GraphQL API ID and Amplify environment name to custom JS resolver stash diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index 51a646de6f..ace4008700 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -1,4 +1,4 @@ -import { Template } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; import { App, Duration, Stack } from 'aws-cdk-lib'; @@ -158,4 +158,52 @@ void describe('convertJsResolverDefinition', () => { template.resourceCountIs('AWS::AppSync::Resolver', 1); }); + + void it('adds api id and environment name to stash', () => { + const absolutePath = resolve( + fileURLToPath(import.meta.url), + '../../lib/assets', + 'js_resolver_handler.js' + ); + + const schema = a.schema({ + customQuery: a + .query() + .authorization((allow) => allow.publicApiKey()) + .returns(a.string()) + .handler( + a.handler.custom({ + entry: absolutePath, + }) + ), + }); + const { jsFunctions } = schema.transform(); + convertJsResolverDefinition(stack, amplifyApi, jsFunctions); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::AppSync::Resolver', { + Runtime: { + Name: 'APPSYNC_JS', + RuntimeVersion: '1.0.0', + }, + Kind: 'PIPELINE', + TypeName: 'Query', + FieldName: 'customQuery', + Code: { + 'Fn::Join': [ + '', + [ + "\n /**\n * Pipeline resolver request handler\n */\n export const request = (ctx) => {\n ctx.stash.apiId = '", + { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'), + 'ApiId', + ], + }, + "';\n ctx.stash.amplifyEnvironmentName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ", + ], + ], + }, + }); + }); }); diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index e4d23eec33..4701cc812e 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -2,34 +2,12 @@ import { Construct } from 'constructs'; import { AmplifyData } from '@aws-amplify/data-construct'; import { CfnFunctionConfiguration, CfnResolver } from 'aws-cdk-lib/aws-appsync'; import { JsResolver } from '@aws-amplify/data-schema-types'; -import { resolve } from 'path'; -import { fileURLToPath } from 'node:url'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; import { resolveEntryPath } from './resolve_entry_path.js'; const APPSYNC_PIPELINE_RESOLVER = 'PIPELINE'; const APPSYNC_JS_RUNTIME_NAME = 'APPSYNC_JS'; const APPSYNC_JS_RUNTIME_VERSION = '1.0.0'; -const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js'; - -/** - * - * This returns the top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js) - * It's required for defining a pipeline resolver. The only purpose it serves is returning the output of the last function in the pipeline back to the client. - * - * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` - */ -const defaultJsResolverAsset = (scope: Construct): Asset => { - const resolvedTemplatePath = resolve( - fileURLToPath(import.meta.url), - '../../lib', - JS_PIPELINE_RESOLVER_HANDLER - ); - - return new Asset(scope, 'default_js_resolver_handler_asset', { - path: resolveEntryPath(resolvedTemplatePath), - }); -}; /** * Converts JS Resolver definition emitted by data-schema into AppSync pipeline @@ -44,8 +22,6 @@ export const convertJsResolverDefinition = ( return; } - const jsResolverTemplateAsset = defaultJsResolverAsset(scope); - for (const resolver of jsResolvers) { const functions: string[] = resolver.handlers.map((handler, idx) => { const fnName = `Fn_${resolver.typeName}_${resolver.fieldName}_${idx + 1}`; @@ -71,12 +47,38 @@ export const convertJsResolverDefinition = ( const resolverName = `Resolver_${resolver.typeName}_${resolver.fieldName}`; + const amplifyEnvironmentName = + scope.node.tryGetContext('amplifyEnvironmentName') ?? 'NONE'; new CfnResolver(scope, resolverName, { apiId: amplifyApi.apiId, fieldName: resolver.fieldName, typeName: resolver.typeName, kind: APPSYNC_PIPELINE_RESOLVER, - codeS3Location: jsResolverTemplateAsset.s3ObjectUrl, + /** + * The top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js) + * It's required for defining a pipeline resolver. Adds the GraphQL API ID and Amplify environment name to the context stash. + * Returns the output of the last function in the pipeline back to the client. + * + * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` + * + * Use inline code to remove circular dependency when adding the API ID. + */ + code: ` + /** + * Pipeline resolver request handler + */ + export const request = (ctx) => { + ctx.stash.apiId = '${amplifyApi.apiId}'; + ctx.stash.amplifyEnvironmentName = '${amplifyEnvironmentName}'; + return {}; + }; + /** + * Pipeline resolver response handler + */ + export const response = (ctx) => { + return ctx.prev.result; + }; + `, runtime: { name: APPSYNC_JS_RUNTIME_NAME, runtimeVersion: APPSYNC_JS_RUNTIME_VERSION, From 263efd0cccf416849b06a470e6cf88abc248e96c Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Tue, 26 Nov 2024 11:56:27 -0700 Subject: [PATCH 2/8] chore: add backend package to changeset --- .changeset/strong-toes-sniff.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/strong-toes-sniff.md b/.changeset/strong-toes-sniff.md index 75cce3dfa6..f20c42c831 100644 --- a/.changeset/strong-toes-sniff.md +++ b/.changeset/strong-toes-sniff.md @@ -1,5 +1,6 @@ --- '@aws-amplify/backend-data': minor +'@aws-amplify/backend': minor --- Add GraphQL API ID and Amplify environment name to custom JS resolver stash From 59102df26fd01195fd0f9bd3469085dad31cad59 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Tue, 26 Nov 2024 14:33:48 -0700 Subject: [PATCH 3/8] fix: update stash variable names --- packages/backend-data/src/convert_js_resolvers.test.ts | 4 ++-- packages/backend-data/src/convert_js_resolvers.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index ace4008700..07b7980de2 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -193,14 +193,14 @@ void describe('convertJsResolverDefinition', () => { 'Fn::Join': [ '', [ - "\n /**\n * Pipeline resolver request handler\n */\n export const request = (ctx) => {\n ctx.stash.apiId = '", + "\n /**\n * Pipeline resolver request handler\n */\n export const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '", { 'Fn::GetAtt': [ Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'), 'ApiId', ], }, - "';\n ctx.stash.amplifyEnvironmentName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ", + "';\n ctx.stash.awsAmplifyEnvironmentName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ", ], ], }, diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index 4701cc812e..051a6d4a26 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -68,8 +68,8 @@ export const convertJsResolverDefinition = ( * Pipeline resolver request handler */ export const request = (ctx) => { - ctx.stash.apiId = '${amplifyApi.apiId}'; - ctx.stash.amplifyEnvironmentName = '${amplifyEnvironmentName}'; + ctx.stash.awsAppsyncApiId = '${amplifyApi.apiId}'; + ctx.stash.awsAmplifyEnvironmentName = '${amplifyEnvironmentName}'; return {}; }; /** From 304d3c1d1d61ac36e851ce8f01bd0fba75ae4a03 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Wed, 27 Nov 2024 08:28:29 -0700 Subject: [PATCH 4/8] Update packages/backend-data/src/convert_js_resolvers.ts Co-authored-by: Tim Schmelter --- packages/backend-data/src/convert_js_resolvers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index 051a6d4a26..8fe5bb5ef0 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -61,7 +61,7 @@ export const convertJsResolverDefinition = ( * * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` * - * Use inline code to remove circular dependency when adding the API ID. + * Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable. */ code: ` /** From bc9b37c74acf34fc6a2889f3de710e27512a2f2c Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Wed, 27 Nov 2024 09:49:50 -0700 Subject: [PATCH 5/8] fix: change awsAmplifyEnvironmentName to amplifyBranchName --- packages/backend-data/src/convert_js_resolvers.test.ts | 2 +- packages/backend-data/src/convert_js_resolvers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index 07b7980de2..3603096712 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -200,7 +200,7 @@ void describe('convertJsResolverDefinition', () => { 'ApiId', ], }, - "';\n ctx.stash.awsAmplifyEnvironmentName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ", + "';\n ctx.stash.amplifyBranchName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ", ], ], }, diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index 8fe5bb5ef0..bb1a11e966 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -69,7 +69,7 @@ export const convertJsResolverDefinition = ( */ export const request = (ctx) => { ctx.stash.awsAppsyncApiId = '${amplifyApi.apiId}'; - ctx.stash.awsAmplifyEnvironmentName = '${amplifyEnvironmentName}'; + ctx.stash.amplifyBranchName = '${amplifyEnvironmentName}'; return {}; }; /** From 9c6ab19f2345b46e0460b94ee75efdb6fb4a1f31 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Mon, 2 Dec 2024 09:26:52 -0700 Subject: [PATCH 6/8] fix: use compiled file --- .../src/assets/js_resolver_handler.ts | 4 +- .../src/convert_js_resolvers.test.ts | 4 +- .../backend-data/src/convert_js_resolvers.ts | 55 ++++++++++--------- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/packages/backend-data/src/assets/js_resolver_handler.ts b/packages/backend-data/src/assets/js_resolver_handler.ts index 282dfb725d..230dd6cb40 100644 --- a/packages/backend-data/src/assets/js_resolver_handler.ts +++ b/packages/backend-data/src/assets/js_resolver_handler.ts @@ -1,7 +1,9 @@ /** * Pipeline resolver request handler */ -export const request = () => { +export const request = (ctx: Record>) => { + ctx.stash.awsAppsyncApiId = '${amplifyApiId}'; + ctx.stash.amplifyBranchName = '${amplifyEnvironmentName}'; return {}; }; /** diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index 3603096712..37d8e0d06e 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -193,14 +193,14 @@ void describe('convertJsResolverDefinition', () => { 'Fn::Join': [ '', [ - "\n /**\n * Pipeline resolver request handler\n */\n export const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '", + "/**\n * Pipeline resolver request handler\n */\nexport const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '", { 'Fn::GetAtt': [ Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'), 'ApiId', ], }, - "';\n ctx.stash.amplifyBranchName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ", + "';\n ctx.stash.amplifyBranchName = 'NONE';\n return {};\n};\n/**\n * Pipeline resolver response handler\n */\nexport const response = (ctx) => {\n return ctx.prev.result;\n};\n", ], ], }, diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index bb1a11e966..d9ca655f93 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -2,12 +2,40 @@ import { Construct } from 'constructs'; import { AmplifyData } from '@aws-amplify/data-construct'; import { CfnFunctionConfiguration, CfnResolver } from 'aws-cdk-lib/aws-appsync'; import { JsResolver } from '@aws-amplify/data-schema-types'; +import { resolve } from 'path'; +import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'fs'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; import { resolveEntryPath } from './resolve_entry_path.js'; const APPSYNC_PIPELINE_RESOLVER = 'PIPELINE'; const APPSYNC_JS_RUNTIME_NAME = 'APPSYNC_JS'; const APPSYNC_JS_RUNTIME_VERSION = '1.0.0'; +const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js'; + +/** + * + * This returns the top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js) + * It's required for defining a pipeline resolver. The only purpose it serves is returning the output of the last function in the pipeline back to the client. + * + * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` + * + * Add Amplify API ID and environment name to the context stash for use in the customer-provided handlers. + */ +const defaultJsResolverCode = ( + amplifyApiId: string, + amplifyEnvironmentName: string +): string => { + const resolvedTemplatePath = resolve( + fileURLToPath(import.meta.url), + '../../lib', + JS_PIPELINE_RESOLVER_HANDLER + ); + + return readFileSync(resolvedTemplatePath, 'utf-8') + .replace('${amplifyApiId}', amplifyApiId) + .replace('${amplifyEnvironmentName}', amplifyEnvironmentName); +}; /** * Converts JS Resolver definition emitted by data-schema into AppSync pipeline @@ -54,31 +82,8 @@ export const convertJsResolverDefinition = ( fieldName: resolver.fieldName, typeName: resolver.typeName, kind: APPSYNC_PIPELINE_RESOLVER, - /** - * The top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js) - * It's required for defining a pipeline resolver. Adds the GraphQL API ID and Amplify environment name to the context stash. - * Returns the output of the last function in the pipeline back to the client. - * - * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` - * - * Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable. - */ - code: ` - /** - * Pipeline resolver request handler - */ - export const request = (ctx) => { - ctx.stash.awsAppsyncApiId = '${amplifyApi.apiId}'; - ctx.stash.amplifyBranchName = '${amplifyEnvironmentName}'; - return {}; - }; - /** - * Pipeline resolver response handler - */ - export const response = (ctx) => { - return ctx.prev.result; - }; - `, + // Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable. + code: defaultJsResolverCode(amplifyApi.apiId, amplifyEnvironmentName), runtime: { name: APPSYNC_JS_RUNTIME_NAME, runtimeVersion: APPSYNC_JS_RUNTIME_VERSION, From 80684b52b7774b8d0a4e56d4d457414fe92f84e4 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Mon, 2 Dec 2024 10:14:50 -0700 Subject: [PATCH 7/8] test: add test to assert js resolver is valid js --- .../src/convert_js_resolvers.test.ts | 32 +++++++++++++++++-- .../backend-data/src/convert_js_resolvers.ts | 2 +- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index 37d8e0d06e..6a133536a6 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -6,10 +6,15 @@ import { AmplifyData, AmplifyDataDefinition, } from '@aws-amplify/data-construct'; -import { resolve } from 'path'; +import { join, resolve } from 'path'; +import { tmpdir } from 'os'; import { fileURLToPath } from 'url'; -import { convertJsResolverDefinition } from './convert_js_resolvers.js'; +import { + convertJsResolverDefinition, + defaultJsResolverCode, +} from './convert_js_resolvers.js'; import { a } from '@aws-amplify/data-schema'; +import { writeFileSync } from 'node:fs'; // stub schema for the AmplifyApi construct // not relevant to this test suite @@ -28,6 +33,29 @@ const createStackAndSetContext = (): Stack => { return stack; }; +void describe('defaultJsResolverCode', () => { + void it('returns the default JS resolver code with api id and env name in valid JS', async () => { + const code = defaultJsResolverCode('testApiId', 'testEnvName'); + assert(code.includes("ctx.stash.awsAppsyncApiId = 'testApiId';")); + assert(code.includes("ctx.stash.amplifyBranchName = 'testEnvName';")); + + const tempDir = tmpdir(); + const filename = join(tempDir, 'js_resolver_handler.js'); + writeFileSync(filename, code); + + const resolver = await import(filename); + const context = { stash: {}, prev: { result: 'result' } }; + assert.deepEqual(resolver.request(context), {}); + + // assert api id and env name are added to the context stash + assert.deepEqual(context.stash, { + awsAppsyncApiId: 'testApiId', + amplifyBranchName: 'testEnvName', + }); + assert.equal(resolver.response(context), 'result'); + }); +}); + void describe('convertJsResolverDefinition', () => { let stack: Stack; let amplifyApi: AmplifyData; diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index d9ca655f93..ffc2066c0e 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -22,7 +22,7 @@ const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js'; * * Add Amplify API ID and environment name to the context stash for use in the customer-provided handlers. */ -const defaultJsResolverCode = ( +export const defaultJsResolverCode = ( amplifyApiId: string, amplifyEnvironmentName: string ): string => { From 9ad666cab991a5aca80679d26c4e2ee6eb860343 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Mon, 2 Dec 2024 10:54:26 -0700 Subject: [PATCH 8/8] test: use file url for dynamic import --- .eslint_dictionary.json | 1 + packages/backend-data/src/convert_js_resolvers.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index 4f0f494eee..37e2a7cc16 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -76,6 +76,7 @@ "hotfix", "hotswap", "hotswapped", + "href", "iamv2", "identitypool", "idps", diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index 6a133536a6..421348fb91 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -8,7 +8,7 @@ import { } from '@aws-amplify/data-construct'; import { join, resolve } from 'path'; import { tmpdir } from 'os'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; import { convertJsResolverDefinition, defaultJsResolverCode, @@ -43,7 +43,9 @@ void describe('defaultJsResolverCode', () => { const filename = join(tempDir, 'js_resolver_handler.js'); writeFileSync(filename, code); - const resolver = await import(filename); + // windows requires dynamic imports to use file urls + const fileUrl = pathToFileURL(filename).href; + const resolver = await import(fileUrl); const context = { stash: {}, prev: { result: 'result' } }; assert.deepEqual(resolver.request(context), {});