diff --git a/.docsettings.yml b/.docsettings.yml index 8c9790f94e47..d5509adf2ccb 100644 --- a/.docsettings.yml +++ b/.docsettings.yml @@ -19,7 +19,8 @@ required_readme_sections: known_content_issues: - ["README.md", "#1583"] - ["sdk/template/template/README.md", "#1583"] - - ["sdk/applicationinsights/applicationinsights-query/README.md", "#1583"] + - ["sdk/appconfiguration/app-configuration/README.md", "#1583"] + - ["sdk/applicationinsights/applicationinsights-query/README.md", "#1583"] - ["sdk/batch/batch/README.md", "#1583"] - [ "sdk/cognitiveservices/cognitiveservices-anomalydetector/README.md", diff --git a/sdk/appconfiguration/app-configuration/README.md b/sdk/appconfiguration/app-configuration/README.md index 0a4e89bfd7c4..bbc96dccd247 100644 --- a/sdk/appconfiguration/app-configuration/README.md +++ b/sdk/appconfiguration/app-configuration/README.md @@ -1,115 +1,98 @@ # Azure App Configuration client library for JS -This package contains an isomorphic SDK for ConfigurationClient. +Azure App Configuration is a managed service that helps developers centralize their application configurations simply and securely. + +Modern programs, especially programs running in a cloud, generally have many components that are distributed in nature. Spreading configuration settings across these components can lead to hard-to-troubleshoot errors during an application deployment. Use App Configuration to securely store all the settings for your application in one place. + +Use the client library for App Configuration to: + +* Create centrally stored application configuration settings +* Retrieve settings +* Update settings +* Delete settings + +[NPM](https://www.npmjs.com/package/@azure/app-configuration) | [Product documentation](https://docs.microsoft.com/en-us/azure/azure-app-configuration/) ## Getting started ### Currently supported environments -- Node.js version 6.x.x or higher -- Browser JavaScript +- Node.js version 8.x.x or higher ### How to Install ```bash -npm install @azure/app-config +npm install @azure/app-configuration ``` + ## Key concepts -### How to use +### Configuration Setting -#### nodejs - Authentication, client creation and listConfigurationSettings as an example written in TypeScript. +A Configuration Setting is the fundamental resource within a Configuration Store. +In its simplest form, it is a key and a value. However, there are additional properties such as +the modifiable content type and tags fields that allows the value to be interpreted or associated +in different ways. -##### Install @azure/ms-rest-nodeauth +The `label` property of a Configuration Setting provides a way to separate configuration settings +into different dimensions. These dimensions are user defined and can take any form. Some common +examples of dimensions to use for a label include regions, semantic versions, or environments. +Many applications have a required set of configuration keys that have varying values as the +application exists across different dimensions. -```bash -npm install @azure/ms-rest-nodeauth -``` +For example, MaxRequests may be 100 in "NorthAmerica", and 200 in "WestEurope". By creating a +Configuration Setting named MaxRequests with a label of "NorthAmerica" and another, only with +a different value, in the "WestEurope" label, an application can seamlessly retrieve +Configuration Settings as it runs in these two dimensions. ## Examples +#### nodejs - Authentication, client creation and listConfigurationSettings as an example written in TypeScript. + ##### Sample code ```typescript -import * as coreHttp from "@azure/core-http"; -import * as coreArm from "@azure/core-arm"; -import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; -import { ConfigurationClient, ConfigurationModels, ConfigurationMappers } from "@azure/app-config"; -const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; - -msRestNodeAuth.interactiveLogin().then((creds) => { - const client = new ConfigurationClient(creds, subscriptionId); - const label = ["testlabel"]; - const key = ["testkey"]; - const acceptDateTime = new Date().toISOString(); - const fields = ["etag"]; - client.listConfigurationSettings(label, key, acceptDateTime, fields).then((result) => { - console.log("The result is:"); - console.log(result); - }); -}).catch((err) => { - console.error(err); -}); -``` +import { AppConfigurationClient } from "@azure/app-configuration"; -#### browser - Authentication, client creation and listConfigurationSettings as an example written in JavaScript. +const connectionString = process.env["AZ_CONFIG_CONNECTION"]!; +const client = new AppConfigurationClient(connectionString); -##### Install @azure/ms-rest-browserauth +let configurationSetting = await client.getConfigurationSetting("testkey"); -```bash -npm install @azure/ms-rest-browserauth +console.log("The result is:"); +console.log(configurationSetting.value); ``` -##### Sample code +More examples can be found in the samples folder on [github](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/appconfiguration/app-configuration/samples) -See https://github.com/Azure/ms-rest-browserauth to learn how to authenticate to Azure in the browser. - -- index.html -```html - - - - @azure/app-configuration sample - - - - - - - - -``` +## Next steps -## Troubleshooting +Explore the samples to understand how to work with Azure App Configuration. -## Next steps +* [`helloworld.ts`](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/appconfiguration/app-configuration/samples/helloworld.ts) - getting, setting and deleting configuration values +* [`helloworldWithLabels.ts`](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/appconfiguration/app-configuration/samples/helloworldWithLabels.ts) - using labels to add additional dimensions to your settings +* [`helloworldWithETag.ts`](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/appconfiguration/app-configuration/samples/helloworldWithETag.ts) - setting values using etags to prevent accidental overwrites ## Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/master/CONTRIBUTING.md) to learn more about how to build and test the code. + +This module's tests are live tests, which require you to have an Azure App Configuration instance. To execute the tests +you'll need to run: +1. `rush update` +2. `rush build` +3. `npm run test`. + +View our tests ([index.spec.ts](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/appconfiguration/app-configuration/test/index.spec.ts)) for more details. + ## Related projects - [Microsoft Azure SDK for Javascript](https://github.com/Azure/azure-sdk-for-js) diff --git a/sdk/appconfiguration/app-configuration/samples/helloworld.ts b/sdk/appconfiguration/app-configuration/samples/helloworld.ts new file mode 100644 index 000000000000..971673ba37d6 --- /dev/null +++ b/sdk/appconfiguration/app-configuration/samples/helloworld.ts @@ -0,0 +1,50 @@ +// NOTE: replace with import { AppConfigurationClient } from "@azure/app-configuration" +// in a standalone project +import { AppConfigurationClient } from "../src" + +export async function run() { + console.log("Running helloworld sample"); + + // You will need to set this environment variable + const connectionString = process.env["AZ_CONFIG_CONNECTION"]!; + const client = new AppConfigurationClient(connectionString); + + const greetingKey = "Samples:Greeting"; + + await cleanupSampleValues([greetingKey], client); + + // creating a new setting + console.log(`Adding in new setting ${greetingKey}`); + await client.addConfigurationSetting(greetingKey, { value: "Hello!" }); + + const newSetting = await client.getConfigurationSetting(greetingKey); + console.log(`${greetingKey} has been set to ${newSetting.value}`); + + // changing the value of a setting + await client.setConfigurationSetting(greetingKey, { value: "Goodbye!" }); + + const updatedSetting = await client.getConfigurationSetting(greetingKey); + console.log(`${greetingKey} has been set to ${updatedSetting.value}`); + + // removing the setting + await client.deleteConfigurationSetting(greetingKey, {}); + console.log(`${greetingKey} has been deleted`); + + await cleanupSampleValues([greetingKey], client); +} + +async function cleanupSampleValues(keys: string[], client: AppConfigurationClient) { + const existingSettings = await client.listConfigurationSettings({ + key: keys + }); + + for (const setting of existingSettings) { + await client.deleteConfigurationSetting(setting.key!, { label: setting.label }); + } +} + +// If you want to run this sample from a console +// uncomment these lines so run() will get called +// run().catch(err => { +// console.log(`ERROR: ${err}`); +// }); \ No newline at end of file diff --git a/sdk/appconfiguration/app-configuration/samples/helloworldWithETag.ts b/sdk/appconfiguration/app-configuration/samples/helloworldWithETag.ts new file mode 100644 index 000000000000..2ab39158dc33 --- /dev/null +++ b/sdk/appconfiguration/app-configuration/samples/helloworldWithETag.ts @@ -0,0 +1,88 @@ +// NOTE: replace with import { AppConfigurationClient } from "@azure/app-configuration" +// in a standalone project +import { AppConfigurationClient } from "../src" + +export async function run() { + console.log("Running helloworld sample using etags"); + + // You will need to set this environment variable + const connectionString = process.env["AZ_CONFIG_CONNECTION"]!; + const client = new AppConfigurationClient(connectionString); + + const greetingKey = "Samples:Greeting"; + + await cleanupSampleValues([greetingKey], client); + + // create a new setting as Client Alpha + console.log(`Client Alpha: adding in new setting ${greetingKey}`); + let initialSettingFromClientA = await client.addConfigurationSetting(greetingKey, { value: "Created for Client Alpha" }); + console.log(`Client Alpha: ${greetingKey} has been set to '${initialSettingFromClientA.value}' with etag of ${initialSettingFromClientA.etag}`); + + // Each setting, when added, will come back with an etag (https://en.wikipedia.org/wiki/HTTP_ETag) + // This allows you to update a value but only if it hasn't changed from the last time you read it. + // + // Let's simulate two processes attempting to update the value + + // if you don't specify an etag the update is unconditional + let updateFromClientBeta = await client.setConfigurationSetting(greetingKey, { + value: "Update from Client Beta" + }); + + console.log(`Client Beta: updated the value of ${greetingKey} without specifying an etag (unconditional update).`); + console.log(` Client Beta's etag is ${updateFromClientBeta.etag}`); + console.log(` Client Alpha's etag from the initial creation is ${initialSettingFromClientA.etag}`); + + // at this point we've got this sequence of events + // + // 1. Client Alpha created the setting (and stored off its etag) + // 2. Client Beta updated the setting, ignoring the etag + + // Now Client Alpha wants to update the value _but_ Client Alpha will pay attention to the + // etag and only update the value if the value currently stored is the same as when we + // initially updated the setting. + // + // This allows us to prevent unintentional overwrites and allows you to implement + // optimistic concurrency (https://en.wikipedia.org/wiki/Optimistic_concurrency_control) + // within your application. + + console.log("Client Alpha: attempting update that doesn't include an etag"); + await client.setConfigurationSetting(greetingKey, { + value: "Update from Client Alpha that should only get set if the value has not changed from the last time we loaded it", + etag: initialSettingFromClientA.etag + }).catch(err => { + console.log(" Update failed - etag didn't match"); + }); + + // if we want to update then we need to retrieve the new setting and determine if our update makes sense + let actualStoredSetting = await client.getConfigurationSetting(greetingKey); + + console.log("Client Alpha: getting current value and merging/updating based on it") + // now we can figure out if we want to merge our value, overwrite with our value, etc... + // in this case we'll just update the value to what we want (again, specifying the etag to + // prevent unintended overwrite) + await client.updateConfigurationSetting(greetingKey, { + value: "Theoretical update from Client Alpha that takes Client Beta's changes into account", + etag: actualStoredSetting.etag + }); + + let currentSetting = await client.getConfigurationSetting(greetingKey); + console.log(`The value is now updated to '${currentSetting.value}'`); + + await cleanupSampleValues([greetingKey], client); +} + +async function cleanupSampleValues(keys: string[], client: AppConfigurationClient) { + const existingSettings = await client.listConfigurationSettings({ + key: keys + }); + + for (const setting of existingSettings) { + await client.deleteConfigurationSetting(setting.key!, { label: setting.label }); + } +} + +// If you want to run this sample from a console +// uncomment these lines so run() will get called +// run().catch(err => { +// console.log("ERROR", err); +// }); \ No newline at end of file diff --git a/sdk/appconfiguration/app-configuration/samples/helloworldWithLabels.ts b/sdk/appconfiguration/app-configuration/samples/helloworldWithLabels.ts new file mode 100644 index 000000000000..359dc7c37e05 --- /dev/null +++ b/sdk/appconfiguration/app-configuration/samples/helloworldWithLabels.ts @@ -0,0 +1,45 @@ +// NOTE: replace with import { AppConfigurationClient } from "@azure/app-configuration" +// in a standalone project +import { AppConfigurationClient } from "../src" + +export async function run() { + console.log("Running helloworldWithLabels sample"); + + // You will need to set this environment variable + const connectionString = process.env["AZ_CONFIG_CONNECTION"]!; + const client = new AppConfigurationClient(connectionString); + + const urlKey = "Samples:Endpoint:Url"; + + await cleanupSampleValues([urlKey], client); + + // labels allow you to use the same key with different values for separate environments + // or clients + console.log("Adding in endpoint with two labels - beta and production"); + await client.addConfigurationSetting(urlKey, { label: "beta", value: "https://beta.example.com" }); + await client.addConfigurationSetting(urlKey, { label: "production", value: "https://example.com" }); + + const betaEndpoint = await client.getConfigurationSetting(urlKey, { label: "beta" }); + console.log(`Endpoint with beta label: ${betaEndpoint.value}`); + + const productionEndpoint = await client.getConfigurationSetting(urlKey, { label: "production" }); + console.log(`Endpoint with production label: ${productionEndpoint.value}`); + + await cleanupSampleValues([urlKey], client); +} + +async function cleanupSampleValues(keys: string[], client: AppConfigurationClient) { + const existingSettings = await client.listConfigurationSettings({ + key: keys + }); + + for (const setting of existingSettings) { + await client.deleteConfigurationSetting(setting.key!, { label: setting.label }); + } +} + +// If you want to run this sample from a console +// uncomment these lines so run() will get called +// run().catch(err => { +// console.log(`ERROR: ${err}`); +// }); \ No newline at end of file diff --git a/sdk/appconfiguration/app-configuration/samples/index.ts b/sdk/appconfiguration/app-configuration/samples/index.ts new file mode 100644 index 000000000000..d768fc0d0d6b --- /dev/null +++ b/sdk/appconfiguration/app-configuration/samples/index.ts @@ -0,0 +1,7 @@ +import * as helloworld from "./helloworld"; +import * as helloworldWithLabels from "./helloworldWithLabels"; + +export async function runAll() { + await helloworld.run(); + await helloworldWithLabels.run(); +} \ No newline at end of file diff --git a/sdk/appconfiguration/app-configuration/src/index.ts b/sdk/appconfiguration/app-configuration/src/index.ts index f1aa1b606567..bd815dcaa3ab 100644 --- a/sdk/appconfiguration/app-configuration/src/index.ts +++ b/sdk/appconfiguration/app-configuration/src/index.ts @@ -102,6 +102,12 @@ export class AppConfigurationClient { */ constructor(uri: string, credential: TokenCredential); constructor(uriOrConnectionString: string, credential?: TokenCredential) { + if (uriOrConnectionString == null) { + throw new Error( + "You must provide a connection string or the URL for your AppConfiguration instance" + ); + } + const regexMatch = uriOrConnectionString.match(ConnectionStringRegex); if (regexMatch) { const credential = new AppConfigCredential(regexMatch[2], regexMatch[3]); diff --git a/sdk/appconfiguration/app-configuration/test/index.spec.ts b/sdk/appconfiguration/app-configuration/test/index.spec.ts index aa80f2f00770..98ce524118ad 100644 --- a/sdk/appconfiguration/app-configuration/test/index.spec.ts +++ b/sdk/appconfiguration/app-configuration/test/index.spec.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as assert from "assert"; +import { getConnectionStringFromEnvironment } from "./testHelpers"; import * as dotenv from "dotenv"; import { AppConfigurationClient } from "../src"; @@ -9,14 +10,11 @@ dotenv.config(); describe("AppConfigurationClient", () => { const settings: Array<{ key: string; label?: string }> = []; - const connectionString: string = process.env["APPCONFIG_CONNECTION_STRING"]!; let client: AppConfigurationClient; before("validate environment variables", () => { - if (!connectionString) { - throw new Error("APPCONFIG_CONNECTION_STRING not defined."); - } + let connectionString = getConnectionStringFromEnvironment(); client = new AppConfigurationClient(connectionString); }); @@ -28,6 +26,7 @@ describe("AppConfigurationClient", () => { describe("constructor", () => { it("supports connection string", async () => { + const connectionString = getConnectionStringFromEnvironment(); const client = new AppConfigurationClient(connectionString); // make sure a service call succeeds await client.listConfigurationSettings(); diff --git a/sdk/appconfiguration/app-configuration/test/samples.spec.ts b/sdk/appconfiguration/app-configuration/test/samples.spec.ts new file mode 100644 index 000000000000..c594b699408a --- /dev/null +++ b/sdk/appconfiguration/app-configuration/test/samples.spec.ts @@ -0,0 +1,7 @@ +import { runAll } from "../samples"; + +describe("AppConfiguration samples", () => { + it("Make sure all the samples build and run", async () => { + await runAll(); + }); +}); \ No newline at end of file diff --git a/sdk/appconfiguration/app-configuration/test/testHelpers.ts b/sdk/appconfiguration/app-configuration/test/testHelpers.ts new file mode 100644 index 000000000000..3838664fe319 --- /dev/null +++ b/sdk/appconfiguration/app-configuration/test/testHelpers.ts @@ -0,0 +1,26 @@ +import { AppConfigurationClient } from "../src" + +// allow loading from a .env file as an alternative to defining the variable +// in the environment +import * as dotenv from "dotenv"; +dotenv.config(); + +export function getConnectionStringFromEnvironment() : string { + const connectionString = process.env["AZ_CONFIG_CONNECTION"]!; + + if (connectionString == null) { + throw Error(`No connection string in environment - set AZ_CONFIG_CONNECTION with a connection string for your AppConfiguration instance.`); + } + + return connectionString; +} + +export async function cleanupSampleValues(keys: string[], client: AppConfigurationClient) { + const existingSettings = await client.listConfigurationSettings({ + key: keys + }); + + for (const setting of existingSettings) { + await client.deleteConfigurationSetting(setting.key!, { label: setting.label }); + } +} \ No newline at end of file