javascript
31 TopicsCreate your own QA RAG Chatbot with LangChain.js + Azure OpenAI Service
Demo: Mpesa for Business Setup QA RAG Application In this tutorial we are going to build a Question-Answering RAG Chat Web App. We utilize Node.js and HTML, CSS, JS. We also incorporate Langchain.js + Azure OpenAI + MongoDB Vector Store (MongoDB Search Index). Get a quick look below. Note: Documents and illustrations shared here are for demo purposes only and Microsoft or its products are not part of Mpesa. The content demonstrated here should be used for educational purposes only. Additionally, all views shared here are solely mine. What you will need: An active Azure subscription, get Azure for Student for free or get started with Azure for 12 months free. VS Code Basic knowledge in JavaScript (not a must) Access to Azure OpenAI, click here if you don't have access. Create a MongoDB account (You can also use Azure Cosmos DB vector store) Setting Up the Project In order to build this project, you will have to fork this repository and clone it. GitHub Repository link: https://github.com/tiprock-network/azure-qa-rag-mpesa . Follow the steps highlighted in the README.md to setup the project under Setting Up the Node.js Application. Create Resources that you Need In order to do this, you will need to have Azure CLI or Azure Developer CLI installed in your computer. Go ahead and follow the steps indicated in the README.md to create Azure resources under Azure Resources Set Up with Azure CLI. You might want to use Azure CLI to login in differently use a code. Here's how you can do this. Instead of using az login. You can do az login --use-code-device OR you would prefer using Azure Developer CLI and execute this command instead azd auth login --use-device-code Remember to update the .env file with the values you have used to name Azure OpenAI instance, Azure models and even the API Keys you have obtained while creating your resources. Setting Up MongoDB After accessing you MongoDB account get the URI link to your database and add it to the .env file along with your database name and vector store collection name you specified while creating your indexes for a vector search. Running the Project In order to run this Node.js project you will need to start the project using the following command. npm run dev The Vector Store The vector store used in this project is MongoDB store where the word embeddings were stored in MongoDB. From the embeddings model instance we created on Azure AI Foundry we are able to create embeddings that can be stored in a vector store. The following code below shows our embeddings model instance. //create new embedding model instance const azOpenEmbedding = new AzureOpenAIEmbeddings({ azureADTokenProvider, azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME, azureOpenAIApiEmbeddingsDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_EMBEDDING_NAME, azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION, azureOpenAIBasePath: "https://eastus2.api.cognitive.microsoft.com/openai/deployments" }); The code in uploadDoc.js offers a simple way to do embeddings and store them to MongoDB. In this approach the text from the documents is loaded using the PDFLoader from Langchain community. The following code demonstrates how the embeddings are stored in the vector store. // Call the function and handle the result with await const storeToCosmosVectorStore = async () => { try { const documents = await returnSplittedContent() //create store instance const store = await MongoDBAtlasVectorSearch.fromDocuments( documents, azOpenEmbedding, { collection: vectorCollection, indexName: "myrag_index", textKey: "text", embeddingKey: "embedding", } ) if(!store){ console.log('Something wrong happened while creating store or getting store!') return false } console.log('Done creating/getting and uploading to store.') return true } catch (e) { console.log(`This error occurred: ${e}`) return false } } In this setup, Question Answering (QA) is achieved by integrating Azure OpenAI’s GPT-4o with MongoDB Vector Search through LangChain.js. The system processes user queries via an LLM (Large Language Model), which retrieves relevant information from a vectorized database, ensuring contextual and accurate responses. Azure OpenAI Embeddings convert text into dense vector representations, enabling semantic search within MongoDB. The LangChain RunnableSequence structures the retrieval and response generation workflow, while the StringOutputParser ensures proper text formatting. The most relevant code snippets to include are: AzureChatOpenAI instantiation, MongoDB connection setup, and the API endpoint handling QA queries using vector search and embeddings. There are some code snippets below to explain major parts of the code. Azure AI Chat Completion Model This is the model used in this implementation of RAG, where we use it as the model for chat completion. Below is a code snippet for it. const llm = new AzureChatOpenAI({ azTokenProvider, azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME, azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME, azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION }) Using a Runnable Sequence to give out Chat Output This shows how a runnable sequence can be used to give out a response given the particular output format/ output parser added on to the chain. //Stream response app.post(`${process.env.BASE_URL}/az-openai/runnable-sequence/stream/chat`, async (req,res) => { //check for human message const { chatMsg } = req.body if(!chatMsg) return res.status(201).json({ message:'Hey, you didn\'t send anything.' }) //put the code in an error-handler try{ //create a prompt template format template const prompt = ChatPromptTemplate.fromMessages( [ ["system", `You are a French-to-English translator that detects if a message isn't in French. If it's not, you respond, "This is not French." Otherwise, you translate it to English.`], ["human", `${chatMsg}`] ] ) //runnable chain const chain = RunnableSequence.from([prompt, llm, outPutParser]) //chain result let result_stream = await chain.stream() //set response headers res.setHeader('Content-Type','application/json') res.setHeader('Transfer-Encoding','chunked') //create readable stream const readable = Readable.from(result_stream) res.status(201).write(`{"message": "Successful translation.", "response": "`); readable.on('data', (chunk) => { // Convert chunk to string and write it res.write(`${chunk}`); }); readable.on('end', () => { // Close the JSON response properly res.write('" }'); res.end(); }); readable.on('error', (err) => { console.error("Stream error:", err); res.status(500).json({ message: "Translation failed.", error: err.message }); }); }catch(e){ //deliver a 500 error response return res.status(500).json( { message:'Failed to send request.', error:e } ) } }) To run the front end of the code, go to your BASE_URL with the port given. This enables you to run the chatbot above and achieve similar results. The chatbot is basically HTML+CSS+JS. Where JavaScript is mainly used with fetch API to get a response. Thanks for reading. I hope you play around with the code and learn some new things. Additional Reads Introduction to LangChain.js Create an FAQ Bot on Azure Build a basic chat app in Python using Azure AI Foundry SDK51Views0likes0CommentsSupercharge Your TypeScript Workflow: ESLint, Prettier, and Build Tools
Introduction TypeScript has become the go-to language for modern JavaScript developers, offering static typing, better tooling, and improved maintainability. But writing clean and efficient TypeScript isn’t just about knowing the syntax, it’s about using the right tools to enhance your workflow. In this blog, we’ll explore essential TypeScript tools like ESLint, Prettier, tsconfig settings, and VS Code extensions to help you write better code, catch errors early, and boost productivity. By the end, you’ll have a fully optimized TypeScript development environment with links to quality resources to deepen your knowledge. Why Tooling Matters in TypeScript Development Unlike JavaScript, TypeScript enforces static typing and requires compilation. This means proper tooling can: ✅ Catch syntax and type errors early. ✅ Ensure consistent formatting across your project. ✅ Improve code maintainability and collaboration. ✅ Enhance debugging and refactoring efficiency. Now, let’s dive into the must-have TypeScript tools for an optimal workflow! Setting Up ESLint for TypeScript 🛠 ESLint is a linter that helps catch errors, bad practices, and inconsistencies in your TypeScript code. Installing ESLint in a TypeScript Project First, install the required packages for ESLint, TypeScript, and our tooling: npm install --save-dev eslint @eslint/js typescript typescript-eslint Configuring ESLint Create an eslint.config.mjs file in your project root and populate it with the following: // TS-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, tseslint.configs.recommended, ); This setup: ✅ Uses TypeScript parser to understand TypeScript syntax. ✅ Enables recommended TypeScript rules to enforce best practices. ✅ Disables some strict rules for flexibility (can be adjusted later). Running ESLint To check your code for errors, run: pnpm eslint . 💡 Pro Tip: Add "lint": "pnpm eslint ." in package.json scripts to run ESLint easily with pnpm lint. ESLint will lint all TypeScript compatible files within the current folder, and will output the results to your terminal. Optimizing tsconfig.json for Better Type Safety TypeScript’s compiler settings (tsconfig.json) control how TypeScript checks and compiles your code. Recommended tsconfig.json Setup 💡 Pro Tip: Use tsc --noEmit to check for type errors without compiling the code. Debugging TypeScript in VS Code 🐞 VS Code provides built-in debugging for TypeScript. Setting Up Debugging Go to Run & Debug (Ctrl + Shift + D) → Click "Create a launch.json file". Select "Node.js". Modify .vscode/launch.json: Run the debugger by pressing F5. 💡 Pro Tip: Set breakpoints in .ts files and VS Code will map them correctly to .js files using source maps. Best VS Code Extensions for TypeScript Boost your productivity with these must-have extensions: ✅ Prettier ESLint TypeScript Formatter – Formats TypeScript code through Prettier, then through ESLint. ✅ Path Intellisense – Auto-suggests import paths. ✅ Error Lens – Highlights TypeScript errors inline. MS Learn Resources to Deepen Your Knowledge 📚 Here are some official Microsoft Learn resources to help you master TypeScript tooling: Using ESLint and Prettier in Visual Studio Code Linting JavaScript/Typescript in Visual Studio Getting Started with ESLint282Views0likes0CommentsWhy Every JavaScript Developer Should Try TypeScript
Introduction "Why did the JavaScript developer break up with TypeScript?" "Because they couldn’t handle the commitment!" As a student entrepreneur, you're constantly juggling coursework, projects, and maybe even a startup idea. You don’t have time to debug mysterious JavaScript errors at 2 AM. That's where TypeScript comes in helping you write cleaner, more reliable code so you can focus on building, not debugging. In this post, I’ll show you why TypeScript is a must-have skill for any student developer and how it can set your projects up for success. Overview of TypeScript JavaScript, the world's most-used programming language, powers cross-platform applications but wasn't designed for large-scale projects. It lacks some features needed for managing extensive codebases, making it challenging for IDEs. TypeScript overcomes these limitations while preserving JavaScript’s versatility, ensuring code runs seamlessly across platforms, browsers, and hosts. What is TypeScript? TypeScript is an open-source, strongly typed superset of JavaScript that compiles down to regular JavaScript. Created by Microsoft, it introduces static typing, interfaces, and modern JavaScript features, making it a favorite for both small projects and enterprise applications Why Should Student Entrepreneurs Care About TypeScript? TypeScript Saves You Time: You know that feeling when your JavaScript app breaks for no reason just before a hackathon deadline? TypeScript catches errors before your code even runs, so you don’t waste hours debugging. TypeScript Makes Your Code More Professional: If you're building a startup, investors and potential employers will look at your code. TypeScript makes your projects scalable, readable, and industry ready. TypeScript Helps You Learn Faster: As a student, you’re still learning. Typescripts autocomplete and type hints guide you, reducing the number of Google searches you need. For a beginner-friendly introduction to TypeScript, check out this MS Learn module: 🔗 Introduction to TypeScript Setting Up TypeScript in 5 Minutes Prerequisites Knowledge of JavaScript NodeJS Code editor Visual Studio Code Install TypeScript TypeScript is available as a package in the npm registry as typescript. To install the latest version of TypeScript: In the Command Prompt window, enter npm install -g typescript. npm install -g typescript Enter tsc to confirm that TypeScript is installed. If it was successfully installed, this command should show a list of compiler commands and options. Create a new TypeScript file Create a new folder in your desktop called “demo”, right-click on the folder icon and select open with vs code When vs code opens, click on add file icon and create new file “index.ts” Let’s write a simple function to add two numbers Compile a TypeScript file TypeScript is a strict superset of ECMAScript 2015 (ECMAScript 6 or ES6). All JavaScript code is also TypeScript code, and a TypeScript program can seamlessly consume JavaScript. You can convert a JavaScript file to a TypeScript file just by renaming the extension from .js to .ts. However, not all TypeScript code is JavaScript code. TypeScript adds new syntax to JavaScript, which makes the JavaScript easier to read and implements some features, such as static typing. You transform TypeScript code into JavaScript code by using the TypeScript compiler. You run the TypeScript compiler at the command prompt by using the tsc command. When you run tsc with no parameters, it compiles all the .ts files in the current folder and generates a .js file for each one. To compile our code, open command prompt in vs code and type tsc index.ts Notice that a new JavaScript file has been added, You might need to refresh the Explorer pane to view the file At the Terminal command prompt, enter node index.js. This command runs the JavaScript and displays the result in the console log. And that’s it! 🎉 Core TypeScript Features Every Developer Should Know Static Typing for Safer Code – TypeScript’s static typing prevents runtime errors by catching type mismatches at compile time, making code more reliable. This prevents unintended assignments like: Interfaces for Better Object Structures – Interfaces help define the structure of objects, ensuring consistency and maintainability across a codebase. Enums for Readable Constants – Enums define named constants, making code more readable and reducing the risk of using incorrect values. Generics for Reusable Code – Generics allow you to create flexible, type-safe functions and components that work with multiple data types. Type Assertions for Flexibility – Type assertions let you explicitly specify a value’s type when TypeScript cannot infer it correctly, enhancing type safety in dynamic scenarios. Conclusion: TypeScript is Your Superpower🚀 TypeScript is more than just a superset of JavaScript—it's a game-changer for developers, especially those working on large-scale projects or building career-defining applications. By introducing static typing, interfaces, Enums, generics, and type assertions, TypeScript helps eliminate common JavaScript pitfalls while maintaining flexibility. These features not only enhance code quality and maintainability but also improve collaboration among teams, ensuring that projects scale smoothly. Whether you're a student entrepreneur, a freelancer, or a professional developer, adopting TypeScript early will give you a competitive edge in the industry. Embracing TypeScript means writing safer, cleaner, and more efficient code without sacrificing JavaScript’s versatility. With its powerful developer tools and seamless integration with modern frameworks, TypeScript ensures that your code remains robust and adaptable to changing requirements. As the demand for TypeScript continues to grow, learning and using it in your projects will open new opportunities and set you apart in the ever-evolving world of web development. Read More And do more with Typescript Declare variables in Typescript TypeScript repository on GitHub TypeScript tutorial in Visual Studio Code Build JavaScript applications using TypeScript225Views2likes0CommentsGetting Started with Azure Cosmos DB SDK for TypeScript/JavaScript (4.2.0)
In this blog, we will walk through how to get started with Azure Cosmos DB SDK for TypeScript. Using the SDK, we'll cover how to set up a Cosmos DB client, interact with containers and items, and perform basic CRUD operations such as creating, updating, querying, and deleting items. By the end of this tutorial, you'll have a solid understanding of how to integrate Azure Cosmos DB into your TypeScript applications. What is an SDK? An SDK (Software Development Kit) is a collection of software development tools, libraries, and documentation that helps developers integrate and interact with a service or platform. In this case, the Azure Cosmos DB SDK for JavaScript/TypeScript provides a set of tools to interact with the Cosmos DB service, making it easier to perform operations like database and container management, data insertion, querying, and more. What is the Azure Cosmos DB Client Library for JavaScript/TypeScript? The Azure Cosmos DB Client Library for JavaScript/TypeScript is a package that allows developers to interact with Azure Cosmos DB through an easy-to-use API. It supports operations for creating databases, containers, and documents, as well as querying and updating documents. For our example, we will be using the SQL API, which is the most widely used API in Cosmos DB, and will show how to use the SDK for basic CRUD operations. To get started, make sure to install the SDK by running: npm i @azure/cosmos Prerequisites Before we can start interacting with Cosmos DB, we need to make sure we have the following prerequisites in place: 1. Azure Subscription You need an active Azure subscription. If you don’t have one, you can Sign up for a free Azure account, or Azure for students to get $100 azure credit. 2. Azure Cosmos DB Account To interact with Cosmos DB, you need to have a Azure Cosmos DB API account. Create one from the Azure Portal and keep the Endpoint URL and Primary Key handy. If you dont know how to do so check out this blog Getting started with Azure Cosmos Database (A Deep Dive) Overview of Cosmos Client Concepts Before diving into code, let's briefly go over the essential concepts you will interact with in Azure Cosmos DB. 1. Database A Database is a container for data in Cosmos DB. You can think of it as a high-level entity under which collections (containers) are stored. client.databases("<db id>") for creating new databases, and reading/querying all databases 2. Container A Container (formerly known as a collection) is a logical unit for storing items (documents). In Cosmos DB, each container is independent, and the items within a container are stored as JSON-like documents. Operations for reading, replacing, or deleting a specific, existing container by id. For creating new containers and reading/querying all containers; use .containers() ie .container(id).read() 3. Partition Key A Partition Key is used to distribute data across multiple physical partitions. When you insert data into a container, you must define a partition key. This helps Cosmos DB scale and optimize read and write operations. 4. Item An Item in Cosmos DB is a single piece of data that resides in a container. It is typically stored as a JSON document, and each item must have a unique ID and be associated with a partition key. Used to perform operations on a specific item. read method const { resource,statusCode } = await usersContainer.item(id, id).read<TUser>(); delete method const { statusCode } = await usersContainer.item(id, id).delete() 5. Items Items in Cosmos DB is used for Operations for creating new items and reading/querying all items. Used to perform operations on a many items. query method const { resources } = await usersContainer.items.query(querySpec).fetchAll(); read all items method const { resources} = await usersContainer.items.readAll<TUser[]>().fetchAll(); upsert (update or insert if item doesn't exist) const { resource } = await usersContainer.items.upsert<Partial<TUser>>(user); Environment Variables Setup Create a .env file in the root directory of your project with the following contents: NODE_ENV=DEVELOPMENT AZURE_COSMOS_DB_ENDPOINT=https://<your-cosmos-db-account>.documents.azure.com:443 AZURE_COSMOS_DB_KEY=<your-primary-key> AZURE_COSMOS_DB_DATABASE_NAME=<your-database-name> Let's set up a simple node app Initialize the Project Create a new directory for your project and navigate into it: mkdir simple-node-app cd simple-node-app Initialize a new Node.js project with default settings: npm init -y Install Dependencies Install the necessary dependencies for Express, TypeScript, and other tools: npm install @azure/cosmos zod uuid dotenv npm install --save-dev typescript rimraf tsx @types/node Configure TypeScript Create a tsconfig.json file in the root of your project with the following content: { "compilerOptions": { /* Base Options: */ "target": "es2022", "esModuleInterop": true, "skipLibCheck": true, "moduleResolution": "nodenext", "resolveJsonModule": true, /* Strictness */ "strict": true, "allowUnreachableCode": false, "noUnusedLocals": true, // "noUnusedParameters": true, "strictBindCallApply": true, /* If transpiling with TypeScript: */ "module": "NodeNext", "outDir": "dist", "rootDir": "./", "lib": [ "ES2022" ], }, "include": [ "./*.ts" ], "exclude": [ "node_modules", "dist" ] } Loading Environment Variables Create env.ts to securely and type safe load our env variables using zod add below code //./env.ts import dotenv from 'dotenv'; import { z } from 'zod'; // Load environment variables from .env file dotenv.config(); // Define the environment schema const EnvSchema = z.object({ // Node Server Configuration NODE_ENV: z.enum(['PRODUCTION', 'DEVELOPMENT']).default('DEVELOPMENT'), // CosmosDB Configuration AZURE_COSMOS_DB_ENDPOINT: z.string({ required_error: "AZURE_COSMOS_DB_ENDPOINT is required", invalid_type_error: "AZURE_COSMOS_DB_ENDPOINT must be a string", }), AZURE_COSMOS_DB_KEY: z.string({ required_error: "AZURE_COSMOS_DB_KEY is required", invalid_type_error: "AZURE_COSMOS_DB_KEY must be a string", }), AZURE_COSMOS_DB_DATABASE_NAME: z.string({ required_error: "AZURE_COSMOS_DB DB Name is required", invalid_type_error: "AZURE_COSMOS_DB must be a string", }), }); // Parse and validate the environment variables export const env = EnvSchema.parse(process.env); // Configuration object consolidating all settings const config = { nodeEnv: env.NODE_ENV, cosmos: { endpoint: env.AZURE_COSMOS_DB_ENDPOINT, key: env.AZURE_COSMOS_DB_KEY, database: env.AZURE_COSMOS_DB_DATABASE_NAME, containers: { users: 'usersContainer', }, }, }; export default config; Securely Setting Up Cosmos Client Instance To interact with Cosmos DB, we need to securely set up a CosmosClient instance. Here's how to initialize the client using environment variables for security. Create cosmosClient.ts and add below code // ./cosmosdb.config.ts import { PartitionKeyDefinitionVersion, PartitionKeyKind, Database, CosmosClient, Container, CosmosDbDiagnosticLevel, ErrorResponse, RestError, AbortError, TimeoutError } from '@azure/cosmos'; import config from './env'; let client: CosmosClient; let database: Database; let usersContainer: Container; async function initializeCosmosDB(): Promise<void> { try { // Create a new CosmosClient instance client = new CosmosClient({ endpoint: config.cosmos.endpoint, key: config.cosmos.key, diagnosticLevel: config.nodeEnv === 'PRODUCTION' ? CosmosDbDiagnosticLevel.info : CosmosDbDiagnosticLevel.debug }); // Create or get the database const { database: db } = await client.databases.createIfNotExists({ id: config.cosmos.database }); database = db; console.log(`Database '${config.cosmos.database}' initialized.`); // Initialize containers usersContainer = await createUsersContainer(); console.log('Cosmos DB initialized successfully.'); } catch (error: any) { return handleCosmosError(error); } } // Create the users container async function createUsersContainer(): Promise<Container> { const containerDefinition = { id: config.cosmos.containers.users, partitionKey: { paths: ['/id'], version: PartitionKeyDefinitionVersion.V2, kind: PartitionKeyKind.Hash, }, }; try { const { container } = await database.containers.createIfNotExists(containerDefinition); console.log(`'${container.id}' is ready.`); // const { container, diagnostics } = await database.containers.createIfNotExists(containerDefinition); // console.log(diagnostics.clientConfig) Contains aggregates diagnostic details for the client configuration // console.log(diagnostics.diagnosticNode) is intended for debugging non-production environments only return container; } catch (error: any) { return handleCosmosError(error); } } // Getter functions for containers function getUsersContainer(): Container { if (!usersContainer) { throw new Error('user container is not initialized.'); } return usersContainer; } const handleCosmosError = (error: any) => { if (error instanceof RestError) { throw new Error(`error: ${error.name}, message: ${error.message}`); } else if (error instanceof ErrorResponse) { throw new Error(`Error: ${error.message}, message: ${error.message}`); } else if (error instanceof AbortError) { throw new Error(error.message); } else if (error instanceof TimeoutError) { throw new Error(`TimeoutError code: ${error.code}, message: ${error.message}`); } else if (error.code === 409) { //if you try to create an item using an id that's already in use in your Cosmos DB database, a 409 error is returned throw new Error('Conflict occurred while creating an item using an existing ID.'); } else { console.log(JSON.stringify(error)); throw new Error('An error occurred while processing your request.'); } }; export { initializeCosmosDB, getUsersContainer, handleCosmosError }; This code is for initializing and interacting with Azure Cosmos DB using the Azure Cosmos SDK in a Node.js environment. Here's a brief and straightforward explanation of what each part does: Imports: The code imports several classes and enums from azure/cosmos that are needed to interact with Cosmos DB, like CosmosClient, Database, Container, and various error types. Variables: client, database, and usersContainer are declared to hold references to the Cosmos DB client, database, and a specific container for user data. initializeCosmosDB() function: Purpose: Initializes the Cosmos DB client, database, and container. Steps: Creates a new CosmosClient with credentials from the config (like endpoint, key, and diagnosticLevel). Attempts to create or retrieve a database (using createIfNotExists). Logs success and proceeds to initialize the usersContainer by calling createUsersContainer(). createUsersContainer() function: Purpose: Creates a container for storing user data in Cosmos DB with a partition key. Steps: Defines a partition key for the container (using /id as the partition key path). Attempts to create the container (or retrieves it if it already exists) with the given definition. Returns the container instance. getUsersContainer() function: Purpose: Returns the usersContainer object if it exists. Throws an error if the container is not initialized. handleCosmosError() function: Purpose: Handles errors thrown by Cosmos DB operations. Error Handling: It checks the type of error (e.g., RestError, ErrorResponse, AbortError, TimeoutError) and throws a formatted error message. Specifically handles conflict errors (HTTP 409) when attempting to create an item with an existing ID. Key Exported Functions: initializeCosmosDB: Initializes the Cosmos DB client and container. getUsersContainer: Returns the initialized users container. handleCosmosError: Custom error handler for Cosmos DB operations. Create User Schema This code defines data validation schemas using Zod, a TypeScript-first schema declaration and validation library. Create user.schema.ts and add below code // ./user.schema.ts import { z } from 'zod'; const coerceDate = z.preprocess((arg) => { if (typeof arg === 'string' || arg instanceof Date) { return new Date(arg); } else { return arg; } }, z.date()); export const userSchema = z.object({ id: z.string().uuid(), fullname: z.string(), email: z.string().email(), address: z.string(), createdAt: coerceDate.default(() => new Date()), }) const responseSchema = z.object({ statusCode: z.number(), message: z.string(), }) export type TResponse = z.infer<typeof responseSchema>; export type TUser = z.infer<typeof userSchema>; Here's a concise breakdown of the code: 1. coerceDate Schema: Purpose: This schema is designed to coerce a value into a Date object. How it works: z.preprocess() allows preprocessing of input before applying the base schema (z.date()). If the input is a string or an instance of Date, it converts it into a Date object. If the input is neither of these, it returns the original input without modification. Use: The coerceDate is used later in the userSchema to ensure that the createdAt field is always a valid Date object. 2. userSchema: Purpose: Defines the structure and validation rules for a user object. Fields: id: A required string that must be a valid UUID (z.string().uuid()). fullname: A required string. email: A required string that must be a valid email format (z.string().email()). address: A required string. createdAt: A Date field, which defaults to the current date/time if not provided (z.date() with default(() => new Date())), and uses coerceDate for preprocessing to ensure the value is a valid Date object. 3. responseSchema: Purpose: Defines the structure of a response object. Fields: statusCode: A required number (z.number()). message: A required string. 4. Type Inference: TResponse and TUser are TypeScript types that are automatically inferred from the responseSchema and userSchema, respectively. z.infer<typeof schema> generates TypeScript types based on the Zod schema, so: TResponse will be inferred as { statusCode: number, message: string }. TUser will be inferred as { id: string, fullname: string, email: string, address: string, createdAt: Date }. Let's implement Create Read Delete and Update Create user.service.ts and add the code below // ./user.service.ts import { SqlQuerySpec } from '@azure/cosmos'; import { getUsersContainer, handleCosmosError } from './cosmosClient'; import { TResponse, TUser } from './user.schema'; // Save user service export const saveUserService = async (user: TUser): Promise<Partial<TUser>> => { try { const usersContainer = getUsersContainer(); const res = await usersContainer.items.create<TUser>(user); if (!res.resource) { throw new Error('Failed to save user.'); } return res.resource; } catch (error: any) { return handleCosmosError(error); } }; // Update user service export const updateUserService = async (user: Partial<TUser>): Promise<Partial<TUser>> => { try { const usersContainer = getUsersContainer(); const { resource } = await usersContainer.items.upsert<Partial<TUser>>(user); if (!resource) { throw new Error('Failed to update user.'); } return resource; } catch (error: any) { return handleCosmosError(error); } }; // Fetch users service export const fetchUsersService = async (): Promise<TUser[] | null> => { try { const usersContainer = getUsersContainer(); const querySpec: SqlQuerySpec = { query: 'SELECT * FROM c ORDER BY c._ts DESC', }; const { resources } = await usersContainer.items.query<TUser[]>(querySpec).fetchAll(); return resources.flat(); } catch (error: any) { return handleCosmosError(error); } }; // Fetch user by email service export const fetchUserByEmailService = async (email: string): Promise<TUser | null> => { try { const usersContainer = getUsersContainer(); const querySpec: SqlQuerySpec = { query: 'SELECT * FROM c WHERE c.email = ', parameters: [ { name: '@email', value: email }, ], }; const { resources } = await usersContainer.items.query<TUser>(querySpec).fetchAll(); return resources.length > 0 ? resources[0] : null; } catch (error: any) { return handleCosmosError(error); } } // Fetch user by ID service export const fetchUserByIdService = async (id: string): Promise<TUser | null> => { try { const usersContainer = getUsersContainer(); const { resource } = await usersContainer.item(id, id).read<TUser>(); if (!resource) { return null; } return resource; } catch (error: any) { return handleCosmosError(error); } }; // Delete user by ID service export const deleteUserByIdService = async (id: string): Promise<TResponse> => { try { const usersContainer = getUsersContainer(); const userIsAvailable = fetchUserByIdService(id); if (!userIsAvailable) { throw new Error('User not found'); } const { statusCode } = await usersContainer.item(id, id).delete(); if (statusCode !== 204) { throw new Error('Failed to delete user.'); } return { statusCode, message: 'User deleted successfully', } } catch (error: any) { return handleCosmosError(error); } }; This code provides a set of service functions to interact with the Cosmos DB container for managing user data, including create, update, fetch, and delete operations. Here's a brief breakdown of each function: 1. saveUserService: Purpose: Saves a new user to the Cosmos DB container. How it works: Retrieves the usersContainer using getUsersContainer(). Uses items.create<TUser>(user) to create a new user document in the container. If the operation fails (i.e., no resource is returned), it throws an error. Returns the saved user object (with partial properties). Error Handling: Catches any error and passes it to handleCosmosError(). 2. updateUserService: Purpose: Updates an existing user in the Cosmos DB container. How it works: Retrieves the usersContainer. Uses items.upsert<Partial<TUser>>(user) to either insert or update the user data. If no resource is returned, an error is thrown. Returns the updated user object. Error Handling: Catches any error and passes it to handleCosmosError(). 3. fetchUsersService: Purpose: Fetches all users from the Cosmos DB container. How it works: Retrieves the usersContainer. Executes a SQL query (SELECT * FROM c ORDER BY c._ts DESC) to fetch all users ordered by timestamp (_ts). If the query is successful, it returns the list of users. If an error occurs, it is passed to handleCosmosError(). Return Type: Returns an array of TUser[] or null if no users are found. 4. fetchUserByEmailService: Purpose: Fetches a user by their email address. How it works: Retrieves the usersContainer. Executes a SQL query to search for a user by email (SELECT * FROM c WHERE c.email = Email). If the query finds a matching user, it returns the user object, otherwise returns null. Error Handling: Catches any error and passes it to handleCosmosError(). 5. fetchUserByIdService: Purpose: Fetches a user by their unique id. How it works: Retrieves the usersContainer. Uses item(id, id).read<TUser>() to read a user by its id. If no user is found, returns null. If the user is found, returns the user object. Error Handling: Catches any error and passes it to handleCosmosError(). 6. deleteUserByIdService: Purpose: Deletes a user by their unique id. How it works: Retrieves the usersContainer. Checks if the user exists by calling fetchUserByIdService(id). If the user is not found, throws an error. Deletes the user using item(id, id).delete(). Returns a response object with statusCode and a success message if the deletion is successful. Error Handling: Catches any error and passes it to handleCosmosError(). Summary of the Service Functions: Save a new user (saveUserService). Update an existing user (updateUserService). Fetch all users (fetchUsersService), a user by email (fetchUserByEmailService), or by id (fetchUserByIdService). Delete a user by id (deleteUserByIdService). Key Points: Upsert operation (upsert): If the user exists, it is updated; if not, it is created. Error Handling: All errors are passed to a centralized handleCosmosError() function, which ensures consistent error responses. Querying: Uses SQL-like queries in Cosmos DB to fetch users based on conditions (e.g., email or id). Type Safety: The services rely on the TUser and TResponse types from the schema, ensuring that the input and output adhere to the expected structure. This structure makes the service functions reusable and maintainable, while providing clean, type-safe interactions with the Azure Cosmos DB. Let's Create Server.ts Create server.ts and add the code below. //./server.ts import { initializeCosmosDB } from "./cosmosClient"; import { v4 as uuidv4 } from 'uuid'; import { TUser } from "./user.schema"; import { fetchUsersService, fetchUserByIdService, deleteUserByIdService, saveUserService, updateUserService, fetchUserByEmailService } from "./user.service"; // Start server (async () => { try { // Initialize CosmosDB await initializeCosmosDB(); // Create a new user const newUser: TUser = { id: uuidv4(), fullname: "John Doe", email: "john.doe@example.com", address: "Nairobi, Kenya", createdAt: new Date() }; const createdUser = await saveUserService(newUser); console.log('User created:', createdUser); // Fetch all users const users = await fetchUsersService(); console.log('Fetched users:', users); let userID = "81b4c47c-f222-487b-a5a1-805463c565a0"; // Fetch user by ID const user = await fetchUserByIdService(userID); console.log('Fetched user with ID:', user); //search for user by email const userByEmail = await fetchUserByEmailService("john.doe@example.com"); console.log('Fetched user with email:', userByEmail); // Update user const updatedUser = await updateUserService({ id: userID, fullname: "Jonathan Doe" }); console.log('User updated:', updatedUser); // Delete user const deleteResponse = await deleteUserByIdService(userID); console.log('Delete response:', deleteResponse); } catch (error: any) { console.error('Error:', error.message); } finally { process.exit(0); } })(); This server.ts file is the entry point of an application that interacts with Azure Cosmos DB to manage user data. It initializes the Cosmos DB connection and performs various CRUD operations (Create, Read, Update, Delete) on user records. Breakdown of the Code: 1. Imports: initializeCosmosDB: Initializes the Cosmos DB connection and sets up the database and container. uuidv4: Generates a unique identifier (UUID) for the id field of the user object. TUser: Type definition for a user, ensuring that the user object follows the correct structure (from user.schema.ts). Service Functions: These are the CRUD operations that interact with the Cosmos DB (fetchUsersService, fetchUserByIdService, etc.). 2. Asynchronous IIFE (Immediately Invoked Function Expression): The entire script runs inside an async IIFE, which is an asynchronous function that executes immediately when the file is run. 3. Workflow: Here’s what the script does step-by-step: Initialize Cosmos DB: The initializeCosmosDB() function is called to set up the connection to Cosmos DB. If the connection is successful, it logs Cosmos DB initialized. to the console. Create a New User: A new user is created with a unique ID (uuidv4()), full name, email, address, and a createdAt timestamp. The saveUserService(newUser) function is called to save the new user to the Cosmos DB container. If successful, the created user is logged to the console. Fetch All Users: The fetchUsersService() function is called to fetch all users from the Cosmos DB. The list of users is logged to the console. Fetch User by ID: The fetchUserByIdService(userID) function is called with a hardcoded userID to fetch a specific user by their unique ID. The user (if found) is logged to the console. Fetch User by Email: The fetchUserByEmailService(email) function is called to find a user by their email address ("john.doe@example.com"). The user (if found) is logged to the console. Update User: The updateUserService({ id: userID, fullname: "Jonathan Doe" }) function is called to update the user's full name. The updated user is logged to the console. Delete User: The deleteUserByIdService(userID) function is called to delete the user with the specified ID. The response from the deletion (status code and message) is logged to the console. 4. Error Handling: If any operation fails, the catch block catches the error and logs the error message to the console. This ensures that any issues (e.g., database connection failure, user not found, etc.) are reported. 5. Exit Process: After all operations are completed (or if an error occurs), the script exits the process with process.exit(0) to ensure the Node.js process terminates cleanly. Example Output: If everything runs successfully, the console output would look like this (assuming the hardcoded userID exists in the database and the operations succeed): Cosmos DB initialized. User created: { id: 'some-uuid', fullname: 'John Doe', email: 'john.doe@example.com', address: 'Nairobi, Kenya', createdAt: 2024-11-28T12:34:56.789Z } Fetched users: [{ id: 'some-uuid', fullname: 'John Doe', email: 'john.doe@example.com', address: 'Nairobi, Kenya', createdAt: 2024-11-28T12:34:56.789Z }] Fetched user with ID: { id: '81b4c47c-f222-487b-a5a1-805463c565a0', fullname: 'John Doe', email: 'john.doe@example.com', address: 'Nairobi, Kenya', createdAt: 2024-11-28T12:34:56.789Z } Fetched user with email: { id: 'some-uuid', fullname: 'John Doe', email: 'john.doe@example.com', address: 'Nairobi, Kenya', createdAt: 2024-11-28T12:34:56.789Z } User updated: { id: '81b4c47c-f222-487b-a5a1-805463c565a0', fullname: 'Jonathan Doe', email: 'john.doe@example.com', address: 'Nairobi, Kenya', createdAt: 2024-11-28T12:34:56.789Z } Delete response: { statusCode: 204, message: 'User deleted successfully' } Error Handling The SDK generates various types of errors that can occur during an operation. ErrorResponse is thrown if the response of an operation returns an error code of >=400. TimeoutError is thrown if Abort is called internally due to timeout. AbortError is thrown if any user passed signal caused the abort. RestError is thrown in case of failure of underlying system call due to network issues. Errors generated by any devDependencies. For Eg. azure/identity package could throw CredentialUnavailableError. Following is an example for handling errors of type ErrorResponse, TimeoutError, AbortError, and RestError. import { ErrorResponse, RestError, AbortError, TimeoutError } from '@azure/cosmos'; const handleCosmosError = (error: any) => { if (error instanceof RestError) { throw new Error(`error: ${error.name}, message: ${error.message}`); } else if (error instanceof ErrorResponse) { throw new Error(`Error: ${error.message}, message: ${error.message}`); } else if (error instanceof AbortError) { throw new Error(error.message); } else if (error instanceof TimeoutError) { throw new Error(`TimeoutError code: ${error.code}, message: ${error.message}`); } else if (error.code === 409) { //if you try to create an item using an id that's already in use in your Cosmos DB database, a 409 error is returned throw new Error('Conflict occurred while creating an item using an existing ID.'); } else { console.log(JSON.stringify(error)); throw new Error('An error occurred while processing your request.'); } }; Read More Quickstart Guide for Azure Cosmos DB Javascript SDK v4 Best practices for JavaScript SDK in Azure Cosmos DB for NoSQL Visit the JavaScript SDK v4 Release Notes page for the rest of our documentation and sample code. Announcing JavaScript SDK v4 for Azure Cosmos DB455Views1like0CommentsFirst Steps into Automation: Building Your First Playwright Test
Your journey into automated testing begins here! This is not just a blog; it's your gateway to mastering the art of reliable and consistent user experiences. With Playwright's rich features and tools, you're equipped to tackle modern web development challenges head-on. Don't wait, start learning Playwright today and take your web testing to the next level!8.8KViews0likes1CommentDon't miss the 2024 Azure Developers JavaScript Day!
Do you want to discover the latest services and features in Azure designed specifically for JavaScript developers? Are you looking for cutting-edge cloud development techniques that can save you time and money, while providing your customers with the best experience possible?2.3KViews1like2CommentsWhat is GitHub Codespaces and how can Students access it for free?
GitHub Codespaces is a new service that is free for anyone to develop with powerful environments using Visual Studio Code. In this post, we'll cover how you can make use of this new technology and take advantage of its most powerful features.45KViews5likes5CommentsHow to use Azure Maps to Build a Taxi Hailing Web App
Learn how simple it is to set up an Azure Maps Resource account and quickly create applications that have beautiful maps that can be used in a range of solutions. In this tutorial we are going to create a simple and fast taxi hailing Web application with only HTML, CSS and Vanilla JavaScript.3KViews0likes0Comments