diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..26786f9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9306c8a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Node Functions", + "type": "node", + "request": "attach", + "port": 9229, + "preLaunchTask": "func: host start" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f4ea398 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "azureFunctions.deploySubpath": "api", + "azureFunctions.postDeployTask": "npm install", + "azureFunctions.projectLanguage": "TypeScript", + "azureFunctions.projectRuntime": "~3", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "npm prune" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b7946b1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,43 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "command": "host start", + "problemMatcher": "$func-node-watch", + "isBackground": true, + "dependsOn": "npm build", + "options": { + "cwd": "${workspaceFolder}/api" + } + }, + { + "type": "shell", + "label": "npm build", + "command": "npm run build", + "dependsOn": "npm install", + "problemMatcher": "$tsc", + "options": { + "cwd": "${workspaceFolder}/api" + } + }, + { + "type": "shell", + "label": "npm install", + "command": "npm install", + "options": { + "cwd": "${workspaceFolder}/api" + } + }, + { + "type": "shell", + "label": "npm prune", + "command": "npm prune --production", + "dependsOn": "npm build", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/api" + } + } + ] +} diff --git a/DB_SETUP/CREATE_DATABASE.sh b/DB_SETUP/CREATE_DATABASE.sh index 2caf1dc..b8e275e 100755 --- a/DB_SETUP/CREATE_DATABASE.sh +++ b/DB_SETUP/CREATE_DATABASE.sh @@ -9,25 +9,5 @@ echo "Beginning database creation process..." groupName=$(az group list --query "[0].name" -o tsv) echo "Creating Cosmos DB database $accountName in Resource Group $groupName..." -echo "This can take up to 10 minutes. Feel free to continue with the Learn Module. Just make sure to keep this terminal running." -az cosmosdb create -n $accountName -g $groupName -o none - -echo "Creating 'tailwind' database in $accountName..." -az cosmosdb sql database create -a $accountName -g $groupName -n $databaseName -o none - -echo "Creating 'products' collection in 'tailwind' database..." -az cosmosdb sql container create -g $groupName -a $accountName -d $databaseName -n $containerName -p /brand/name -o none - -echo "Preparing to import data..." - -endpoint=https://$accountName.documents.azure.com:443 -key=$(az cosmosdb list-keys -g $groupName -n $accountName --query "primaryMasterKey" -o json) - -echo "Installing Node modules..." - -npm i --silent - -echo "Populating database..." -node ./POPULATE_DATABASE.js --endpoint $endpoint --key $key --databaseName $databaseName --containerName $containerName - -echo "Finished! Your database, $accountName, is now ready." +echo "This can take up to 10 minutes. Feel free to continue with the Learn Module." +az cosmosdb create -n $accountName -g $groupName -o none \ No newline at end of file diff --git a/DB_SETUP/GET_CONNECTION_STRING.sh b/DB_SETUP/GET_CONNECTION_STRING.sh new file mode 100755 index 0000000..31a3cb8 --- /dev/null +++ b/DB_SETUP/GET_CONNECTION_STRING.sh @@ -0,0 +1,30 @@ +databaseName=tailwind +containerName=products + +# Get the connection string +echo "Getting connection string. This might take up to two minutes as we prepare the database..." + +# Get the account name, which is randomized +accountName=$(az cosmosdb list --query "[0].name" -o tsv) + +# Get the group name, which is preassigned +groupName=$(az group list --query "[0].name" -o tsv) + +# Create the database +az cosmosdb sql database create -a $accountName -g $groupName -n $databaseName -o none + +# Add products data +az cosmosdb sql container create -g $groupName -a $accountName -d $databaseName -n $containerName -p /brand/name -o none + +endpoint=https://$accountName.documents.azure.com:443 +key=$(az cosmosdb keys list -g $groupName -n $accountName --type keys --query "primaryMasterKey" -o json) + +## silent npm install +npm install > "/dev/null" 2>&1 + +node ./POPULATE_DATABASE.js --endpoint $endpoint --key $key --databaseName $databaseName --containerName $containerName + +echo "This is your connection string. Copy it to your clipboard..." +az cosmosdb keys list -n $accountName -g $groupName --type connection-strings --query "connectionStrings[0].connectionString" -o tsv + + diff --git a/README.md b/README.md index 443a0bf..08092f3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ --- page_type: sample languages: -- javascript + - javascript products: -- azure + - azure description: "Products Manager application for Serverless API Learn Module" urlFragment: "mslearn-build-api-azure-functions" --- @@ -24,15 +24,15 @@ This is a sample web application frontend for the Serverless API Learn Module. Outline the file contents of the repository. It helps users navigate the codebase, build configuration and any related assets. -| File/folder | Description | -| ----------------- | ---------------------------------------------------------- | -| `frontend` | The frontend website for the Products Manager application. | -| `api` | An empty folder where the user will create the API project | -| `.gitignore` | Define what to ignore at commit time. | -| `CHANGELOG.md` | List of changes to the sample. | -| `CONTRIBUTING.md` | Guidelines for contributing to the sample. | -| `README.md` | This README file. | -| `LICENSE` | The license for the sample. | +| File/folder | Description | +| ----------------- | ----------------------------------------------------------------------------- | +| `frontend` | The frontend website for the Products Manager application. | +| `api` | A base Azure Functions project where the user will finish out the API project | +| `.gitignore` | Define what to ignore at commit time. | +| `CHANGELOG.md` | List of changes to the sample. | +| `CONTRIBUTING.md` | Guidelines for contributing to the sample. | +| `README.md` | This README file. | +| `LICENSE` | The license for the sample. | ## Prerequisites diff --git a/api/CreateProduct/function.json b/api/CreateProduct/function.json new file mode 100644 index 0000000..2fd3c83 --- /dev/null +++ b/api/CreateProduct/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/CreateProduct/index.js" +} diff --git a/api/CreateProduct/index.ts b/api/CreateProduct/index.ts new file mode 100644 index 0000000..f749572 --- /dev/null +++ b/api/CreateProduct/index.ts @@ -0,0 +1,21 @@ +import { AzureFunction, Context, HttpRequest } from "@azure/functions"; +import productsService from "../services/productsService"; + +const httpTrigger: AzureFunction = async function ( + context: Context, + req: HttpRequest, +): Promise { + let response; + + try { + const product = req.body; + const result = await productsService.create(product); + response = { body: result, status: 200 }; + } catch (err) { + response = { body: err.message, status: 500 }; + } + + context.res = response; +}; + +export default httpTrigger; diff --git a/api/CreateProduct/sample.dat b/api/CreateProduct/sample.dat new file mode 100644 index 0000000..2e60943 --- /dev/null +++ b/api/CreateProduct/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/api/DeleteProduct/function.json b/api/DeleteProduct/function.json new file mode 100644 index 0000000..aba36f4 --- /dev/null +++ b/api/DeleteProduct/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/DeleteProduct/index.js" +} diff --git a/api/DeleteProduct/index.ts b/api/DeleteProduct/index.ts new file mode 100644 index 0000000..65942b3 --- /dev/null +++ b/api/DeleteProduct/index.ts @@ -0,0 +1,22 @@ +import { AzureFunction, Context, HttpRequest } from "@azure/functions"; +import productsService from "../services/productsService"; + +const httpTrigger: AzureFunction = async function ( + context: Context, + req: HttpRequest, +): Promise { + let response; + + try { + const id = req.params.id; + const brand = req.body.brand; + const result = await productsService.delete(id, brand.name); + response = { body: result, status: 200 }; + } catch (err) { + response = { body: err.message, status: 500 }; + } + + context.res = response; +}; + +export default httpTrigger; diff --git a/api/DeleteProduct/sample.dat b/api/DeleteProduct/sample.dat new file mode 100644 index 0000000..2e60943 --- /dev/null +++ b/api/DeleteProduct/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/api/README.MD b/api/README.MD deleted file mode 100644 index 0f29f77..0000000 --- a/api/README.MD +++ /dev/null @@ -1 +0,0 @@ -API project will go in this directory. You will create it in a later exercise. diff --git a/api/UpdateProduct/function.json b/api/UpdateProduct/function.json new file mode 100644 index 0000000..7ec7f55 --- /dev/null +++ b/api/UpdateProduct/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/UpdateProduct/index.js" +} diff --git a/api/UpdateProduct/index.ts b/api/UpdateProduct/index.ts new file mode 100644 index 0000000..39a5794 --- /dev/null +++ b/api/UpdateProduct/index.ts @@ -0,0 +1,21 @@ +import { AzureFunction, Context, HttpRequest } from "@azure/functions"; +import productsService from "../services/productsService"; + +const httpTrigger: AzureFunction = async function ( + context: Context, + req: HttpRequest, +): Promise { + let response; + + try { + const product = req.body; + const result = await productsService.update(product); + response = { body: result, status: 200 }; + } catch (err) { + response = { body: err.message, status: 500 }; + } + + context.res = response; +}; + +export default httpTrigger; diff --git a/api/UpdateProduct/sample.dat b/api/UpdateProduct/sample.dat new file mode 100644 index 0000000..2e60943 --- /dev/null +++ b/api/UpdateProduct/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..405a04c --- /dev/null +++ b/api/package.json @@ -0,0 +1,20 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "prestart": "npm run build", + "start": "func start", + "test": "echo \"No tests yet...\"" + }, + "dependencies": { + "@azure/cosmos": "^3.9.3" + }, + "devDependencies": { + "@azure/functions": "^1.0.2-beta2", + "@types/node": "^14.14.10", + "typescript": "^3.3.3" + } +} diff --git a/api/services/productsService.ts b/api/services/productsService.ts new file mode 100644 index 0000000..9ca5d23 --- /dev/null +++ b/api/services/productsService.ts @@ -0,0 +1,40 @@ +import { CosmosClient } from "@azure/cosmos"; + +// Set connection string from CONNECTION_STRING value in local.settings.json +const CONNECTION_STRING = process.env.CONNECTION_STRING; + +const productService = { + init() { + try { + this.client = new CosmosClient(CONNECTION_STRING); + this.database = this.client.database("tailwind"); + this.container = this.database.container("products"); + } catch (err) { + console.log(err.message); + } + }, + async create(productToCreate) { + const { resource } = await this.container.items.create(productToCreate); + return resource; + }, + async read(): Promise { + const iterator = this.container.items.readAll(); + const { resources } = await iterator.fetchAll(); + return JSON.stringify(resources); + }, + async update(product) { + const { resource } = await this.container.item( + product.id, + product.brand.name, + ) + .replace(product); + return resource; + }, + async delete(id, brandName) { + const result = await this.container.item(id, brandName).delete(); + }, +}; + +productService.init(); + +export default productService; diff --git a/frontend/index.js b/frontend/index.js index 7c94788..ac63547 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -1,9 +1,5 @@ -(function() { +(function () { const API = "http://127.0.0.1:7071/api"; - const KEY = ""; - - // axios defaults - axios.defaults.headers.common["x-functions-key"] = KEY; new Vue({ el: "#app", @@ -25,10 +21,10 @@ getProducts() { this.products = axios .get(`${API}/products`) - .then(response => { + .then((response) => { this.products = response.data; }) - .catch(err => { + .catch((err) => { this.showError("Get", err.message); }); }, @@ -38,18 +34,18 @@ .then(() => { this.showSuccess("Item updated"); }) - .catch(err => { + .catch((err) => { this.showError("Update", err.message); }); }, createProduct() { axios .post(`${API}/product`, this.newProduct) - .then(item => { + .then((item) => { this.products.push(item.data); this.showSuccess("Item created"); }) - .catch(err => { + .catch((err) => { this.showError("Create", err.message); }) .finally(() => { @@ -70,7 +66,7 @@ this.products.splice(index, 1); this.showSuccess("Item deleted"); }) - .catch(err => { + .catch((err) => { this.showError("Delete", err.message); }); }, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..48e341a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +}