diff --git a/.github/workflows/functionapp-cicd.yml b/.github/workflows/functionapp-cicd.yml new file mode 100644 index 0000000..ddb3a0a --- /dev/null +++ b/.github/workflows/functionapp-cicd.yml @@ -0,0 +1,43 @@ + +# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions +# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples + +name: Common FunctionApp +on: push + +env: + AZURE_FUNCTIONAPP_NAME: bfyoc-functionapp-1 # set this to your application's name + AZURE_FUNCTIONAPP_PACKAGE_PATH: './dist' # set this to the path to your web app project, defaults to the repository root + NODE_VERSION: '12.x' # set this to the node version to use (supports 8.x, 10.x, 12.x) + +jobs: + build-and-deploy: + name: Build and Deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: 'Login to Azure' + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_FUNCTIONAPP_CREDENTIALS }} + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v1 + with: + node-version: ${{ env.NODE_VERSION }} + - name: install azure core tools + run: + npm i -g azure-functions-core-tools@3 --unsafe-perm true + - name: npm install, build, and test + run: | + npm install + npm run build:production + - uses: actions/upload-artifact@v2 + with: + name: ${{ env.AZURE_FUNCTIONAPP_NAME }} + path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/ + - name: deploy to azure functions + run: | + cp -r ./node_modules ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/ + cd ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/ + func azure functionapp publish ${{ env.AZURE_FUNCTIONAPP_NAME }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba985d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +tmp + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..dde673d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e214bf3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Attach by Azure Function Tools", + "processId": "${command:PickProcess}", + "request": "attach", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + } + + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e056e7c --- /dev/null +++ b/LICENSE @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/LICENSE-CODE b/LICENSE-CODE new file mode 100644 index 0000000..4a807d6 --- /dev/null +++ b/LICENSE-CODE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) KEVIN HILLINGER. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..787bba2 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# OpenHack - Serverless + +![function app](https://github.com/kevinhillinger/oh-serverless-coaching/workflows/function%20app/badge.svg) + +My solution to the serverless open hack that Microsoft runs. + +## Resources +[OpenHack - Serverless Events Calendar](https://openhack.microsoft.com/#events-calendar) \ No newline at end of file diff --git a/azure/deploy.sh b/azure/deploy.sh new file mode 100755 index 0000000..17c593c --- /dev/null +++ b/azure/deploy.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +echo "Deploying Resource Groups." +az deployment sub create --name "oh-deployment-1" --location "eastus2" \ + --template-file ./azure/templates/resourcegroups.json \ + --parameters ./azure/templates/resourcegroups.parameters.json \ + --output none +echo "Done." + +resource_groups=$(cat ./azure/templates/resourcegroups.parameters.json | jq '.parameters.resourceGroups.value') + +for rg in $(echo $resource_groups | jq -c -r '.[]'); do + suffix=$(echo $rg | jq '.suffix' -r) + resource_group=$(echo $rg | jq '.name' -r)-$suffix + location=$(echo $rg | jq '.location' -r) + + echo "Deploying resources to:" + echo " Resource Group: $resource_group" + echo " Location: $location" + + # function app + func_storage_name="bfyocfunc24store$suffix" + func_app=bfyoc-functionapp-$suffix + + az storage account create \ + --name $func_storage_name \ + --location $location \ + -g $resource_group \ + --sku Standard_LRS \ + --kind StorageV2 + + az functionapp create -g $resource_group \ + --consumption-plan-location $location \ + -n $func_app \ + -s $func_storage_name \ + --runtime node \ + --functions-version 3 \ + --runtime-version 12 \ + --os-type Linux + + az functionapp cors add -g $resource_group -n $func_app \ + --allowed-origins * + + # todo: get function path for getProducts and the function code, update logicapp.definition + + # logic app + echo "Creating logic app" + logic_app=bfyoc-logicapp-$suffix + + az logic workflow create \ + --definition ./azure/logicapp.definition.json \ + --location $location \ + --name $logic_app \ + --resource-group $resource_group + + # cosmos + echo "Creating cosmos db." + cosmos_account=bfyoc-cosmos-$suffix + cosmos_database=default + + az cosmosdb create \ + --resource-group $resource_group \ + --name $cosmos_account + --default-consistency-level Session + + az cosmosdb sql database create \ + -a $cosmos_account \ + -g $resource_group \ + -n $cosmos_database \ + --max-throughput $max_throughput + + # ratings container + partition_key='/productId' + max_throughput=4000 + container_name=ratings + + az cosmosdb sql container create \ + -a $cosmos_account \ + -g $resource_group \ + -d $cosmos_database \ + -n $container_name \ + -p $partition_key + + # set configuration connection string for function app + functionapp_cosmosdb_connection_setting_name=COSMOS_CONNECTION_STRING + cosmos_connection_string=$(az cosmosdb list-connection-strings --name $cosmos_account --resource-group $resource_group -o tsv --query 'connectionStrings[0].connectionString') + + az functionapp config appsettings set \ + --name $func_app \ + --resource-group $resource_group \ + --settings "$functionapp_cosmosdb_connection_setting_name=$cosmos_connection_string" + + # api management + apim_name=bfyoc-apim-$suffix + + az apim create --name $apim_name -g $resource_group \ + --location $location \ + --sku-name Consumption \ + --publisher-email bfyoc@nowhere.com \ + --publisher-name bfyoc + + # logic apps distributor notification + echo "Creating logic app" + logic_app_notify=bfyoc-logicapp-notify-$suffix + + az logic workflow create \ + --definition ./azure/logicapp.notify.definition.json \ + --location $location \ + --name $logic_app_notify \ + --resource-group $resource_group + + # batch processing - challenge 6 + + # storage + batch_storage_name=bfyocbatchstore$suffix + + az storage account create \ + --name $batch_storage_name \ + --location $location \ + -g $resource_group \ + --sku Standard_LRS \ + --kind StorageV2 + + # event grid + + # post to challenge endpoint to register + conn=$(az storage account show-connection-string --name $batch_storage_name --query connectionString -o tsv) + num=challenger-table-24 + + storage_registration_json=$( jq -n \ + --arg conn "$conn" \ + --arg num "$num" \ + '{storageAccountConnectionString: $conn, teamTableNumber: $num, blobContainerName: "orders"}' ) + + curl --header "Content-Type: application/json" \ + --request POST \ + --data $storage_registration_json \ + https://serverlessohmanagementapi.trafficmanager.net/api/team/registerStorageAccount + + # TODO: create an event hub namespace and a hub of 32 partitions, create a Send key, fetch the connection string then post it to the challenge endpoint + # TODO: register app setting with the function app + + # TODO: create azure service bus namespace and topic + # Add SB_CONNECTION_STRING to function app + + # TODO: create bfyoc-textanalytics-1 text analysis service instance, get key + + # challenge 10 + # create stream analaytics instance, bfyoc-streamanalytics-1 + + # create container in cosmos: productanalysis + + # TODO: script out all the configuration values needed to run the functionapp + # - use CLI to fetch values + # - keys are known from a config file + # - set then in Azure App Configuration + # - update to use Azure App Configuration client (@azure/app-configuration) + # - write a wrapper FunctionHandler that does this and sets process.env values based on this + # - update build script so that local debug environment fetches the values from azure app config, sets local.appsettings.json +done \ No newline at end of file diff --git a/azure/logicapp.alert.definition.json b/azure/logicapp.alert.definition.json new file mode 100644 index 0000000..58c1f24 --- /dev/null +++ b/azure/logicapp.alert.definition.json @@ -0,0 +1,197 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Create_HTML_table": { + "inputs": { + "columns": [ + { + "header": "ratingId", + "value": "@item()?['id']" + }, + { + "header": "sentimentScore", + "value": "@item()?['sentimentScore']" + } + ], + "format": "HTML", + "from": "@body('Query_documents_V2')?['Documents']" + }, + "runAfter": { + "Query_documents_V2": [ + "Succeeded" + ] + }, + "type": "Table" + }, + "Query_documents_V2": { + "inputs": { + "body": { + "QueryText": "SELECT * FROM c WHERE c.sentimentScore <= 0.30 AND DateTimeDiff('minute', c.timestamp, GetCurrentDateTime()) <= 5 ORDER BY c.timestamp DESC OFFSET 0 LIMIT 5" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['documentdb']['connectionId']" + } + }, + "method": "post", + "path": "/v2/dbs/@{encodeURIComponent('default')}/colls/@{encodeURIComponent('ratings')}/query" + }, + "runAfter": {}, + "type": "ApiConnection" + }, + "Send_an_email_(V2)": { + "inputs": { + "body": { + "Body": "

@{body('Create_HTML_table')}

", + "Subject": "Ratings with low sentiment (last 5 minutes)", + "To": "youremail.address@yourdomain.com" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['office365']['connectionId']" + } + }, + "method": "post", + "path": "/v2/Mail" + }, + "runAfter": { + "Create_HTML_table": [ + "Succeeded" + ] + }, + "type": "ApiConnection" + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "$connections": { + "defaultValue": {}, + "type": "Object" + } + }, + "triggers": { + "manual": { + "inputs": { + "schema": { + "properties": { + "data": { + "properties": { + "context": { + "properties": { + "activityLog": { + "properties": { + "authorization": { + "properties": { + "action": { + "type": "string" + }, + "scope": { + "type": "string" + } + }, + "type": "object" + }, + "caller": { + "type": "string" + }, + "channels": { + "type": "string" + }, + "claims": { + "type": "string" + }, + "correlationId": { + "type": "string" + }, + "description": { + "type": "string" + }, + "eventDataId": { + "type": "string" + }, + "eventSource": { + "type": "string" + }, + "eventTimestamp": { + "type": "string" + }, + "level": { + "type": "string" + }, + "operationId": { + "type": "string" + }, + "operationName": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + }, + "resourceId": { + "type": "string" + }, + "resourceProviderName": { + "type": "string" + }, + "resourceType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "subStatus": { + "type": "string" + }, + "submissionTimestamp": { + "type": "string" + }, + "subscriptionId": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "properties": { + "properties": {}, + "type": "object" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "schemaId": { + "type": "string" + } + }, + "type": "object" + } + }, + "kind": "Http", + "type": "Request" + } + } + }, + "parameters": { + "$connections": { + "value": { + "documentdb": { + "connectionId": "/subscriptions//resourceGroups/oh-challenger-24-1/providers/Microsoft.Web/connections/documentdb", + "connectionName": "documentdb", + "id": "/subscriptions//providers/Microsoft.Web/locations/eastus2/managedApis/documentdb" + }, + "office365": { + "connectionId": "/subscriptions//resourceGroups/oh-challenger-24-1/providers/Microsoft.Web/connections/office365", + "connectionName": "office365", + "id": "/subscriptions//providers/Microsoft.Web/locations/eastus2/managedApis/office365" + } + } + } + } +} \ No newline at end of file diff --git a/azure/logicapp.definition.json b/azure/logicapp.definition.json new file mode 100644 index 0000000..58063b1 --- /dev/null +++ b/azure/logicapp.definition.json @@ -0,0 +1,58 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "HTTP": { + "inputs": { + "method": "GET", + "queries": { + "code": "eej/kC92V46AAwMIVtgZpIkD7YQ66Uuj08N0GOZ2KQIDaFEXbG/3zA==", + "productId": "@triggerBody()?['productId']" + }, + "uri": "https://bfyoc-functions-24.azurewebsites.net/api/products" + }, + "runAfter": {}, + "type": "Http" + }, + "Response": { + "inputs": { + "body": "@{body('HTTP')} and the description is This starfruit ice cream is out of this world!", + "statusCode": 200 + }, + "kind": "Http", + "runAfter": { + "HTTP": [ + "Succeeded" + ] + }, + "type": "Response" + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "productId": { + "defaultValue": "null", + "type": "String" + } + }, + "triggers": { + "manual": { + "inputs": { + "method": "POST", + "schema": { + "properties": { + "productId": { + "type": "string" + } + }, + "type": "object" + } + }, + "kind": "Http", + "type": "Request" + } + } + }, + "parameters": {} +} \ No newline at end of file diff --git a/azure/logicapp.notify.definition.json b/azure/logicapp.notify.definition.json new file mode 100644 index 0000000..9b379d1 --- /dev/null +++ b/azure/logicapp.notify.definition.json @@ -0,0 +1,252 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Add_Internal_User_Email_Address": { + "inputs": { + "name": "emailAddresses", + "value": "youremail.address@yourdomain.com" + }, + "runAfter": { + "For_each": [ + "Succeeded" + ] + }, + "type": "AppendToArrayVariable" + }, + "Build_HTML_Rows": { + "actions": { + "Append_to_array_variable": { + "inputs": { + "name": "htmlProductRows", + "value": "\n @{items('Build_HTML_Rows')['productName']}\n @{items('Build_HTML_Rows')['productDescription']}\n @{items('Build_HTML_Rows')['productId']}\n" + }, + "runAfter": {}, + "type": "AppendToArrayVariable" + } + }, + "foreach": "@variables('products')", + "runAfter": { + "Initialize_variable": [ + "Succeeded" + ] + }, + "type": "Foreach" + }, + "Create_HTML_Body": { + "inputs": { + "variables": [ + { + "name": "htmlTemplate", + "type": "string", + "value": "@{replace(body('Get_HTML_Template'), '', join(variables('htmlProductRows'), '\n'))}" + } + ] + }, + "runAfter": { + "Get_HTML_Template": [ + "Succeeded" + ] + }, + "type": "InitializeVariable" + }, + "For_each": { + "actions": { + "Append_to_array_variable_2": { + "inputs": { + "name": "emailAddresses", + "value": "@items('For_each')?['emailaddress1']" + }, + "runAfter": {}, + "type": "AppendToArrayVariable" + } + }, + "foreach": "@body('List_records')?['value']", + "runAfter": { + "List_records": [ + "Succeeded" + ] + }, + "type": "Foreach" + }, + "Foreach_Email_Address": { + "actions": { + "Send_an_email_(V2)": { + "inputs": { + "body": { + "Body": "

@{variables('htmlTemplate')}

", + "Subject": "BFYOC 24", + "To": "@{items('Foreach_Email_Address')}" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['office365']['connectionId']" + } + }, + "method": "post", + "path": "/v2/Mail" + }, + "runAfter": {}, + "type": "ApiConnection" + } + }, + "foreach": "@variables('emailAddresses')", + "runAfter": { + "Add_Internal_User_Email_Address": [ + "Succeeded" + ], + "Create_HTML_Body": [ + "Succeeded" + ] + }, + "type": "Foreach" + }, + "Get_HTML_Template": { + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "get", + "path": "/datasets/default/files/@{encodeURIComponent(encodeURIComponent('JTJmZW1haWwtdGVtcGxhdGVzJTJmcHJvZHVjdC5ub3RpZnkudG1wbC5odG1s'))}/content", + "queries": { + "inferContentType": true + } + }, + "metadata": { + "JTJmZW1haWwtdGVtcGxhdGVzJTJmcHJvZHVjdC5ub3RpZnkudG1wbC5odG1s": "/email-templates/product.notify.tmpl.html" + }, + "runAfter": { + "Build_HTML_Rows": [ + "Succeeded" + ] + }, + "type": "ApiConnection" + }, + "HTTP": { + "inputs": { + "headers": { + "Ocp-Apim-Subscription-Key": "@{triggerOutputs()['headers']?['Ocp-Apim-Subscription-Key']}" + }, + "method": "GET", + "uri": "https://bfyoc-apim-1.azure-api.net/api/product/" + }, + "runAfter": {}, + "type": "Http" + }, + "Initialize_Email_Addresses": { + "inputs": { + "variables": [ + { + "name": "emailAddresses", + "type": "array" + } + ] + }, + "runAfter": {}, + "type": "InitializeVariable" + }, + "Initialize_Products": { + "inputs": { + "variables": [ + { + "name": "products", + "type": "array", + "value": "@body('HTTP')" + } + ] + }, + "runAfter": { + "HTTP": [ + "Succeeded" + ] + }, + "type": "InitializeVariable" + }, + "Initialize_variable": { + "inputs": { + "variables": [ + { + "name": "htmlProductRows", + "type": "array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_Products": [ + "Succeeded" + ] + }, + "type": "InitializeVariable" + }, + "List_records": { + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['commondataservice']['connectionId']" + } + }, + "method": "get", + "path": "/v2/datasets/@{encodeURIComponent(encodeURIComponent('org01c23854.crm'))}/tables/@{encodeURIComponent(encodeURIComponent('contacts'))}/items" + }, + "runAfter": { + "Initialize_Email_Addresses": [ + "Succeeded" + ] + }, + "type": "ApiConnection" + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "$connections": { + "defaultValue": {}, + "type": "Object" + } + }, + "triggers": { + "manual": { + "conditions": [], + "inputs": { + "method": "POST", + "relativePath": "/api/product", + "schema": { + "properties": { + "notify": { + "type": "string" + } + }, + "type": "object" + } + }, + "kind": "Http", + "operationOptions": "EnableSchemaValidation", + "type": "Request" + } + } + }, + "parameters": { + "$connections": { + "value": { + "azureblob": { + "connectionId": "/subscriptions//resourceGroups/oh-challenger-24-1/providers/Microsoft.Web/connections/azureblob", + "connectionName": "azureblob", + "id": "/subscriptions//providers/Microsoft.Web/locations/eastus2/managedApis/azureblob" + }, + "commondataservice": { + "connectionId": "/subscriptions//resourceGroups/oh-challenger-24-1/providers/Microsoft.Web/connections/commondataservice", + "connectionName": "commondataservice", + "id": "/subscriptions//providers/Microsoft.Web/locations/eastus2/managedApis/commondataservice" + }, + "office365": { + "connectionId": "/subscriptions//resourceGroups/oh-challenger-24-1/providers/Microsoft.Web/connections/office365", + "connectionName": "office365", + "id": "/subscriptions//providers/Microsoft.Web/locations/eastus2/managedApis/office365" + } + } + } + } +} \ No newline at end of file diff --git a/azure/ratings-api.createRating.policy.xml b/azure/ratings-api.createRating.policy.xml new file mode 100644 index 0000000..6d62d00 --- /dev/null +++ b/azure/ratings-api.createRating.policy.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/azure/streamanalytics.sql b/azure/streamanalytics.sql new file mode 100644 index 0000000..8e3ca9e --- /dev/null +++ b/azure/streamanalytics.sql @@ -0,0 +1,47 @@ +SELECT + productId, + productName, + ROUND(AVG(sentimentScore), 2) as [averageSentimentScore], + COUNT(*) AS [count] +INTO + [powerbi-sentiment] +FROM + [productsentiment] +GROUP BY + productId, + productName, + TumblingWindow(minute, 5) + +SELECT + productId, + productName, + source, + ROUND(SUM(purchaseTotal), 2) as [totalSales], + COUNT(*) AS [count] +INTO + [powerbi-distributor] +FROM + [productpurchases] +WHERE source = 'distributor' +GROUP BY + productId, + productName, + source, + TumblingWindow(minute, 5) + +SELECT + productId, + productName, + source, + ROUND(SUM(purchaseTotal), 2) as [totalSales], + COUNT(*) AS [count] +INTO + [powerbi-pos] +FROM + [productpurchases] +WHERE source = 'pos' +GROUP BY + productId, + productName, + source, + TumblingWindow(minute, 5) \ No newline at end of file diff --git a/azure/telemetry/function.telemetry.kql b/azure/telemetry/function.telemetry.kql new file mode 100644 index 0000000..ee62b40 --- /dev/null +++ b/azure/telemetry/function.telemetry.kql @@ -0,0 +1,6 @@ +requests +| where url contains "https://bfyoc-functionapp-1.azurewebsites.net" +| where timestamp > ago(1h) +| summarize RequestTime = avg(duration), TotalRequests = count() by name +| sort by TotalRequests +| project FunctionName = name, RequestTime, TotalRequests diff --git a/azure/telemetry/lowscore.telemtry.kql b/azure/telemetry/lowscore.telemtry.kql new file mode 100644 index 0000000..e02c7f4 --- /dev/null +++ b/azure/telemetry/lowscore.telemtry.kql @@ -0,0 +1,10 @@ +let traceId = 'rating score| '; +traces +| where operation_Name == 'createRating' and message contains traceId +| where timestamp > ago(5m) +| extend sentiment = parse_json(substring(message, strlen(traceId))) +| extend sentimentScore = todouble(sentiment.score) +| extend ratingId = sentiment.ratingId +| where todouble(sentiment.score) <= 0.3 +| summarize AggregatedValue = count() by bin(timestamp, 5m) +| order by timestamp desc \ No newline at end of file diff --git a/azure/templates/resourcegroups.json b/azure/templates/resourcegroups.json new file mode 100644 index 0000000..7e5c1ca --- /dev/null +++ b/azure/templates/resourcegroups.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroups": { + "type": "array" + } + }, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('resourceGroups')[copyIndex()].name, '-', parameters('resourceGroups')[copyIndex()].suffix)]", + "location": "[parameters('resourceGroups')[copyIndex()].location]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "tags": {}, + "properties": { + }, + "copy": { + "name": "webappcopy", + "count": "[length(parameters('resourceGroups'))]" + } + } + ], + "outputs": {} +} \ No newline at end of file diff --git a/azure/templates/resourcegroups.parameters.json b/azure/templates/resourcegroups.parameters.json new file mode 100644 index 0000000..dbfab0f --- /dev/null +++ b/azure/templates/resourcegroups.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroups": { + "value": [ + { + "suffix": "1", + "name": "oh-challenger-24", + "location": "eastus2" + } + ] + } + } +} \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..703d153 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,10 @@ + + +## Notes + +### Architecture + +The question I asked myself during this openhack was: + +- Given autonomous microservices, how do you deploy to the same FunctionApp? +- What is the cost (operationally, etc.) deploying to separate functions instances? diff --git a/img/alert-email.png b/img/alert-email.png new file mode 100644 index 0000000..1b2cd42 Binary files /dev/null and b/img/alert-email.png differ diff --git a/img/insights.png b/img/insights.png new file mode 100644 index 0000000..7c000f9 Binary files /dev/null and b/img/insights.png differ diff --git a/img/product-notification.png b/img/product-notification.png new file mode 100644 index 0000000..1477c27 Binary files /dev/null and b/img/product-notification.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5b935c2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1741 @@ +{ + "name": "bfyoc-functions", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@azure/abort-controller": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.1.tgz", + "integrity": "sha512-wP2Jw6uPp8DEDy0n4KNidvwzDjyVV2xnycEIq7nPzj1rHyb/r+t3OPeNT1INZePP2wy5ZqlwyuyOMTi0ePyY1A==", + "requires": { + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "@azure/amqp-common": { + "version": "1.0.0-preview.15", + "resolved": "https://registry.npmjs.org/@azure/amqp-common/-/amqp-common-1.0.0-preview.15.tgz", + "integrity": "sha512-EoxNsVR7yLioNKRz5JBwQAE9pEdPVGCmmQbPKkZHP72vE5NhaLnOwHOCrk/311cuhJ8aQ60eiLUtF9J2XrEZyA==", + "requires": { + "@types/async-lock": "^1.1.0", + "@types/is-buffer": "^2.0.0", + "async-lock": "^1.1.3", + "buffer": "^5.2.1", + "debug": "^3.1.0", + "events": "^3.0.0", + "is-buffer": "^2.0.3", + "jssha": "^2.3.1", + "process": "^0.11.10", + "rhea": "^1.0.18", + "rhea-promise": "^0.1.15", + "stream-browserify": "^2.0.2", + "tslib": "^1.9.3", + "url": "^0.11.0", + "util": "^0.11.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "@azure/core-amqp": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-1.1.5.tgz", + "integrity": "sha512-8l7xMoyH0/emc1Y1p9I6jVggIBGVgTeKR2KUHWZAlXpBhagglabb6pZLQtCaCATasd8dSoCqwOOxfe/DVSE+kQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.1.3", + "@azure/logger": "^1.0.0", + "@types/async-lock": "^1.1.0", + "@types/is-buffer": "^2.0.0", + "async-lock": "^1.1.3", + "buffer": "^5.2.1", + "events": "^3.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "process": "^0.11.10", + "rhea": "^1.0.21", + "rhea-promise": "^1.0.0", + "stream-browserify": "^3.0.0", + "tslib": "^2.0.0", + "url": "^0.11.0", + "util": "^0.12.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "jssha": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.1.2.tgz", + "integrity": "sha512-6fEObA9he4vcCpz+dt9b5DjqhqvSsz9XMfNPU6/IyKHDQpCHsYayPRkWmAZG61lZC9XVJcjsQNAiUUd0NpskeQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "rhea-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-1.0.0.tgz", + "integrity": "sha512-odAjpbB/IpFFBenPDwPkTWMQldt+DUlMBH9yI48Ct5OgTeDuuQcBnlhB+YCc6g2z8+URiP2ejms88joEanNCaw==", + "requires": { + "debug": "^3.1.0", + "rhea": "^1.0.8", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "util": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.3.tgz", + "integrity": "sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + } + } + }, + "@azure/core-asynciterator-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz", + "integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==" + }, + "@azure/core-auth": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.3.tgz", + "integrity": "sha512-A4xigW0YZZpkj1zK7dKuzbBpGwnhEcRk6WWuIshdHC32raR3EQ1j6VA9XZqE+RFsUgH6OAmIK5BWIz+mZjnd6Q==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-tracing": "1.0.0-preview.8", + "@opentelemetry/api": "^0.6.1", + "tslib": "^2.0.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.8", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.8.tgz", + "integrity": "sha512-ZKUpCd7Dlyfn7bdc+/zC/sf0aRIaNQMDuSj2RhYRFe3p70hVAnYGp3TX4cnG2yoEALp/LTj/XnZGQ8Xzf6Ja/Q==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "^0.6.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "@opentelemetry/api": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.6.1.tgz", + "integrity": "sha512-wpufGZa7tTxw7eAsjXJtiyIQ42IWQdX9iUQp7ACJcKo1hCtuhLU+K2Nv1U6oRwT1oAlZTE6m4CgWKZBhOiau3Q==", + "requires": { + "@opentelemetry/context-base": "^0.6.1" + } + } + } + }, + "@azure/core-http": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.1.6.tgz", + "integrity": "sha512-/C+qNzhwlLKt0F6SjaBEyY2pwZvwL2LviyS5PHlCh77qWuTF1sETmYAINM88BCN+kke+UlECK4YOQaAjJwyHvQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.1.3", + "@azure/core-tracing": "1.0.0-preview.9", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^0.10.2", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.1", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.0.0", + "tunnel": "^0.0.6", + "uuid": "^8.1.0", + "xml2js": "^0.4.19" + } + }, + "@azure/core-tracing": { + "version": "1.0.0-preview.9", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", + "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "^0.10.2", + "tslib": "^2.0.0" + } + }, + "@azure/cosmos": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.8.1.tgz", + "integrity": "sha512-nnydidKZBNaQvqkN0k4EntjEEglGWnfNi/4Cbcj+Ypvvd3H0hNbx0bwU1cBNBWQEzHiWzU3k44BgNrhayCpiSw==", + "requires": { + "@types/debug": "^4.1.4", + "debug": "^4.1.1", + "fast-json-stable-stringify": "^2.0.0", + "jsbi": "^3.1.3", + "node-abort-controller": "^1.0.4", + "node-fetch": "^2.6.0", + "os-name": "^3.1.0", + "priorityqueuejs": "^1.0.0", + "semaphore": "^1.0.5", + "tslib": "^2.0.0", + "uuid": "^8.1.0" + } + }, + "@azure/event-hubs": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/event-hubs/-/event-hubs-5.2.2.tgz", + "integrity": "sha512-F/1jaTC9NxgNjMkO7SAs9Q9BndJ16AtRwQu0l21FNyRCN8kWl4Noiblsbsjtv+BPYa+ARrocR5POMlJ5eveR9w==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^1.1.4", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-tracing": "1.0.0-preview.8", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^0.6.1", + "buffer": "^5.2.1", + "process": "^0.11.10", + "rhea-promise": "^1.0.0", + "tslib": "^2.0.0", + "uuid": "^8.1.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.8", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.8.tgz", + "integrity": "sha512-ZKUpCd7Dlyfn7bdc+/zC/sf0aRIaNQMDuSj2RhYRFe3p70hVAnYGp3TX4cnG2yoEALp/LTj/XnZGQ8Xzf6Ja/Q==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "^0.6.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "@opentelemetry/api": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.6.1.tgz", + "integrity": "sha512-wpufGZa7tTxw7eAsjXJtiyIQ42IWQdX9iUQp7ACJcKo1hCtuhLU+K2Nv1U6oRwT1oAlZTE6m4CgWKZBhOiau3Q==", + "requires": { + "@opentelemetry/context-base": "^0.6.1" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "rhea-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-1.0.0.tgz", + "integrity": "sha512-odAjpbB/IpFFBenPDwPkTWMQldt+DUlMBH9yI48Ct5OgTeDuuQcBnlhB+YCc6g2z8+URiP2ejms88joEanNCaw==", + "requires": { + "debug": "^3.1.0", + "rhea": "^1.0.8", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + } + } + }, + "@azure/functions": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-1.2.2.tgz", + "integrity": "sha512-p/dDHq1sG/iAib+eDY4NxskWHoHW1WFzD85s0SfWxc2wVjJbxB0xz/zBF4s7ymjVgTu+0ceipeBk+tmpnt98oA==" + }, + "@azure/logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz", + "integrity": "sha512-g2qLDgvmhyIxR3JVS8N67CyIOeFRKQlX/llxYJQr1OSGQqM3HTpVP8MjmjcEKbL/OIt2N9C9UFaNQuKOw1laOA==", + "requires": { + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "@azure/service-bus": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-1.1.8.tgz", + "integrity": "sha512-+QSFGydKC0VKqfRZdK4+s8lgB1bCm4p3cPKJREDTOTNWHgbdkoRr8flbl2n/+KqrCGZFS5SfrdTQhl4qU+iByg==", + "requires": { + "@azure/amqp-common": "1.0.0-preview.15", + "@azure/core-http": "^1.0.0", + "@opentelemetry/types": "^0.2.0", + "@types/is-buffer": "^2.0.0", + "@types/long": "^4.0.0", + "buffer": "^5.2.1", + "debug": "^4.1.1", + "is-buffer": "^2.0.3", + "long": "^4.0.0", + "process": "^0.11.10", + "rhea": "^1.0.23", + "rhea-promise": "^0.1.15", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "@opencensus/web-types": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", + "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" + }, + "@opentelemetry/api": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", + "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", + "requires": { + "@opentelemetry/context-base": "^0.10.2" + }, + "dependencies": { + "@opentelemetry/context-base": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", + "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" + } + } + }, + "@opentelemetry/context-base": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.6.1.tgz", + "integrity": "sha512-5bHhlTBBq82ti3qPT15TRxkYTFPPQWbnkkQkmHPtqiS1XcTB69cEKd3Jm7Cfi/vkPoyxapmePE9tyA7EzLt8SQ==" + }, + "@opentelemetry/types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/types/-/types-0.2.0.tgz", + "integrity": "sha512-GtwNB6BNDdsIPAYEdpp3JnOGO/3AJxjPvny53s3HERBdXSJTGQw8IRhiaTEX0b3w9P8+FwFZde4k+qkjn67aVw==" + }, + "@types/async-lock": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.2.tgz", + "integrity": "sha512-j9n4bb6RhgFIydBe0+kpjnBPYumDaDyU8zvbWykyVMkku+c2CSu31MZkLeaBfqIwU+XCxlDpYDfyMQRkM0AkeQ==" + }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, + "@types/is-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.0.tgz", + "integrity": "sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw==", + "requires": { + "@types/node": "*" + } + }, + "@types/lodash": { + "version": "4.14.159", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.159.tgz", + "integrity": "sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg==" + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/node": { + "version": "14.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", + "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==" + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/tunnel": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", + "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", + "requires": { + "@types/node": "*" + } + }, + "@types/uuid": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.9.tgz", + "integrity": "sha512-XDwyIlt/47l2kWLTzw/mtrpLdB+GPSskR2n/PIcPn+VYhVO77rGhRncIR5GPU0KRzXuqkDO+J5qqrG0Y8P6jzQ==" + }, + "@types/validator": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-9.4.4.tgz", + "integrity": "sha512-7bWNKQ3lDMhRS2lxe1aHGTBijZ/a6wQfZmCtKJDefpb81sYd+FrfNqj6Gda1Tcw8bYK0gG1CVuNLWV2JS7K8Dw==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=" + }, + "async-lock": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.2.4.tgz", + "integrity": "sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "available-typed-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", + "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", + "requires": { + "array-filter": "^1.0.0" + } + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "durable-functions": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/durable-functions/-/durable-functions-1.4.3.tgz", + "integrity": "sha512-MJMnqpHmAuZt+IK6uTij7DE+yzMa+++YPhdb1FOIwm+AeBI/R4CYZnnsmOo6DnrmOIfnF0XBewL0nEhx23se8g==", + "requires": { + "@azure/functions": "^1.0.2-beta2", + "@types/lodash": "^4.14.119", + "@types/uuid": "~3.4.4", + "@types/validator": "^9.4.3", + "axios": "^0.19.0", + "commander": "~2.9.0", + "debug": "~2.6.9", + "lodash": "^4.17.15", + "rimraf": "~2.5.4", + "typedoc": "^0.17.1", + "uuid": "~3.3.2", + "validator": "~10.8.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==" + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "highlight.js": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.1.2.tgz", + "integrity": "sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA==" + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==" + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typed-array": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", + "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "requires": { + "available-typed-arrays": "^1.0.0", + "es-abstract": "^1.17.4", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "jsbi": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz", + "integrity": "sha512-nBJqA0C6Qns+ZxurbEoIR56wyjiUszpNy70FHvxO5ervMoCbZVE3z3kxr5nKGhlxr/9MhKTSUBs7cAwwuf3g9w==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jssha": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-2.4.2.tgz", + "integrity": "sha512-/jsi/9C0S70zfkT/4UlKQa5E1xKurDnXcQizcww9JSR/Fv+uIbWM2btG+bFcL3iNoK9jIGS0ls9HWLr1iw0kFg==" + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "lunr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", + "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==" + }, + "macos-release": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", + "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "marked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.0.0.tgz", + "integrity": "sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng==" + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "module-alias": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g=" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.1.0.tgz", + "integrity": "sha512-dEYmUqjtbivotqjraOe8UvhT/poFfog1BQRNsZm/MSEDDESk2cQ1tvD8kGyuN07TM/zoW+n42odL8zTeJupYdQ==" + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "requires": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "priorityqueuejs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", + "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg=" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "rhea": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/rhea/-/rhea-1.0.24.tgz", + "integrity": "sha512-PEl62U2EhxCO5wMUZ2/bCBcXAVKN9AdMSNQOrp3+R5b77TEaOSiy16MQ0sIOmzj/iqsgIAgPs1mt3FYfu1vIXA==", + "requires": { + "debug": "0.8.0 - 3.5.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "rhea-promise": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-0.1.15.tgz", + "integrity": "sha512-+6uilZXSJGyiqVeHQI3Krv6NTAd8cWRCY2uyCxmzR4/5IFtBqqFem1HV2OiwSj0Gu7OFChIJDfH2JyjN7J0vRA==", + "requires": { + "debug": "^3.1.0", + "rhea": "^1.0.4", + "tslib": "^1.9.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, + "rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "string.prototype.padend": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz", + "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tls": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tls/-/tls-0.0.1.tgz", + "integrity": "sha1-CrK/WWjXHfL4wOFRXSSiJAuYqsg=" + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, + "typed-rest-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.7.3.tgz", + "integrity": "sha512-CwTpx/TkRHGZoHkJhBcp4X8K3/WtlzSHVQR0OIFnt10j4tgy4ypgq/SrrgVpA1s6tAL49Q6J3R5C0Cgfh2ddqA==", + "requires": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "1.8.3" + } + }, + "typedoc": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.17.8.tgz", + "integrity": "sha512-/OyrHCJ8jtzu+QZ+771YaxQ9s4g5Z3XsQE3Ma7q+BL392xxBn4UMvvCdVnqKC2T/dz03/VXSLVKOP3lHmDdc/w==", + "requires": { + "fs-extra": "^8.1.0", + "handlebars": "^4.7.6", + "highlight.js": "^10.0.0", + "lodash": "^4.17.15", + "lunr": "^2.3.8", + "marked": "1.0.0", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shelljs": "^0.8.4", + "typedoc-default-themes": "^0.10.2" + } + }, + "typedoc-default-themes": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.2.tgz", + "integrity": "sha512-zo09yRj+xwLFE3hyhJeVHWRSPuKEIAsFK5r2u47KL/HBKqpwdUSanoaz5L34IKiSATFrjG5ywmIu98hPVMfxZg==", + "requires": { + "lunr": "^2.3.8" + } + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, + "uglify-js": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.1.tgz", + "integrity": "sha512-RjxApKkrPJB6kjJxQS3iZlf///REXWYxYJxO/MpmlQzVkDWVI3PSnCBWezMecmTU/TRkNxrl8bmsfFQCp+LO+Q==", + "optional": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validator": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.8.0.tgz", + "integrity": "sha512-mXqMxfCh5NLsVgYVKl9WvnHNDPCcbNppHSPPowu0VjtSsGWVY+z8hJF44edLR1nbLNzi3jYoYsIl8KZpioIk6g==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-typed-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", + "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "requires": { + "available-typed-arrays": "^1.0.2", + "es-abstract": "^1.17.5", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } + }, + "windows-release": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "requires": { + "execa": "^1.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e09fd3 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "bfyoc-functions", + "version": "1.0.0", + "_moduleAliases": { + "@lib": "lib" + }, + "scripts": { + "build": "bash ./scripts/build.sh", + "build:production": "npm run prestart && npm prune --production", + "deploy": "bash ./scripts/deploy.sh", + "watch": "tsc --w", + "prestart": "npm run build && func extensions install", + "start:host": "cd src/functions && func start", + "start": "npm-run-all --parallel start:host watch", + "test": "echo \"No tests yet...\"" + }, + "description": "", + "devDependencies": { + "@azure/functions": "^1.0.1-beta1", + "@types/node": "^14.0.27", + "npm-run-all": "^4.1.5", + "typescript": "^3.9.7" + }, + "dependencies": { + "@azure/cosmos": "^3.8.1", + "@azure/event-hubs": "^5.2.2", + "@azure/service-bus": "^1.1.8", + "durable-functions": "^1.4.3", + "fs": "0.0.1-security", + "guid-typescript": "^1.0.9", + "module-alias": "^2.2.2", + "net": "^1.0.2", + "tls": "0.0.1", + "ts-node": "^8.10.2", + "typed-rest-client": "^1.7.3" + } +} diff --git a/samples/combinOrders.request.json b/samples/combinOrders.request.json new file mode 100644 index 0000000..dc2fa74 --- /dev/null +++ b/samples/combinOrders.request.json @@ -0,0 +1,5 @@ +{ + "orderHeaderDetailsCSVUrl": "https://bfyocbatchstore1.blob.core.windows.net/orders/20200817214000-OrderHeaderDetails.csv", + "orderLineItemsCSVUrl": "https://bfyocbatchstore1.blob.core.windows.net/orders/20200817214000-OrderLineItems.csv", + "productInformationCSVUrl": "https://bfyocbatchstore1.blob.core.windows.net/orders/20200817214000-ProductInformation.csv" + } \ No newline at end of file diff --git a/samples/combineOrders.response.json b/samples/combineOrders.response.json new file mode 100644 index 0000000..7c52615 --- /dev/null +++ b/samples/combineOrders.response.json @@ -0,0 +1,199 @@ +[ + { + "headers": { + "salesNumber": "JKS555", + "dateTime": "8/9/2020 9:29:37 PM", + "locationId": "CCC333", + "locationName": "VanArsdel Ltd.", + "locationAddress": "789 FE Road", + "locationPostcode": "98052", + "totalCost": "197.05", + "totalTax": "19.705" + }, + "details": [ + { + "productId": "288fd748-ad2b-4417-83b9-7aa5be9cff22", + "quantity": "10", + "unitCost": "5.99", + "totalCost": "59.9", + "totalTax": "5.99", + "productName": "Tropical Mango", + "productDescription": "You know what they say... It takes two. You. And this ice cream." + }, + { + "productId": "75542e38-563f-436f-adeb-f426f1dabb5c", + "quantity": "10", + "unitCost": "3.99", + "totalCost": "39.9", + "totalTax": "3.99", + "productName": "Starfruit Explosion", + "productDescription": "This starfruit ice cream is out of this world!" + }, + { + "productId": "4c25613a-a3c2-4ef3-8e02-9c335eb23204", + "quantity": "5", + "unitCost": "3.49", + "totalCost": "17.45", + "totalTax": "1.745", + "productName": "Truly Orange-inal", + "productDescription": "Made from concentrate." + }, + { + "productId": "75542e38-563f-436f-adeb-f426f1dabb5c", + "quantity": "20", + "unitCost": "3.99", + "totalCost": "79.8", + "totalTax": "7.98", + "productName": "Starfruit Explosion", + "productDescription": "This starfruit ice cream is out of this world!" + } + ] + }, + { + "headers": { + "salesNumber": "PPP320", + "dateTime": "7/22/2020 3:33:48 AM", + "locationId": "AAA111", + "locationName": "Contoso Suites", + "locationAddress": "123 Wholesale Road", + "locationPostcode": "98112", + "totalCost": "17.45", + "totalTax": "1.745" + }, + "details": [ + { + "productId": "4c25613a-a3c2-4ef3-8e02-9c335eb23204", + "quantity": "5", + "unitCost": "3.49", + "totalCost": "17.45", + "totalTax": "1.745", + "productName": "Truly Orange-inal", + "productDescription": "Made from concentrate." + } + ] + }, + { + "headers": { + "salesNumber": "GFK116", + "dateTime": "7/21/2020 3:32:26 AM", + "locationId": "CCC333", + "locationName": "VanArsdel Ltd.", + "locationAddress": "789 FE Road", + "locationPostcode": "98052", + "totalCost": "59.8", + "totalTax": "5.98" + }, + "details": [ + { + "productId": "80bab959-ef8b-4ae3-8bf2-e876d77277b6", + "quantity": "20", + "unitCost": "2.99", + "totalCost": "59.8", + "totalTax": "5.98", + "productName": "French Vanilla", + "productDescription": "It's vanilla ice cream." + } + ] + }, + { + "headers": { + "salesNumber": "DCV238", + "dateTime": "8/10/2020 7:23:02 AM", + "locationId": "AAA111", + "locationName": "Contoso Suites", + "locationAddress": "123 Wholesale Road", + "locationPostcode": "98112", + "totalCost": "239.85", + "totalTax": "23.985" + }, + "details": [ + { + "productId": "0f5a0fe8-4506-4332-969e-699a693334a8", + "quantity": "15", + "unitCost": "15.99", + "totalCost": "239.85", + "totalTax": "23.985", + "productName": "Beer", + "productDescription": "Hey this isn't ice cream!" + } + ] + }, + { + "headers": { + "salesNumber": "QVD251", + "dateTime": "8/13/2020 10:48:39 AM", + "locationId": "BBB222", + "locationName": "Northwind Traders", + "locationAddress": "456 Foodcenter Lane", + "locationPostcode": "98101", + "totalCost": "79.95", + "totalTax": "7.995" + }, + "details": [ + { + "productId": "0f5a0fe8-4506-4332-969e-699a693334a8", + "quantity": "5", + "unitCost": "15.99", + "totalCost": "79.95", + "totalTax": "7.995", + "productName": "Beer", + "productDescription": "Hey this isn't ice cream!" + } + ] + }, + { + "headers": { + "salesNumber": "SRA430", + "dateTime": "7/26/2020 9:57:42 PM", + "locationId": "BBB222", + "locationName": "Northwind Traders", + "locationAddress": "456 Foodcenter Lane", + "locationPostcode": "98101", + "totalCost": "319.8", + "totalTax": "31.98" + }, + "details": [ + { + "productId": "0f5a0fe8-4506-4332-969e-699a693334a8", + "quantity": "20", + "unitCost": "15.99", + "totalCost": "319.8", + "totalTax": "31.98", + "productName": "Beer", + "productDescription": "Hey this isn't ice cream!" + } + ] + }, + { + "headers": { + "salesNumber": "RNO494", + "dateTime": "7/18/2020 1:48:53 PM", + "locationId": "AAA111", + "locationName": "Contoso Suites", + "locationAddress": "123 Wholesale Road", + "locationPostcode": "98112", + "totalCost": "84.85", + "totalTax": "8.485" + }, + "details": [ + { + "productId": "65ab124a-9b2c-4294-a52d-18839364ef15", + "quantity": "5", + "unitCost": "8.99", + "totalCost": "44.95", + "totalTax": "4.495", + "productName": "Durian Durian", + "productDescription": "Smells suspect but tastes... also suspect." + }, + { + "productId": "75542e38-563f-436f-adeb-f426f1dabb5c", + "quantity": "10", + "unitCost": "3.99", + "totalCost": "39.9", + "totalTax": "3.99", + "productName": "Starfruit Explosion", + "productDescription": "This starfruit ice cream is out of this world!" + } + ] + } +] \ No newline at end of file diff --git a/samples/combineOrders.txt b/samples/combineOrders.txt new file mode 100644 index 0000000..24e632a --- /dev/null +++ b/samples/combineOrders.txt @@ -0,0 +1,9 @@ + +POST +https://serverlessohmanagementapi.trafficmanager.net/api/definition/order/combineOrderContent + +{ + "orderHeaderDetailsCSVUrl": "https://bfyocbatchstore1.blob.core.windows.net/orders/20200817214000-OrderHeaderDetails.csv", + "orderLineItemsCSVUrl": "https://bfyocbatchstore1.blob.core.windows.net/orders/20200817214000-OrderLineItems.csv", + "productInformationCSVUrl": "https://bfyocbatchstore1.blob.core.windows.net/orders/20200817214000-ProductInformation.csv" +} \ No newline at end of file diff --git a/samples/distributorFiles/20200817214000-OrderHeaderDetails.csv b/samples/distributorFiles/20200817214000-OrderHeaderDetails.csv new file mode 100644 index 0000000..0e24395 --- /dev/null +++ b/samples/distributorFiles/20200817214000-OrderHeaderDetails.csv @@ -0,0 +1,8 @@ +ponumber,datetime,locationid,locationname,locationaddress,locationpostcode,totalcost,totaltax +JKS555,8/9/2020 9:29:37 PM,CCC333,VanArsdel Ltd.,789 FE Road,98052,197.05,19.705 +PPP320,7/22/2020 3:33:48 AM,AAA111,Contoso Suites,123 Wholesale Road,98112,17.45,1.745 +GFK116,7/21/2020 3:32:26 AM,CCC333,VanArsdel Ltd.,789 FE Road,98052,59.8,5.98 +DCV238,8/10/2020 7:23:02 AM,AAA111,Contoso Suites,123 Wholesale Road,98112,239.85,23.985 +QVD251,8/13/2020 10:48:39 AM,BBB222,Northwind Traders,456 Foodcenter Lane,98101,79.95,7.995 +SRA430,7/26/2020 9:57:42 PM,BBB222,Northwind Traders,456 Foodcenter Lane,98101,319.8,31.98 +RNO494,7/18/2020 1:48:53 PM,AAA111,Contoso Suites,123 Wholesale Road,98112,84.85,8.485 \ No newline at end of file diff --git a/samples/distributorFiles/20200817214000-OrderLineItems.csv b/samples/distributorFiles/20200817214000-OrderLineItems.csv new file mode 100644 index 0000000..5ded861 --- /dev/null +++ b/samples/distributorFiles/20200817214000-OrderLineItems.csv @@ -0,0 +1,12 @@ +ponumber,productid,quantity,unitcost,totalcost,totaltax +JKS555,288fd748-ad2b-4417-83b9-7aa5be9cff22,10,5.99,59.9,5.99 +JKS555,75542e38-563f-436f-adeb-f426f1dabb5c,10,3.99,39.9,3.99 +JKS555,4c25613a-a3c2-4ef3-8e02-9c335eb23204,5,3.49,17.45,1.745 +JKS555,75542e38-563f-436f-adeb-f426f1dabb5c,20,3.99,79.8,7.98 +PPP320,4c25613a-a3c2-4ef3-8e02-9c335eb23204,5,3.49,17.45,1.745 +GFK116,80bab959-ef8b-4ae3-8bf2-e876d77277b6,20,2.99,59.8,5.98 +DCV238,0f5a0fe8-4506-4332-969e-699a693334a8,15,15.99,239.85,23.985 +QVD251,0f5a0fe8-4506-4332-969e-699a693334a8,5,15.99,79.95,7.995 +SRA430,0f5a0fe8-4506-4332-969e-699a693334a8,20,15.99,319.8,31.98 +RNO494,65ab124a-9b2c-4294-a52d-18839364ef15,5,8.99,44.95,4.495 +RNO494,75542e38-563f-436f-adeb-f426f1dabb5c,10,3.99,39.9,3.99 \ No newline at end of file diff --git a/samples/distributorFiles/20200817214000-ProductInformation.csv b/samples/distributorFiles/20200817214000-ProductInformation.csv new file mode 100644 index 0000000..c569836 --- /dev/null +++ b/samples/distributorFiles/20200817214000-ProductInformation.csv @@ -0,0 +1,7 @@ +productid,productname,productdescription +75542e38-563f-436f-adeb-f426f1dabb5c,Starfruit Explosion,This starfruit ice cream is out of this world! +288fd748-ad2b-4417-83b9-7aa5be9cff22,Tropical Mango,You know what they say... It takes two. You. And this ice cream. +80bab959-ef8b-4ae3-8bf2-e876d77277b6,French Vanilla,It's vanilla ice cream. +4c25613a-a3c2-4ef3-8e02-9c335eb23204,Truly Orange-inal,Made from concentrate. +65ab124a-9b2c-4294-a52d-18839364ef15,Durian Durian,Smells suspect but tastes... also suspect. +0f5a0fe8-4506-4332-969e-699a693334a8,Beer,Hey this isn't ice cream! \ No newline at end of file diff --git a/samples/products.response.sample.json b/samples/products.response.sample.json new file mode 100644 index 0000000..d6c713c --- /dev/null +++ b/samples/products.response.sample.json @@ -0,0 +1,52 @@ +[ + { + "productId": "75542e38-563f-436f-adeb-f426f1dabb5c", + "productName": "Starfruit Explosion", + "productDescription": "This starfruit ice cream is out of this world!" + }, + { + "productId": "e94d85bc-7bd0-44f3-854e-d8cd70348b63", + "productName": "Just Peachy", + "productDescription": "Your taste buds and this ice cream were made for peach other." + }, + { + "productId": "288fd748-ad2b-4417-83b9-7aa5be9cff22", + "productName": "Tropical Mango", + "productDescription": "You know what they say... It takes two. You. And this ice cream." + }, + { + "productId": "76065ecd-8a14-426d-a4cd-abbde2acbb10", + "productName": "Gone Bananas", + "productDescription": "I'm not sure how appealing banana ice cream really is." + }, + { + "productId": "551a9be9-7f1c-447d-83ee-b18f5a6fb018", + "productName": "Matcha Green Tea", + "productDescription": "Green tea ice cream is good for you because it is green." + }, + { + "productId": "80bab959-ef8b-4ae3-8bf2-e876d77277b6", + "productName": "French Vanilla", + "productDescription": "It's vanilla ice cream." + }, + { + "productId": "4c25613a-a3c2-4ef3-8e02-9c335eb23204", + "productName": "Truly Orange-inal", + "productDescription": "Made from concentrate." + }, + { + "productId": "65ab124a-9b2c-4294-a52d-18839364ef15", + "productName": "Durian Durian", + "productDescription": "Smells suspect but tastes... also suspect." + }, + { + "productId": "e4e7068e-500e-4a00-8be4-630d4594735b", + "productName": "It's Grape!", + "productDescription": "Unraisinably good ice cream." + }, + { + "productId": "0f5a0fe8-4506-4332-969e-699a693334a8", + "productName": "Beer", + "productDescription": "Hey this isn't ice cream!" + } +] \ No newline at end of file diff --git a/samples/rating.response.sample.json b/samples/rating.response.sample.json new file mode 100644 index 0000000..2589f2c --- /dev/null +++ b/samples/rating.response.sample.json @@ -0,0 +1,9 @@ +{ + "id": "79c2779e-dd2e-43e8-803d-ecbebed8972c", + "userId": "cc20a6fb-a91f-4192-874d-132493685376", + "productId": "4c25613a-a3c2-4ef3-8e02-9c335eb23204", + "timestamp": "2018-05-21 21:27:47Z", + "locationName": "Sample ice cream shop", + "rating": 5, + "userNotes": "I love the subtle notes of orange in this ice cream!" + } \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..8282ad8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +rm -rf ./dist + +# for func in $(find ./dist/**/function.json); +# do +# func_dir=$(dirname $func) +# rm -rf $func_dir +# done + +# rm -rf ./dist/tmp + +tsc --build + +cp ./package.json ./dist/ + +echo "functions files: " +echo " - { host.json, settings.json }" +cp ./src/functions/{host.json,local.settings.json} ./dist/ + +for func_dir in $(ls -d1 ./src/functions/*/); +do + dir_name=$(basename $func_dir) + echo " - $dir_name/function.json" + cp ./src/functions/$dir_name/function.json ./dist/functions/$dir_name/ +done + +for func_dir in $(ls -d1 ./dist/functions/*/); +do + dir_name=$(basename $func_dir) + mv -f ./dist/functions/$dir_name ./dist/ +done + +rm -r ./dist/functions + +echo "" \ No newline at end of file diff --git a/src/functions/.funcignore b/src/functions/.funcignore new file mode 100644 index 0000000..09293b8 --- /dev/null +++ b/src/functions/.funcignore @@ -0,0 +1,6 @@ +*.js.map +*.ts +.git* +.vscode +local.settings.json +test \ No newline at end of file diff --git a/src/functions/createRating/function.json b/src/functions/createRating/function.json new file mode 100644 index 0000000..57d35f1 --- /dev/null +++ b/src/functions/createRating/function.json @@ -0,0 +1,17 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ "post"], + "route": "rating" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/src/functions/createRating/index.ts b/src/functions/createRating/index.ts new file mode 100644 index 0000000..79248f4 --- /dev/null +++ b/src/functions/createRating/index.ts @@ -0,0 +1,102 @@ +import 'module-alias/register'; +import { AzureFunction, Context, HttpRequest } from "@azure/functions" +import { UserValidator } from '@lib/user/validator'; +import { ProductValidator } from '@lib/product/validator'; +import { Rating } from '@lib/product/ratings/model'; +import { Guid } from 'guid-typescript'; +import { RatingService } from '@lib/product/ratings/service'; +import { RatingValidator } from '@lib/product/ratings/validator'; +import { SentimentService } from '@lib/product/sentiment/service'; +import { Bus, busFactory } from '@lib/core/bus'; +import { IProductSentimentReceived } from '@lib/product/sentiment/sentimentReceived'; +import { ProductService, IProduct } from '@lib/product/service'; +import { Log, ILogger } from '@lib/core/log'; + +const handler: AzureFunction = async function (context: Context, req: HttpRequest): Promise { + context.log('Create Rating called.'); + Log.logger = context; + + let command: CreateRatingRequest = req.body as CreateRatingRequest; + let response: HttpResponse = {}; + + let isValidRequest = await validateRequest(command.userId, command.productId, command.rating); + response.status = !isValidRequest ? 404 : 200; + + if (isValidRequest) { + let rating: Rating = map(command); + + rating.sentimentScore = await sentimentService.get(rating); + context.log("rating score|", JSON.stringify({ ratingId: rating.id, score: rating.sentimentScore })); + context.log('Request is valid. Inserting the rating'); + context.log(rating); + + await service.create(rating); + + // publish event that a product sentiment was received + let bus: Bus = busFactory.create(); + let event: IProductSentimentReceived = await mapFrom(rating); + await bus.sendEvents("productsentiment", [event]); + + response.body = rating; + } + + context.res = response; +}; + +interface CreateRatingRequest { + userId: string, + productId: string, + locationName: string, + rating: number, + userNotes: string +} + +interface HttpResponse { + [key: string]: any +} + +function map(command: CreateRatingRequest): Rating { + return { + id: Guid.create().toString(), + productId: command.productId, + userId: command.userId, + timestamp: new Date().toISOString(), + rating: command.rating, + locationName: command.locationName, + userNotes: command.userNotes + }; +} + +const service = new RatingService(); +const sentimentService = new SentimentService(); + +async function validateRequest(userId: string, productId: string, ratingValue: number): Promise { + const userValidator = new UserValidator(); + const productValidator = new ProductValidator(); + const ratingValidator = new RatingValidator(); + + let userResults = await userValidator.validate(userId); + let productResults = await productValidator.validate(productId); + let ratingResults = ratingValidator.validate(ratingValue); + + return userResults.isValid && productResults.isValid && ratingResults.isValid; +} + +async function mapFrom(rating: Rating): Promise { + let productService = new ProductService(); + let product: IProduct = await productService.get(rating.productId); + + let bus: Bus = busFactory.create(); + + let event = { + id: Guid.create().toString(), + productId: product.productId, + productName: product.productName, + sentimentScore: rating.sentimentScore, + timestamp: rating.timestamp, + }; + + return event; +} + +export default handler; \ No newline at end of file diff --git a/src/functions/createRating/request.sample.json b/src/functions/createRating/request.sample.json new file mode 100644 index 0000000..40d54f5 --- /dev/null +++ b/src/functions/createRating/request.sample.json @@ -0,0 +1,7 @@ +{ + "userId": "cc20a6fb-a91f-4192-874d-132493685376", + "productId": "4c25613a-a3c2-4ef3-8e02-9c335eb23204", + "locationName": "Sample ice cream shop", + "rating": 5, + "userNotes": "I love the subtle notes of orange in this ice cream!" +} \ No newline at end of file diff --git a/src/functions/distributorOrderHandler/function.json b/src/functions/distributorOrderHandler/function.json new file mode 100644 index 0000000..d9db828 --- /dev/null +++ b/src/functions/distributorOrderHandler/function.json @@ -0,0 +1,10 @@ +{ + "bindings": [ + { + "name": "context", + "type": "entityTrigger", + "direction": "in" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/src/functions/distributorOrderHandler/index.ts b/src/functions/distributorOrderHandler/index.ts new file mode 100644 index 0000000..2588a02 --- /dev/null +++ b/src/functions/distributorOrderHandler/index.ts @@ -0,0 +1,32 @@ +import 'module-alias/register'; +import { entity } from "durable-functions" +import { IEntityFunctionContext } from "durable-functions/lib/src/classes"; +import { DistributorOrderMessage } from "@lib/sales/distributor/distributorOrderMessage"; +import { distributorOrderSagaFactory } from "@lib/sales/distributor/saga"; +import { DistributorOrderSagaData } from "@lib/sales/distributor/sagaData"; +import { Log, ILogger } from '@lib/core/log'; + +const handler = entity(async function (context: IEntityFunctionContext): Promise { + context.log("distributorOrderHandler called.") + Log.logger = context; + + let message = context.df.getInput(); + let operation = context.df.operationName; + + let data = context.df.getState(() => new DistributorOrderSagaData()); + data.id = context.df.entityId; + + let processor = distributorOrderSagaFactory.create(data); + await processor[operation](message); + context.log("Saga data", data); + + if (!data.isComplete) { + context.df.setState(data); + context.log("state set. saga not complete."); + } else { + context.log("Calling destruct on exit."); + this.context.df.destructOnExit(); + } +}); + +export default handler; \ No newline at end of file diff --git a/src/functions/distributorOrderHandlerTrigger/function.json b/src/functions/distributorOrderHandlerTrigger/function.json new file mode 100644 index 0000000..abb991a --- /dev/null +++ b/src/functions/distributorOrderHandlerTrigger/function.json @@ -0,0 +1,15 @@ +{ + "bindings": [ + { + "type": "eventGridTrigger", + "name": "message", + "direction": "in" + }, + { + "name": "starter", + "type": "durableClient", + "direction": "in" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/src/functions/distributorOrderHandlerTrigger/index.ts b/src/functions/distributorOrderHandlerTrigger/index.ts new file mode 100644 index 0000000..64f4200 --- /dev/null +++ b/src/functions/distributorOrderHandlerTrigger/index.ts @@ -0,0 +1,30 @@ +import 'module-alias/register'; +import * as durableFunctions from "durable-functions" +import { DurableOrchestrationClient, IEntityFunctionContext, EntityId } from "durable-functions/lib/src/classes"; +import { EventGridEvent } from "@lib/core/eventGridEvent" +import entityIdFactory from "@lib/sales/distributor/entityIdFactory"; +import { DistributorOrderMessage, distributorOrderMessageFactory } from "@lib/sales/distributor/distributorOrderMessage"; +import { Log, ILogger } from '@lib/core/log'; + +const hasStarted = async function (entityId: EntityId, client: DurableOrchestrationClient) { + let state = await client.readEntityState(entityId); + return !state.entityExists; +} + +const handler = async function (context: IEntityFunctionContext, message: EventGridEvent): Promise { + Log.logger = context; + + const client: DurableOrchestrationClient = durableFunctions.getClient(context); + const distributorOrderMessage: DistributorOrderMessage = distributorOrderMessageFactory(message); + const entityId = entityIdFactory(message); + + const hasDistributorOrderSagaBeenStarted = hasStarted(entityId, client); + const operation = hasDistributorOrderSagaBeenStarted ? 'handle' : 'start'; + context.log(distributorOrderMessage); + + await client.signalEntity(entityId, operation, distributorOrderMessage); + + context.log(`distributorOrderHandlerTrigger with ID = '${entityId.key}'.`); +} + +export default handler; \ No newline at end of file diff --git a/src/functions/getProduct/function.json b/src/functions/getProduct/function.json new file mode 100644 index 0000000..dcbef68 --- /dev/null +++ b/src/functions/getProduct/function.json @@ -0,0 +1,17 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ "get"], + "route": "products/{id?}" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/src/functions/getProduct/index.ts b/src/functions/getProduct/index.ts new file mode 100644 index 0000000..1d9d9f6 --- /dev/null +++ b/src/functions/getProduct/index.ts @@ -0,0 +1,30 @@ +import { AzureFunction, Context, HttpRequest } from "@azure/functions" +import { Guid } from "guid-typescript" + +const handler: AzureFunction = async function (context: Context, req: HttpRequest): Promise { + context.log('HTTP trigger function processed a request.'); + const productId = Guid.parse(req.query.productId || context.bindingData.id); + let isProductIdValid = productId.toString() != Guid.EMPTY; + let response: HttpResponse = {}; + + if (isProductIdValid) { + response.body = createResponse(productId); + } + else { + response.status = 406; + response.body = `Invalid Product ID of ${context.bindingData.id}.`; + } + + context.res = response; +}; + +interface HttpResponse { + [key: string]: any +} + +function createResponse(productId: Guid): string { + let message = `The product name for your product id ${productId} is Starfruit Explosion`; + return message; +} + +export default handler; \ No newline at end of file diff --git a/src/functions/getRating/function.json b/src/functions/getRating/function.json new file mode 100644 index 0000000..4f5e92f --- /dev/null +++ b/src/functions/getRating/function.json @@ -0,0 +1,17 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ "get"], + "route": "rating/{ratingId}" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/src/functions/getRating/index.ts b/src/functions/getRating/index.ts new file mode 100644 index 0000000..f639e8c --- /dev/null +++ b/src/functions/getRating/index.ts @@ -0,0 +1,33 @@ +import 'module-alias/register'; +import { AzureFunction, Context, HttpRequest } from "@azure/functions" +import { RatingService } from "@lib/product/ratings/service"; + +const ratingService: RatingService = new RatingService(); + +const handler: AzureFunction = async function (context: Context, req: HttpRequest): Promise { + context.log('Request received to get rating'); + + const ratingId: string = context.bindingData.ratingId; + const rating = await ratingService.get(ratingId); + + if (rating) { + context.res = { + body: { + id: rating.id, + productId: rating.productId, + userId: rating.userId, + timestamp: rating.timestamp, + locationName: rating.locationName, + userNotes: rating.userNotes + } + }; + } + else { + context.res = { + status: 404, + body: "rating not found." + } + } +}; + +export default handler; \ No newline at end of file diff --git a/src/functions/getRatings/function.json b/src/functions/getRatings/function.json new file mode 100644 index 0000000..4d28f8b --- /dev/null +++ b/src/functions/getRatings/function.json @@ -0,0 +1,17 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ "get"], + "route": "rating" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/src/functions/getRatings/index.ts b/src/functions/getRatings/index.ts new file mode 100644 index 0000000..f8e3fe5 --- /dev/null +++ b/src/functions/getRatings/index.ts @@ -0,0 +1,15 @@ +import 'module-alias/register'; +import { AzureFunction, Context, HttpRequest } from "@azure/functions" +import { RatingService } from "@lib/product/ratings/service" + +const ratingService = new RatingService(); + +const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise { + const userId = req.query.userId; + context.log(`Request received for userId '${userId}'`); + + let ratings = await ratingService.findAll(userId); + context.res = { body: ratings }; +}; + +export default httpTrigger; \ No newline at end of file diff --git a/src/functions/host.json b/src/functions/host.json new file mode 100644 index 0000000..04e2655 --- /dev/null +++ b/src/functions/host.json @@ -0,0 +1,25 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "durableTask": {}, + "eventHubs": { + "batchCheckpointFrequency": 5, + "eventProcessorOptions": { + "maxBatchSize": 64, + "prefetchCount": 256 + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[2.*, 3.0.0)" + } +} \ No newline at end of file diff --git a/src/functions/largeReceiptSubscriber/function.json b/src/functions/largeReceiptSubscriber/function.json new file mode 100644 index 0000000..41cc7cc --- /dev/null +++ b/src/functions/largeReceiptSubscriber/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "name": "message", + "type": "serviceBusTrigger", + "direction": "in", + "topicName": "receiptReceived", + "subscriptionName": "receiptReceived.large", + "connection": "SB_CONNECTION_STRING" + }, + { + "name": "outputBlob", + "type": "blob", + "path": "receipts-high-value/{salesNumber}-{DateTime}.json", + "connection": "RECEIPTS_STORAGE_CONNECTION", + "direction": "out" + } + ] +} \ No newline at end of file diff --git a/src/functions/largeReceiptSubscriber/index.ts b/src/functions/largeReceiptSubscriber/index.ts new file mode 100644 index 0000000..3ce4612 --- /dev/null +++ b/src/functions/largeReceiptSubscriber/index.ts @@ -0,0 +1,18 @@ +import 'module-alias/register'; +import { ReceiptReceived } from "@lib/receipts/receiptReceived"; +import { Context } from "@azure/functions/Interfaces"; +import { receiptReceivedHandlerFactory, ReceiptReceivedHandler } from '@lib/receipts/receiptReceivedHandler'; +import { ILogger, Log } from '@lib/core/log'; + +const handler = async function (context: Context, message: ReceiptReceived): Promise { + Log.logger = context; + context.log('Large ReceiptReceived subscriber triggered: ', message); + + const handler: ReceiptReceivedHandler = receiptReceivedHandlerFactory.create(); + const archiveData = await handler.handle(message, true); + + context.bindings.outputBlob = archiveData; + context.done(); +} + +export default handler; \ No newline at end of file diff --git a/src/functions/local.settings.json b/src/functions/local.settings.json new file mode 100644 index 0000000..36990a0 --- /dev/null +++ b/src/functions/local.settings.json @@ -0,0 +1,14 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_EXTENSION_VERSION": "~3", + "TEXT_ANALYSIS_KEY": "", + "RECEIPTS_STORAGE_CONNECTION": "", + "SB_CONNECTION_STRING": "", + "EVENTHUB_CONNECTION_STRING": "", + "COSMOS_CONNECTION_STRING": "", + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsStorage": "", + }, + "ConnectionStrings": {} +} \ No newline at end of file diff --git a/src/functions/posSalesHandler/function.json b/src/functions/posSalesHandler/function.json new file mode 100644 index 0000000..5118754 --- /dev/null +++ b/src/functions/posSalesHandler/function.json @@ -0,0 +1,13 @@ +{ + "bindings": [ + { + "type": "eventHubTrigger", + "name": "messages", + "direction": "in", + "eventHubName": "salesevents", + "connection": "EVENTHUB_CONNECTION_STRING", + "cardinality": "many", + "consumerGroup": "$Default" + } + ] +} \ No newline at end of file diff --git a/src/functions/posSalesHandler/index.ts b/src/functions/posSalesHandler/index.ts new file mode 100644 index 0000000..7aa11a0 --- /dev/null +++ b/src/functions/posSalesHandler/index.ts @@ -0,0 +1,27 @@ +import 'module-alias/register'; +import { SendableMessageInfo } from '@azure/service-bus' +import { Context } from "@azure/functions/Interfaces"; +import { posOrderProcessorFactory, PosOrderProcessor } from "@lib/sales/pointOfSale/processor"; +import { ILogger, Log } from "@lib/core/log"; +import { IOrder } from '@lib/sales/order'; +import { OrderSource } from '@lib/sales/orderSource'; +import { busFactory, Bus } from '@lib/core/bus'; + +const handler = async function (context: Context, messages: IOrder[]): Promise { + Log.logger = context; + context.log(`POS Sales received: ${messages.length}`); + + messages.forEach(m => m.source = OrderSource.PointOfSale); + + let processor: PosOrderProcessor = posOrderProcessorFactory.create(); + let receiptsReceived = await processor.process(messages); + + let bus: Bus = busFactory.create(); + await bus.publish("receiptReceived", receiptsReceived.map(m => { + body: m, + userProperties: { totalCost: m.totalCost } + })); + context.done(); +} + +export default handler; diff --git a/src/functions/smallReceiptSubscriber/function.json b/src/functions/smallReceiptSubscriber/function.json new file mode 100644 index 0000000..7e41b02 --- /dev/null +++ b/src/functions/smallReceiptSubscriber/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "name": "message", + "type": "serviceBusTrigger", + "direction": "in", + "topicName": "receiptReceived", + "subscriptionName": "receiptReceived.small", + "connection": "SB_CONNECTION_STRING" + }, + { + "name": "outputBlob", + "type": "blob", + "path": "receipts/{salesNumber}-{DateTime}.json", + "connection": "RECEIPTS_STORAGE_CONNECTION", + "direction": "out" + } + ] +} \ No newline at end of file diff --git a/src/functions/smallReceiptSubscriber/index.ts b/src/functions/smallReceiptSubscriber/index.ts new file mode 100644 index 0000000..f12f516 --- /dev/null +++ b/src/functions/smallReceiptSubscriber/index.ts @@ -0,0 +1,18 @@ +import 'module-alias/register'; +import { ReceiptReceived } from "@lib/receipts/receiptReceived"; +import { Context } from "@azure/functions/Interfaces"; +import { receiptReceivedHandlerFactory, ReceiptReceivedHandler } from '@lib/receipts/receiptReceivedHandler'; +import { ILogger, Log } from '@lib/core/log'; + +const handler = async function (context: Context, message: ReceiptReceived): Promise { + Log.logger = context; + + context.log('Small ReceiptReceived subscriber triggered: ', message); + const handler: ReceiptReceivedHandler = receiptReceivedHandlerFactory.create(); + const archiveData = await handler.handle(message, false); + + context.bindings.outputBlob = archiveData; + context.done(); +} + +export default handler; \ No newline at end of file diff --git a/src/lib/config/settings.ts b/src/lib/config/settings.ts new file mode 100644 index 0000000..139597f --- /dev/null +++ b/src/lib/config/settings.ts @@ -0,0 +1,2 @@ + + diff --git a/src/lib/core/bus.ts b/src/lib/core/bus.ts new file mode 100644 index 0000000..083011b --- /dev/null +++ b/src/lib/core/bus.ts @@ -0,0 +1,51 @@ +import { EventHubProducerClient, EventDataBatch } from "@azure/event-hubs"; +import { ServiceBusClient, SendableMessageInfo, Sender } from '@azure/service-bus' +import { ILogger, Log } from "./log"; + +class Bus { + serviceBus : ServiceBusClient; + logger: ILogger; + + constructor(serviceBus : ServiceBusClient, logger: ILogger) { + this.serviceBus = serviceBus; + this.logger = logger; + } + + async sendEvents(hubName: string, messages: T[]): Promise { + const client = new EventHubProducerClient(process.env.EVENTHUB_CONNECTION_STRING, hubName); + const batchOptions = {}; + let batch = await client.createBatch(batchOptions); + + for (let index = 0; index < messages.length; index++) { + const message = { body: messages[index] } + batch.tryAdd(message); + + // this.logger.log(`Message added to batch:`, message); + } + await client.sendBatch(batch); + this.logger.log(`Messages sent to hub ${hubName}:`, batch.count) + } + + async publish(topic: string, messages: T[]): Promise { + const sender = this.getSender(topic); + + for (let index = 0; index < messages.length; index++) { + const msg = messages[index]; + await sender.send(msg); + } + await sender.close(); + } + + private getSender(topic: string): Sender { + return this.serviceBus.createTopicClient(topic).createSender(); + } +} + +const busFactory = { + create() { + const serviceBus = ServiceBusClient.createFromConnectionString(process.env.SB_CONNECTION_STRING); + return new Bus(serviceBus, Log.logger); + } +} + +export { Bus, busFactory } \ No newline at end of file diff --git a/src/lib/core/cache.ts b/src/lib/core/cache.ts new file mode 100644 index 0000000..70f7070 --- /dev/null +++ b/src/lib/core/cache.ts @@ -0,0 +1,16 @@ +interface Dictionary { + [key: string]: T; +} + +// cache +export class Cache { + private items: Dictionary = {}; + + public get(id: string) { + return this.items[id]; + } + + public set(key: string, item: T) { + this.items[key] = item; + } +} \ No newline at end of file diff --git a/src/lib/core/dataContext.ts b/src/lib/core/dataContext.ts new file mode 100644 index 0000000..7853413 --- /dev/null +++ b/src/lib/core/dataContext.ts @@ -0,0 +1,43 @@ +import { CosmosClient, CosmosClientOptions, Container, SqlQuerySpec } from "@azure/cosmos" +import { RequestOptions } from "https"; + +const options: CosmosClientOptions = { + endpoint: "https://bfyoc-cosmos-1.documents.azure.com:443/", + key: "EcrwEJp4U5J4OauX7A2Gu5LEomwtwoL17uFutv8GmX9tKC4FEc7cxVsTIE9QIpa41DOxaqBFQDVtYsY2uBYeXg==" +} + +export class DataContext { + private readonly client: CosmosClient = new CosmosClient(options); + private readonly databaseId: string = "default"; + private containerId: string; + + constructor(containerId: string) { + this.containerId = containerId; + } + + public async create(document: any): Promise { + let container = this.getContainer(); + await container.items.create(document); + } + + public async findAll(query: string): Promise { + const statement = { + query: query + }; + let container = this.getContainer(); + const response = await container.items.query(statement).fetchAll(); + return response.resources; + } + + public async find(id: string): Promise { + let container = this.getContainer(); + let response = await this.findAll(`SELECT * FROM c WHERE c.id = '${id}'`); + return response[0]; + } + + private getContainer(): Container { + return this.client.database(this.databaseId).container(this.containerId); + } +} + +export default DataContext; \ No newline at end of file diff --git a/src/lib/core/eventGridEvent.ts b/src/lib/core/eventGridEvent.ts new file mode 100644 index 0000000..998ef52 --- /dev/null +++ b/src/lib/core/eventGridEvent.ts @@ -0,0 +1,40 @@ +/** + * Properties of an event published to an Event Grid topic. + */ +export interface EventGridEvent { + /** + * An unique identifier for the event. + */ + id: string; + /** + * The resource path of the event source. + */ + topic?: string; + /** + * A resource path relative to the topic path. + */ + subject: string; + /** + * Event data specific to the event type. + */ + data: { + [k: string]: unknown; + }; + /** + * The type of the event that occurred. + */ + eventType: string; + /** + * The time (in UTC) the event was generated. + */ + eventTime: string; + /** + * The schema version of the event metadata. + */ + metadataVersion?: string; + /** + * The schema version of the data object. + */ + dataVersion: string; + [k: string]: unknown; +} diff --git a/src/lib/core/functionHandler.ts b/src/lib/core/functionHandler.ts new file mode 100644 index 0000000..43cdc4b --- /dev/null +++ b/src/lib/core/functionHandler.ts @@ -0,0 +1,11 @@ + +export interface IFunctionHandler { + handle(): Promise +} + +class FunctionHandler implements IFunctionHandler { + handle(): Promise { + throw new Error("Method not implemented."); + } + +} \ No newline at end of file diff --git a/src/lib/core/httpClient.ts b/src/lib/core/httpClient.ts new file mode 100644 index 0000000..a13b375 --- /dev/null +++ b/src/lib/core/httpClient.ts @@ -0,0 +1,46 @@ +import { HttpClient } from "typed-rest-client/HttpClient"; +import { ILogger, Log } from "./log"; +import { IHttpClientResponse, IRequestOptions } from "typed-rest-client/Interfaces"; + +interface IHttpClient { + post(url: string, request: TRequest): Promise; +} + +class DefaultHttpClient implements IHttpClient { + private readonly baseUrl: string; + private readonly client: HttpClient; + private readonly logger: ILogger; + + constructor(baseUrl: string = null, client: HttpClient, logger: ILogger) { + this.baseUrl = baseUrl; + this.client = client + this.logger = logger; + } + + async post(url: string, request: TRequest): Promise { + const requestUrl = this.resolveUrl(url); + const data = JSON.stringify(request, null, 2); + + let httpResponse: IHttpClientResponse = await this.client.post(requestUrl, data); + let body: string = await httpResponse.readBody(); + let response: TResponse = JSON.parse(body); + + return response; + } + + private resolveUrl(url: string): string { + return this.baseUrl ? this.baseUrl.concat(url) : url; + } +} + +const httpClientFactory = { + create(baseUrl: string = null): IHttpClient { + let client = new HttpClient("bfyoc-backend-1", null, { + ignoreSslError: true + }); + let logger = Log.logger; + return new DefaultHttpClient(baseUrl, client, logger); + } +} + +export { IHttpClient, httpClientFactory } \ No newline at end of file diff --git a/src/lib/core/log.ts b/src/lib/core/log.ts new file mode 100644 index 0000000..13eae7b --- /dev/null +++ b/src/lib/core/log.ts @@ -0,0 +1,12 @@ + +export interface ILogger { + log: (...args: any[]) => void +} + +/** + * The global logger + */ +export class Log { + public static logger: ILogger; + private constructor() {} +} \ No newline at end of file diff --git a/src/lib/core/message.ts b/src/lib/core/message.ts new file mode 100644 index 0000000..4b86d1f --- /dev/null +++ b/src/lib/core/message.ts @@ -0,0 +1,4 @@ + +export interface IMessage { + id: string; +} \ No newline at end of file diff --git a/src/lib/core/saga.ts b/src/lib/core/saga.ts new file mode 100644 index 0000000..105073f --- /dev/null +++ b/src/lib/core/saga.ts @@ -0,0 +1,11 @@ +import { ILogger, Log } from "@lib/core/log"; + +export class Saga { + data: TData; + logger: ILogger; + + constructor(data: TData, logger: ILogger) { + this.data = data; + this.logger = logger; + } +} diff --git a/src/lib/core/sagaContext.ts b/src/lib/core/sagaContext.ts new file mode 100644 index 0000000..3797f99 --- /dev/null +++ b/src/lib/core/sagaContext.ts @@ -0,0 +1,10 @@ +import { EntityId } from "durable-functions/lib/src/entities/entityid"; + +export interface ISagaContext { + getState(initializer?: () => unknown): unknown | undefined; + setState(state: unknown): void; + getInput(): unknown | undefined; + destructOnExit(): void; + + readonly entityId: EntityId; +} \ No newline at end of file diff --git a/src/lib/core/sagaData.ts b/src/lib/core/sagaData.ts new file mode 100644 index 0000000..2568ffe --- /dev/null +++ b/src/lib/core/sagaData.ts @@ -0,0 +1,6 @@ +import { EntityId } from "durable-functions/lib/src/entities/entityid"; + +export class SagaData { + id: EntityId; + isComplete: boolean; +} \ No newline at end of file diff --git a/src/lib/core/sagaHandler.ts b/src/lib/core/sagaHandler.ts new file mode 100644 index 0000000..27c11c7 --- /dev/null +++ b/src/lib/core/sagaHandler.ts @@ -0,0 +1,54 @@ +import { SagaData } from "./sagaData"; +import { ISagaContext } from "./sagaContext"; +import { ILogger, Log } from "./log"; + + +class SagaHandler { + private readonly context: ISagaContext; + private readonly stateInitializer: new () => TData; + data: TData; + logger: ILogger; + defaultData: TData; + + constructor(context: ISagaContext, logger: ILogger, defaultData: TData) { + this.context = context; + this.logger = logger; + this.defaultData = defaultData; + } + + public handle(func: (data: TData) => void) { + this.logger.log("sagaHandler.handle executed"); + + this.getState(); + func(this.data); + this.setState(); + } + + private setState() { + this.logger.log("sagaHandler setting state:", this.data); + + if (!this.data.isComplete) { + this.context.setState(this.data); + this.logger.log("sagaHandler state set"); + } else { + this.context.destructOnExit(); + } + } + + private getState() { + this.data = this.context.getState(() => this.defaultData); + + this.data.id = this.context.entityId; + + this.logger.log("sagaHandler data:", this.data); + } +} + +const sagaHandlerFactory = { + create(context: ISagaContext, defaultData: TData): SagaHandler { + let handler = new SagaHandler(context, Log.logger, defaultData); + return handler; + } +} + +export { SagaHandler, sagaHandlerFactory } \ No newline at end of file diff --git a/src/lib/core/service.ts b/src/lib/core/service.ts new file mode 100644 index 0000000..a34bba0 --- /dev/null +++ b/src/lib/core/service.ts @@ -0,0 +1,35 @@ +import { RestClient, IRestResponse, IRequestOptions } from 'typed-rest-client' +import { IRequestQueryParams } from 'typed-rest-client/Interfaces'; +import { Cache } from "@lib/core/cache" + +export abstract class ServiceBase { + private readonly client: RestClient; + protected isCacheFilled: boolean; + protected cache: Cache; + + constructor(cache: Cache, baseUrl: string) { + this.cache = cache; + this.client = new RestClient("bfyoc-backend-", baseUrl); + } + + public async get(id: string): Promise { + await this.fillCache(); + return this.cache.get(id); + } + + abstract async fillCache(): Promise; + + protected async fetch(relativeUrl: string, query: any): Promise { + let response: IRestResponse = await this.client.get(relativeUrl, { + queryParameters: { + params: query + } + }); + return response.result; + } + + protected async fetchAll(relativeUrl: string): Promise { + let response: IRestResponse = await this.client.get(relativeUrl); + return response.result; + } +} \ No newline at end of file diff --git a/src/lib/notifications/product.notify.tmpl.html b/src/lib/notifications/product.notify.tmpl.html new file mode 100644 index 0000000..074d270 --- /dev/null +++ b/src/lib/notifications/product.notify.tmpl.html @@ -0,0 +1,41 @@ + + + + + + + + + +
+ Fruit Ice Cream + +

Best For You Organics

+
+

New Ice Cream Line!

+

+ Best For You Organics have a new line of fruit flavored ice creams. + Below is the information so you can start the ordering process: +

+ + + + + + + + + + + +
Ice CreamDescriptionProduct ID
+

Please contact + your representative at Best For You Organics to get more information..

+ + + \ No newline at end of file diff --git a/src/lib/product/productPurchased.ts b/src/lib/product/productPurchased.ts new file mode 100644 index 0000000..2efd481 --- /dev/null +++ b/src/lib/product/productPurchased.ts @@ -0,0 +1,9 @@ +export interface IProductPurchased { + id: string; + productId: string; + productName: string; + quantity: number; + purchaseTotal: number; + timestamp: string; + source: string; +} \ No newline at end of file diff --git a/src/lib/product/ratings/model.ts b/src/lib/product/ratings/model.ts new file mode 100644 index 0000000..4b7f078 --- /dev/null +++ b/src/lib/product/ratings/model.ts @@ -0,0 +1,11 @@ + +export interface Rating { + id: string; + userId: string; + productId: string; + timestamp: string; + locationName: string; + rating: number; + userNotes: string; + sentimentScore: number +} \ No newline at end of file diff --git a/src/lib/product/ratings/service.ts b/src/lib/product/ratings/service.ts new file mode 100644 index 0000000..225fe72 --- /dev/null +++ b/src/lib/product/ratings/service.ts @@ -0,0 +1,46 @@ +import { DataContext } from "@lib/core/dataContext" +import { Rating } from "./model" +import { ServiceBase } from "@lib/core/service" +import { Cache } from "@lib/core/cache" + +const cache: Cache = new Cache(); + +export class RatingService extends ServiceBase { + private readonly name: string = "rating service"; + private readonly dataContext = new DataContext("ratings"); + + constructor() { + super(cache, null); + } + + public async create(rating: Rating): Promise { + await this.dataContext.create(rating); + } + + public async get(id: string): Promise { + return await this.dataContext.find(id); + } + + fillCache(): Promise { + // do nothing right now; + return; + } + + public async findAll(userId: string): Promise { + const query = `SELECT * FROM c WHERE c.userId = '${userId}'`; + const items = await this.dataContext.findAll(query); + + let ratings = items.map(item => { + return { + id: item.id, + userId: item.userId, + timestamp: item.timestamp, + rating: item.rating, + locationName: item.locationName, + productId: item.productId, + userNotes: item.userNotes + }; + }); + return ratings; + } +} \ No newline at end of file diff --git a/src/lib/product/ratings/validator.ts b/src/lib/product/ratings/validator.ts new file mode 100644 index 0000000..d1a3fcd --- /dev/null +++ b/src/lib/product/ratings/validator.ts @@ -0,0 +1,12 @@ +interface ValidationResult { + isValid: boolean, + reason?: string +} + +class RatingValidator { + public validate(value: number): ValidationResult { + return { isValid: (value >= 0 && value <= 5) }; + } +} + +export { ValidationResult, RatingValidator } \ No newline at end of file diff --git a/src/lib/product/sentiment/sentimentReceived.ts b/src/lib/product/sentiment/sentimentReceived.ts new file mode 100644 index 0000000..1a52946 --- /dev/null +++ b/src/lib/product/sentiment/sentimentReceived.ts @@ -0,0 +1,8 @@ + +export interface IProductSentimentReceived { + id: string; + productId: string; + productName: string; + sentimentScore: number; + timestamp: string; +} \ No newline at end of file diff --git a/src/lib/product/sentiment/sentimentRequest.ts b/src/lib/product/sentiment/sentimentRequest.ts new file mode 100644 index 0000000..7ed3735 --- /dev/null +++ b/src/lib/product/sentiment/sentimentRequest.ts @@ -0,0 +1,9 @@ +export interface IDocument { + language: string; + id: string; + text: string; +} + +export interface ISentimentRequest { + documents: IDocument[]; +} \ No newline at end of file diff --git a/src/lib/product/sentiment/sentimentResponse.ts b/src/lib/product/sentiment/sentimentResponse.ts new file mode 100644 index 0000000..16ab50e --- /dev/null +++ b/src/lib/product/sentiment/sentimentResponse.ts @@ -0,0 +1,28 @@ + +export interface ConfidenceScores { + positive: number; + neutral: number; + negative: number; +} + +export interface Sentence { + sentiment: string; + confidenceScores: ConfidenceScores; + offset: number; + length: number; + text: string; +} + +export interface SentimentDocument { + id: string; + sentiment: string; + confidenceScores: ConfidenceScores; + sentences: Sentence[]; + warnings: any[]; +} + +export interface SentimentResponse { + documents: SentimentDocument[]; + errors: any[]; + modelVersion: string; +} diff --git a/src/lib/product/sentiment/service.ts b/src/lib/product/sentiment/service.ts new file mode 100644 index 0000000..164d49b --- /dev/null +++ b/src/lib/product/sentiment/service.ts @@ -0,0 +1,46 @@ +import { ISentimentRequest, IDocument } from "./sentimentRequest"; +import { HttpClient } from "typed-rest-client/HttpClient"; +import { IHeaders, IHttpClientResponse } from "typed-rest-client/Interfaces" +import { SentimentResponse, SentimentDocument } from "./sentimentResponse"; +import { Rating } from "../ratings/model"; + +export class SentimentService { + private serviceUrl: string = "https://bfyoc-textanalytics-1.cognitiveservices.azure.com/text/analytics/v3.0/sentiment"; + private readonly client: HttpClient; + private key: string; + + constructor() { + this.key = process.env.TEXT_ANALYSIS_KEY; + this.client = new HttpClient("bfyoc-backend-1"); + } + + async get(rating: Rating): Promise { + const request = { + documents: [{ + id: rating.id, + language: 'en', + text: rating.userNotes + }] + } + + const data = JSON.stringify(request); + let httpResponse: IHttpClientResponse = await this.client.post(this.serviceUrl, data, { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Ocp-Apim-Subscription-Key': this.key + }); + + let response = await this.getSentimentResponse(httpResponse); + const score: number = response.documents[0].confidenceScores.positive; + + return score; + } + + async getSentimentResponse(httpResponse: IHttpClientResponse): Promise { + let body: string = await httpResponse.readBody(); + let response: SentimentResponse = JSON.parse(body); + + return response; + } + +} \ No newline at end of file diff --git a/src/lib/product/service.ts b/src/lib/product/service.ts new file mode 100644 index 0000000..a18e167 --- /dev/null +++ b/src/lib/product/service.ts @@ -0,0 +1,26 @@ +import { Cache } from "@lib/core/cache" +import { ServiceBase } from "@lib/core/service" + +export interface IProduct { + productId: string, + productName: string, + productDescription: string +} + +const cache: Cache = new Cache(); +const baseUrl: string = "https://serverlessohproduct.trafficmanager.net/"; + +export class ProductService extends ServiceBase { + constructor() { + super(cache, baseUrl); + } + + async fillCache(): Promise { + if (this.isCacheFilled) { + return; + } + let products = await this.fetchAll("api/GetProducts"); + products.forEach(p => this.cache.set(p.productId, p)); + this.isCacheFilled = true; + } +} \ No newline at end of file diff --git a/src/lib/product/validator.ts b/src/lib/product/validator.ts new file mode 100644 index 0000000..0187f83 --- /dev/null +++ b/src/lib/product/validator.ts @@ -0,0 +1,21 @@ +import { ProductService } from "./service" + +interface ValidationResult { + isValid: boolean, + reason?: string +} + +const productService = new ProductService(); + +class ProductValidator { + public async validate(productId: string): Promise { + let exists = (await productService.get(productId)) != undefined; + + return { + isValid: exists, + reason: exists ? "product exists." : "product not found. invalid product id." + } + } +} + +export { ValidationResult, ProductValidator } \ No newline at end of file diff --git a/src/lib/receipts/archiveReceipt.ts b/src/lib/receipts/archiveReceipt.ts new file mode 100644 index 0000000..ebdad76 --- /dev/null +++ b/src/lib/receipts/archiveReceipt.ts @@ -0,0 +1,15 @@ + + +interface IArchiveReceipt { + Store: string; + SalesNumber: string; + TotalCost: number; + Items: number; + SalesDate: string; +} + +interface ILargeArchiveReceipt extends IArchiveReceipt { + ReceiptImage: string; +} + +export { IArchiveReceipt, ILargeArchiveReceipt } diff --git a/src/lib/receipts/receiptFileSerializer.ts b/src/lib/receipts/receiptFileSerializer.ts new file mode 100644 index 0000000..40d3609 --- /dev/null +++ b/src/lib/receipts/receiptFileSerializer.ts @@ -0,0 +1,92 @@ +import * as fs from 'fs' +import * as path from 'path' +import { Guid } from "guid-typescript" +import { HttpClient } from "typed-rest-client/HttpClient" +import { ILogger } from '@lib/core/log' +import { IHeaders } from 'typed-rest-client/Interfaces' + +class ReceiptFileSerializer { + private readonly client: HttpClient; + private readonly logger: ILogger; + + constructor(logger: ILogger) { + this.client = new HttpClient("bfyoc-backend-1"); + this.logger = logger; + } + + public async serialize(url: string): Promise { + this.logger.log("Serializing receipt file for url: ", url) + + const fileInfo = this.getFileInfo(); + await this.writeHttpResponseToFile(url, fileInfo); + let data = await this.getFileDataAsBase64(fileInfo); + return data; + } + + private async getFileDataAsBase64(fileInfo: FileInfo): Promise { + let buffer= Buffer.from(fs.readFileSync(fileInfo.path, 'binary'), 'binary'); + + this.logger.log('Buffer length of PDF: ', buffer.length) + let data = buffer.toString('base64'); + + if (data.length == 0) { + this.logger.log("PDF contents could not be processed. Returning empty string.") + } + + this.logger.log('deleting file: ', fileInfo.path) + fs.unlinkSync(fileInfo.path); + + return data; + } + + private async writeHttpResponseToFile(url: string, fileInfo: FileInfo): Promise { + let response = await this.client.get(url, { + 'Content-Type': 'application/pdf' + }); + + const file: NodeJS.WritableStream = fileInfo.stream; + const filePath: string = fileInfo.path; + + return new Promise((resolve, reject) => { + file.on("error", (err) => reject(err)); + const stream = response.message.pipe(file); + stream.on("close", () => { + try { resolve(filePath); } catch (err) { + reject(err); + } + }); + }); + } + + private getFileInfo(): FileInfo { + const filePath: string = path.join(this.getTempFolder(), Guid.create().toString() + ".pdf"); + + return { + path: filePath, + stream: fs.createWriteStream(filePath, { encoding: 'binary', autoClose: true }) + } + } + + private getTempFolder(): string { + const folderPath = path.join(process.cwd(), "../tmp"); + + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath); + } + + return folderPath; + } +} + +const receiptFileSerializerFactory = { + create(logger: ILogger): ReceiptFileSerializer { + return new ReceiptFileSerializer(logger); + } +} + +interface FileInfo { + path: string; + stream: NodeJS.WritableStream +} + +export { ReceiptFileSerializer, receiptFileSerializerFactory } \ No newline at end of file diff --git a/src/lib/receipts/receiptReceived.ts b/src/lib/receipts/receiptReceived.ts new file mode 100644 index 0000000..814bc28 --- /dev/null +++ b/src/lib/receipts/receiptReceived.ts @@ -0,0 +1,8 @@ +export interface ReceiptReceived { + totalItems: number; + totalCost: number; + salesNumber: string; + salesDate: string; + storeLocation: string; + receiptUrl: string; +} diff --git a/src/lib/receipts/receiptReceivedHandler.ts b/src/lib/receipts/receiptReceivedHandler.ts new file mode 100644 index 0000000..746f0d7 --- /dev/null +++ b/src/lib/receipts/receiptReceivedHandler.ts @@ -0,0 +1,47 @@ +import { ReceiptFileSerializer } from "./receiptFileSerializer"; +import { ILargeArchiveReceipt, IArchiveReceipt } from "./archiveReceipt"; +import { ReceiptReceived } from "./receiptReceived"; +import * as Url from 'url' +import { ILogger, Log } from "@lib/core/log"; + +class ReceiptReceivedHandler { + private serializer: ReceiptFileSerializer; + private logger: ILogger; + + constructor(serializer: ReceiptFileSerializer, logger: ILogger) { + this.serializer = serializer; + this.logger = logger; + } + + async handle(message: ReceiptReceived, includeReceiptFile: boolean): Promise { + let archive: any = { + Store: message.storeLocation, + SalesNumber: message.salesNumber, + SalesDate: message.salesDate, + TotalCost: message.totalCost, + Items: message.totalItems + }; + + if (includeReceiptFile && this.isReceiptUrlValid(message.receiptUrl)) { + this.logger.log("Receipt Url is valid: ", message.receiptUrl) + + const receiptImage = await this.serializer.serialize(message.receiptUrl); + (archive).ReceiptImage = receiptImage; + } + return JSON.stringify(archive); + } + + isReceiptUrlValid(url: string): boolean { + return url && Url.parse(url) != null; + } +} + +const receiptReceivedHandlerFactory = { + create(): ReceiptReceivedHandler { + let logger: ILogger= Log.logger; + let serializer = new ReceiptFileSerializer(logger); + return new ReceiptReceivedHandler(serializer, logger); + } +} + +export { ReceiptReceivedHandler, receiptReceivedHandlerFactory } \ No newline at end of file diff --git a/src/lib/sales/distributor/batchFile.ts b/src/lib/sales/distributor/batchFile.ts new file mode 100644 index 0000000..314e9e9 --- /dev/null +++ b/src/lib/sales/distributor/batchFile.ts @@ -0,0 +1,28 @@ +import { EventGridEvent } from "@lib/core/eventGridEvent"; +import { BatchFileTypeParser } from "./parsers/batchFileTypeParser"; +import { BatchFileIdParser } from "./parsers/batchFileIdParser"; + +export class BatchFile { + id: string; + fileType: string; + url: string; + + constructor(id: string, fileType: string, url: string) { + this.id = id; + this.fileType = fileType; + this.url = url; + } + + /** + * Factory function for creating a batch file + * @param message + */ + static create(message: EventGridEvent): BatchFile { + let blobPath = message.subject; + let batchId: string = BatchFileIdParser.parse(blobPath); + let batchType = BatchFileTypeParser.parse(blobPath); + let url: string = message.data.url; + + return new BatchFile(batchId, batchType, url) + } +} \ No newline at end of file diff --git a/src/lib/sales/distributor/batchFileType.ts b/src/lib/sales/distributor/batchFileType.ts new file mode 100644 index 0000000..9dc1928 --- /dev/null +++ b/src/lib/sales/distributor/batchFileType.ts @@ -0,0 +1,9 @@ + +/** + * The type of batch files + */ +export enum BatchFileType { + Header = "OrderHeaderDetails", + LineItems = "OrderLineItems", + ProductInformation = "ProductInformation" +} \ No newline at end of file diff --git a/src/lib/sales/distributor/combineOrder/request.ts b/src/lib/sales/distributor/combineOrder/request.ts new file mode 100644 index 0000000..0d8782c --- /dev/null +++ b/src/lib/sales/distributor/combineOrder/request.ts @@ -0,0 +1,6 @@ + + export interface ICombineOrderRequest { + orderHeaderDetailsCSVUrl: string; + orderLineItemsCSVUrl: string; + productInformationCSVUrl: string; + } \ No newline at end of file diff --git a/src/lib/sales/distributor/combineOrder/response.ts b/src/lib/sales/distributor/combineOrder/response.ts new file mode 100644 index 0000000..d6ae51d --- /dev/null +++ b/src/lib/sales/distributor/combineOrder/response.ts @@ -0,0 +1,29 @@ + +export interface IHeaders { + salesNumber: string; + dateTime: string; + locationId: string; + locationName: string; + locationAddress: string; + locationPostcode: string; + totalCost: string; + totalTax: string; +} + +export interface IDetail { + productId: string; + quantity: string; + unitCost: string; + totalCost: string; + totalTax: string; + productName: string; + productDescription: string; +} + +/** + * The response item from the combine order API + */ +export interface IResponseItem { + headers: IHeaders; + details: IDetail[]; +} \ No newline at end of file diff --git a/src/lib/sales/distributor/combineOrder/service.ts b/src/lib/sales/distributor/combineOrder/service.ts new file mode 100644 index 0000000..9fc6007 --- /dev/null +++ b/src/lib/sales/distributor/combineOrder/service.ts @@ -0,0 +1,36 @@ +import { ICombineOrderRequest } from "./request"; +import { IHttpClient, httpClientFactory } from "@lib/core/httpClient"; +import { IResponseItem } from "./response"; +import { OrderSource } from "@lib/sales/orderSource"; +import { IOrder } from "@lib/sales/order"; + +class CombineOrderService { + private readonly client: IHttpClient; + + constructor(client: IHttpClient) { + this.client = client; + } + + async combine(request: ICombineOrderRequest): Promise { + let response = await this.client.post("api/order/combineOrderContent", request); + let orders = response.map(item => { + header: item.headers, + details: item.details, + source: OrderSource.Distributor + }); + + return orders; + } +} + +class CombineOrderServiceFactory { + private static baseUrl: string = "https://serverlessohmanagementapi.trafficmanager.net/"; + + create(): CombineOrderService { + let client = httpClientFactory.create(CombineOrderServiceFactory.baseUrl); + return new CombineOrderService(client); + } +} +const combineOrderServiceFactory = new CombineOrderServiceFactory(); + +export { CombineOrderService, combineOrderServiceFactory } \ No newline at end of file diff --git a/src/lib/sales/distributor/distributorOrderMessage.ts b/src/lib/sales/distributor/distributorOrderMessage.ts new file mode 100644 index 0000000..08c6dab --- /dev/null +++ b/src/lib/sales/distributor/distributorOrderMessage.ts @@ -0,0 +1,21 @@ +import { BatchFile } from "./batchFile"; +import { IMessage } from "@lib/core/message" +import { EventGridEvent } from "@lib/core/eventGridEvent"; + +/** + * Distributor Order Message + */ +class DistributorOrderMessage implements IMessage { + id: string; // the event grid message id + batchFile: BatchFile; +} + +const distributorOrderMessageFactory = function(eventGridEvent: EventGridEvent): DistributorOrderMessage { + let message = new DistributorOrderMessage(); + message.id = eventGridEvent.id; + message.batchFile = BatchFile.create(eventGridEvent); + + return message; +} + +export { DistributorOrderMessage, distributorOrderMessageFactory } \ No newline at end of file diff --git a/src/lib/sales/distributor/entityIdFactory.ts b/src/lib/sales/distributor/entityIdFactory.ts new file mode 100644 index 0000000..c99860d --- /dev/null +++ b/src/lib/sales/distributor/entityIdFactory.ts @@ -0,0 +1,20 @@ +import { EventGridEvent } from "@lib/core/eventGridEvent"; +import { EntityId } from "durable-functions"; +import { DistributorOrderSagaData } from "./sagaData"; + +/** + * Creates an entity id for the saga (e.g. the "durable entity function") to get|set it's state data + * @param message The event grid message that is received when a batch file is received from a distributor + */ +const entityIdFactory = function(message: EventGridEvent): EntityId { + let getBatchId = function() { + const regex = /(?!\/)([0-9])\w+/; + let result = regex.exec(message.subject); + return result[0]; + } + + let batchId = getBatchId(); + return new EntityId(DistributorOrderSagaData.typeName, batchId); +} + +export default entityIdFactory; \ No newline at end of file diff --git a/src/lib/sales/distributor/parsers/batchFileIdParser.ts b/src/lib/sales/distributor/parsers/batchFileIdParser.ts new file mode 100644 index 0000000..935a3cc --- /dev/null +++ b/src/lib/sales/distributor/parsers/batchFileIdParser.ts @@ -0,0 +1,9 @@ + + +export class BatchFileIdParser { + static parse(input: string): string { + const regex = /(?!\/)([0-9])\w+/; + let result = regex.exec(input); + return result[0]; + } +} diff --git a/src/lib/sales/distributor/parsers/batchFileTypeParser.ts b/src/lib/sales/distributor/parsers/batchFileTypeParser.ts new file mode 100644 index 0000000..db17ff2 --- /dev/null +++ b/src/lib/sales/distributor/parsers/batchFileTypeParser.ts @@ -0,0 +1,8 @@ + +export class BatchFileTypeParser { + static parse(input: string): string { + const regex = /([aA-zZ]\w+)(?=\.csv)/; + let result = regex.exec(input); + return result[0]; + } +} \ No newline at end of file diff --git a/src/lib/sales/distributor/saga.ts b/src/lib/sales/distributor/saga.ts new file mode 100644 index 0000000..c613a5b --- /dev/null +++ b/src/lib/sales/distributor/saga.ts @@ -0,0 +1,131 @@ +import { DistributorOrderSagaData } from "./sagaData"; +import { DistributorOrderMessage } from "./distributorOrderMessage"; +import { EntityId } from "durable-functions/lib/src/classes"; +import { IOrder } from "../order"; +import { CombineOrderService, combineOrderServiceFactory } from "./combineOrder/service"; +import { ICombineOrderRequest } from "./combineOrder/request"; +import { BatchFileType } from "./batchFileType"; +import { OrderService, orderServiceFactory } from "../orderService"; +import { ILogger, Log } from "@lib/core/log"; +import { Saga } from "@lib/core/saga"; +import { IProductPurchased } from "@lib/product/productPurchased"; +import { Guid } from "guid-typescript"; +import { busFactory, Bus } from "@lib/core/bus"; + +class DistributorOrderSaga extends Saga { + combineOrderService: CombineOrderService; + orderService: OrderService; + + constructor(data: DistributorOrderSagaData, combineOrderService: CombineOrderService, + orderService: OrderService, logger: ILogger) { + super(data, logger); + this.combineOrderService = combineOrderService; + this.orderService = orderService; + } + + async start(message: DistributorOrderMessage): Promise { + this.logger.log("DistributorOrderProcessor started for batch " + message.batchFile.id); + this.data.id = new EntityId(DistributorOrderSagaData.typeName, message.batchFile.id.toString()); + await this.handle(message); + } + + async handle(message: DistributorOrderMessage): Promise { + this.logger.log('Executing handle for batch:', message); + + this.updateData(message); + + if (this.isBatchOrderReadyToBeCombined()) { + this.logger.log("All files received for batch " + this.data.id.key); + + let orders = await this.combineBatchOrder(); + this.logger.log("Combined orders"); + this.data.isComplete = true; + + await this.saveOrders(orders); + await this.publishProductPurchasedEvents(orders); + + this.logger.log('Distributor order processing complete.') + } + } + + private async publishProductPurchasedEvents(orders: IOrder[]): Promise { + let events: IProductPurchased[] = []; + + for (let index = 0; index < orders.length; index++) { + const order = orders[index]; + + order.details.map(detail => { + const event: IProductPurchased = { + id: Guid.create().toString(), + timestamp: order.header.dateTime, + productId: detail.productId, + productName: detail.productName, + purchaseTotal: parseFloat(detail.totalCost), + quantity: parseFloat(detail.quantity), + source: order.source + }; + return event; + }).forEach(e => events.push(e)); + } + + let bus: Bus = busFactory.create(); + await bus.sendEvents("productpurchases", events); + } + + private updateData(message: DistributorOrderMessage) { + const url = message.batchFile.url; + switch (message.batchFile.fileType) { + case BatchFileType.Header: + this.data.orderHeaderDetailsReceived = true; + this.data.orderHeaderDetailsCSVUrl = url; + break; + case BatchFileType.LineItems: + this.data.orderLIneItemsReceived = true; + this.data.orderLineItemsCSVUrl = url; + break; + case BatchFileType.ProductInformation: + this.data.productInformationReceived = true; + this.data.productInformationCSVUrl = url; + default: + break; + } + } + + private async saveOrders(orders: IOrder[]): Promise { + this.logger.log("Saving orders to the database."); + + for (let index = 0; index < orders.length; index++) { + const order = orders[index]; + await this.orderService.save(order); + } + } + + private async combineBatchOrder(): Promise { + let request = { + orderHeaderDetailsCSVUrl: this.data.orderHeaderDetailsCSVUrl, + orderLineItemsCSVUrl: this.data.orderLineItemsCSVUrl, + productInformationCSVUrl: this.data.productInformationCSVUrl + }; + + this.logger.log("Combine Batch Order: ", request); + let orders = await this.combineOrderService.combine(request); + + return orders; + } + + private isBatchOrderReadyToBeCombined() { + return this.data.orderHeaderDetailsReceived && this.data.orderLIneItemsReceived && this.data.productInformationReceived; + } +} + +const distributorOrderSagaFactory = { + create(data: DistributorOrderSagaData) { + let combineOrderService = combineOrderServiceFactory.create(); + let orderService = orderServiceFactory.create(); + let logger = Log.logger; + + return new DistributorOrderSaga(data, combineOrderService, orderService, logger); + } +} + +export { DistributorOrderSaga, distributorOrderSagaFactory } diff --git a/src/lib/sales/distributor/sagaData.ts b/src/lib/sales/distributor/sagaData.ts new file mode 100644 index 0000000..1cba90a --- /dev/null +++ b/src/lib/sales/distributor/sagaData.ts @@ -0,0 +1,13 @@ +import { SagaData } from "@lib/core/sagaData"; + +export class DistributorOrderSagaData extends SagaData { + public static typeName: string = "distributorOrderHandler"; + + orderHeaderDetailsReceived: boolean; + orderLIneItemsReceived: boolean; + productInformationReceived: boolean; + + orderHeaderDetailsCSVUrl: string; + orderLineItemsCSVUrl: string; + productInformationCSVUrl: string; +} \ No newline at end of file diff --git a/src/lib/sales/order.ts b/src/lib/sales/order.ts new file mode 100644 index 0000000..8a80562 --- /dev/null +++ b/src/lib/sales/order.ts @@ -0,0 +1,28 @@ + +export interface IOrderHeader { + salesNumber: string; + dateTime: string; + locationId: string; + locationName: string; + locationAddress: string; + locationPostcode: string; + totalCost: string; + totalTax: string; + receiptUrl: string; +} + +export interface IOrderDetail { + productId: string; + quantity: string; + unitCost: string; + totalCost: string; + totalTax: string; + productName: string; + productDescription: string; +} + +export interface IOrder { + header: IOrderHeader; + details: IOrderDetail[]; + source: string; +} \ No newline at end of file diff --git a/src/lib/sales/orderService.ts b/src/lib/sales/orderService.ts new file mode 100644 index 0000000..9123091 --- /dev/null +++ b/src/lib/sales/orderService.ts @@ -0,0 +1,23 @@ +import { DataContext } from "@lib/core/dataContext" +import { IOrder } from "./order"; + +class OrderService { + private readonly dataContext: DataContext; + + constructor(dataContext: DataContext) { + this.dataContext = dataContext; + } + + public async save(order: IOrder): Promise { + await this.dataContext.create(order); + } +} + +const orderServiceFactory = { + create() { + let dataContext = new DataContext("orders"); + return new OrderService(dataContext); + } +} + +export { OrderService, orderServiceFactory } \ No newline at end of file diff --git a/src/lib/sales/orderSource.ts b/src/lib/sales/orderSource.ts new file mode 100644 index 0000000..3aac262 --- /dev/null +++ b/src/lib/sales/orderSource.ts @@ -0,0 +1,5 @@ + +export enum OrderSource { + PointOfSale = "pos", + Distributor = "distributor" +} diff --git a/src/lib/sales/pointOfSale/processor.ts b/src/lib/sales/pointOfSale/processor.ts new file mode 100644 index 0000000..f476d95 --- /dev/null +++ b/src/lib/sales/pointOfSale/processor.ts @@ -0,0 +1,89 @@ +import { ILogger, Log } from "@lib/core/log"; +import { OrderService, orderServiceFactory } from "../orderService"; +import { IOrder } from "../order"; +import { ReceiptReceived } from "@lib/receipts/receiptReceived"; +import { IProductPurchased } from "@lib/product/productPurchased"; +import { Guid } from "guid-typescript"; +import { Bus, busFactory } from "@lib/core/bus"; + + +class PosOrderProcessor { + private logger: ILogger; + private service: OrderService; + + constructor(service: OrderService, logger: ILogger) { + this.service = service; + this.logger = logger; + } + + async process(orders: IOrder[]): Promise { + let receiptsReceived: Array = await this.saveOrders(orders); + await this.publishProductPurchasedEvents(orders); + + return receiptsReceived; + } + + private async saveOrders(orders: IOrder[]): Promise { + let receiptsReceived: Array = []; + + for (let index = 0; index < orders.length; index++) { + const order = orders[index]; + + await this.service.save(order); + this.logger.log(`Processed order ${order.header.salesNumber}`); + + if (this.hasReceipt(order)) { + let event = this.mapToReceiptReceived(order); + receiptsReceived.push(event); + } + } + return receiptsReceived; + } + + private async publishProductPurchasedEvents(orders: IOrder[]): Promise { + let events: IProductPurchased[] = []; + + for (let index = 0; index < orders.length; index++) { + const order = orders[index]; + order.details.map(detail => { + const event: IProductPurchased = { + id: Guid.create().toString(), + timestamp: order.header.dateTime, + productId: detail.productId, + productName: detail.productName, + purchaseTotal: parseFloat(detail.totalCost), + quantity: parseFloat(detail.quantity), + source: order.source + }; + return event; + }).forEach(e => events.push(e)); + } + + let bus: Bus = busFactory.create(); + await bus.sendEvents("productPurchases", events); + } + + private mapToReceiptReceived(order: IOrder) { + return { + totalItems: order.details.length, + totalCost: parseFloat(order.header.totalCost), + salesNumber: order.header.salesNumber, + salesDate: order.header.dateTime, + storeLocation: order.header.locationId, + receiptUrl: order.header.receiptUrl + }; + } + + private hasReceipt(order: IOrder) { + return order.header.receiptUrl != undefined || order.header.receiptUrl != null + } +} + +const posOrderProcessorFactory = { + create() { + let service = orderServiceFactory.create(); + return new PosOrderProcessor(service, Log.logger); + } +} + +export { PosOrderProcessor, posOrderProcessorFactory } \ No newline at end of file diff --git a/src/lib/user/service.ts b/src/lib/user/service.ts new file mode 100644 index 0000000..6f104a6 --- /dev/null +++ b/src/lib/user/service.ts @@ -0,0 +1,32 @@ +import { Cache } from "@lib/core/cache" +import { ServiceBase } from "@lib/core/service" + +interface User { + userId: string, + userName: string, + fullName: string +} + +const cache: Cache = new Cache(); +const baseUrl: string = "https://serverlessohuser.trafficmanager.net/"; + +export class UserService extends ServiceBase { + constructor() { + super(cache, baseUrl); + } + + public async get(id: string): Promise { + let user = this.cache.get(id); + if (!user) { + user = await this.fetch("api/GetUser", { userId: id }); + this.cache.set(id, user); + } + return user; + } + + fillCache(): Promise { + this.isCacheFilled = true; + //do nothing, can't fill cache; + return; + } +} \ No newline at end of file diff --git a/src/lib/user/validator.ts b/src/lib/user/validator.ts new file mode 100644 index 0000000..9167974 --- /dev/null +++ b/src/lib/user/validator.ts @@ -0,0 +1,21 @@ +import { UserService } from "@lib/user/service" + +interface ValidationResult { + isValid: boolean, + reason?: string +} + +const userService = new UserService(); + +class UserValidator { + public async validate(userId: string): Promise { + let exists = (await userService.get(userId)) != undefined; + + return { + isValid: exists, + reason: exists ? "user id exists." : "user id not found." + } + } +} + +export { ValidationResult, UserValidator } \ No newline at end of file diff --git a/test/Business User - Rating API.postman_collection.json b/test/Business User - Rating API.postman_collection.json new file mode 100644 index 0000000..80d60c3 --- /dev/null +++ b/test/Business User - Rating API.postman_collection.json @@ -0,0 +1,53 @@ +{ + "info": { + "_postman_id": "6c1ad43e-0fbd-4bd4-a4ef-00c34a8bb123", + "name": "Business User - Rating API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Rating API - Get (Internal Biz)", + "request": { + "method": "GET", + "header": [ + { + "key": "Ocp-Apim-Subscription-Key", + "value": "bce502a4433a4d14ada6a83100041243", + "type": "text" + } + ], + "url": { + "raw": "https://bfyoc-apim-1.azure-api.net/api/rating/e5c11ee8-d0c6-89a9-8f0b-414885906c89", + "protocol": "https", + "host": [ + "bfyoc-apim-1", + "azure-api", + "net" + ], + "path": [ + "api", + "rating", + "e5c11ee8-d0c6-89a9-8f0b-414885906c89" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "key", + "value": "Ocp-Apim-Subscription-Key", + "type": "string" + }, + { + "key": "value", + "value": "bce502a4433a4d14ada6a83100041243", + "type": "string" + } + ] + }, + "protocolProfileBehavior": {} +} \ No newline at end of file diff --git a/test/OH-Serverless.postman_collection.json b/test/OH-Serverless.postman_collection.json new file mode 100644 index 0000000..9551a6e --- /dev/null +++ b/test/OH-Serverless.postman_collection.json @@ -0,0 +1,154 @@ +{ + "info": { + "_postman_id": "f763e691-bb1c-4db9-9d2a-1d5b2ae1fb5e", + "name": "OH-Serverless", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "BFYOC Logic App", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"productId\": \"103c9acc-cea5-4faa-99ab-fae98ecd3131\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://prod-18.eastus2.logic.azure.com:443/workflows/637bf76f2a194f0e9d82b8efeb62c3eb/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=3xpcbjr_ubrpw7m4NUMQaROfdKP274fBMOSbXXETkKI", + "protocol": "https", + "host": [ + "prod-18", + "eastus2", + "logic", + "azure", + "com" + ], + "port": "443", + "path": [ + "workflows", + "637bf76f2a194f0e9d82b8efeb62c3eb", + "triggers", + "manual", + "paths", + "invoke" + ], + "query": [ + { + "key": "api-version", + "value": "2016-10-01" + }, + { + "key": "sp", + "value": "%2Ftriggers%2Fmanual%2Frun" + }, + { + "key": "sv", + "value": "1.0" + }, + { + "key": "sig", + "value": "3xpcbjr_ubrpw7m4NUMQaROfdKP274fBMOSbXXETkKI" + } + ] + } + }, + "response": [] + }, + { + "name": "Func - QS", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://bfyoc-functions-24.azurewebsites.net/api/products/?productId=103c9acc-cea5-4faa-99ab-fae98ecd3131&code=eej/kC92V46AAwMIVtgZpIkD7YQ66Uuj08N0GOZ2KQIDaFEXbG/3zA==", + "protocol": "https", + "host": [ + "bfyoc-functions-24", + "azurewebsites", + "net" + ], + "path": [ + "api", + "products", + "" + ], + "query": [ + { + "key": "productId", + "value": "103c9acc-cea5-4faa-99ab-fae98ecd3131" + }, + { + "key": "code", + "value": "eej/kC92V46AAwMIVtgZpIkD7YQ66Uuj08N0GOZ2KQIDaFEXbG/3zA==" + } + ] + } + }, + "response": [] + }, + { + "name": "Func - Route", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://bfyoc-functions-24.azurewebsites.net/api/products/103c9acc-cea5-4faa-99ab-fae98ecd3131/?code=eej/kC92V46AAwMIVtgZpIkD7YQ66Uuj08N0GOZ2KQIDaFEXbG/3zA==", + "protocol": "https", + "host": [ + "bfyoc-functions-24", + "azurewebsites", + "net" + ], + "path": [ + "api", + "products", + "103c9acc-cea5-4faa-99ab-fae98ecd3131", + "" + ], + "query": [ + { + "key": "code", + "value": "eej/kC92V46AAwMIVtgZpIkD7YQ66Uuj08N0GOZ2KQIDaFEXbG/3zA==" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Rating (By Product ID)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://bfyoc-functionapp-1.azurewebsites.net/api/rating/e5c11ee8-d0c6-89a9-8f0b-414885906c89?code=vvRV84Ezp61I5vMH/Ga0p49qUZT0iJmyMmzPp9WtDpNYuGlsfzu/Iw==", + "protocol": "https", + "host": [ + "bfyoc-functionapp-1", + "azurewebsites", + "net" + ], + "path": [ + "api", + "rating", + "e5c11ee8-d0c6-89a9-8f0b-414885906c89" + ], + "query": [ + { + "key": "code", + "value": "vvRV84Ezp61I5vMH/Ga0p49qUZT0iJmyMmzPp9WtDpNYuGlsfzu/Iw==" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..33197fc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "@lib/*": ["lib/*"], + }, + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": "./src", + "sourceMap": true, + "strict": false, + "esModuleInterop": true + } +} \ No newline at end of file