npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@excopartners/power-data-lib

v2.0.6

Published

This package is designed to be a utility library to connecting to the Microsoft Dynamics / Power Apps 365 Web API. It is part of the Exco Partners Experience Runtime. It specifically:

Downloads

670

Readme

Exco Partners - Power Data Lib

This package is designed to be a utility library to connecting to the Microsoft Dynamics / Power Apps 365 Web API. It is part of the Exco Partners Experience Runtime. It specifically:

  • Simplified Web API calls
  • Creates an abstraction layer for tabled oriented data
  • Creates a framework for row level security

The examples below assume a web application like Next.js where the data access is managed on the server side.

Install our Experience Runtime

First install the experience runtime libraries:

Run the following:

npm install @excopartners/power-portal @excopartners/power-data-lib @excopartners/utils

Setting up Dynamics parameters

For this demo we will connect to our Dynamics Sandbox platform. We will add these settings into environment files.

This setup assumed the Dynamics Sandbox environment is already setup with the service principle refered to below.

  1. Create a new environment file called .env

  2. Add the following lines to the .env file

     DATAVERSE_CRM=https://xxxxx-sandbox.crm6.dynamics.com/
     DATAVERSE_TENANT_ID=XXXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
     DATAVERSE_CLIENT_ID=XXXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  3. Create another environment file called .env.local

  4. Add the following line to the .env.local file

     DATAVERSE_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We seperate these files for one very important reason. The .env file holds all the common environment settings and is typically checked into a source code repository. The .env.local file must NEVER be checked into source code as we use that to store secrets, like DATAVERSE_CLIENT_SECRET.

The data we have added specified:

  • DATAVERSE_CRM: The url to our Sandbox Dynamics environment
  • DATAVERSE_TENANT_ID: Our Azure Tenent that will be used for Authentication.
  • DATAVERSE_CLIENT_ID: The Service Principle / App Registration that will be used for Authentication
  • DATAVERSE_CLIENT_SECRET: The password for the Service Principle

Connecting to Dynamics

Next we will create a helper file that will help us create an authenticated API client to access Dynamics.

  1. Create a new folder called lib we will add all our code and service here. The full path will be /src/lib

  2. Create a new file in the lib folder called DynamicsApi.ts. Add the following code:

     import { Aut, DynamicsApi } from "@excopartners/power-data-lib";
    	
     // Create an authenticator for Dynamics
     export const dynamicsAuth = new Auth(
     	process.env.DATAVERSE_CLIENT_ID || "client_id_not_set", 
     	process.env.DATAVERSE_CLIENT_SECRET || "client_secret_not_set", 
     	process.env.DATAVERSE_TENANT_ID || "tenant_id_not_set",
     	[ process.env.DATAVERSE_CRM + "/.default" ]
     );
    
     export const DYNAMICS_API_URL = process.env.DATAVERSE_CRM || "https://crm_url_not_set";
    
     export const PUBLISHER_PREFIX: string = "exco_";
    
     export const createDynamicsApiClient = () => new DynamicsApi(dynamicsAuth, DYNAMICS_API_URL);
    
     const dynamicsApi = createDynamicsApiClient();
     export default dynamicsApi;

This file pulls the values from the .env* files, creates an Authenticator called dynamicsAuth and then creates a Dynamics API client called dynamicsApi. We will later use this client to access data.

Fetching data from Dynamics

Next we will pull some data from Dynamics. For the demo, we will pull some Contact record data. In this example, assume we are building an API endpoint/route in next.js.

  1. Return to the API file we created in Part 3. /src/app/api/contacts/route.ts

  2. Change the content of the file to be as follows:

     import dynamicsApi from "@/lib/DynamicsApi";
     import { NextRequest, NextResponse } from "next/server";
    
     export async function GET(request: NextRequest) {
    
     	//Get the top set of contact records from dynamics
     	const contacts = await dynamicsApi.dynamicsWebApi.retrieveMultiple({
     		collection: "contacts"
     	});
    
     	//Return the value proprty of the returned contacts
     	return NextResponse.json(contacts.value, {status: 200});
    		
     }

This will pull the top 2 contacts from Dynamics.

Restart the server:

npm run dev

Check the data returned from the API: http://localhost:3000/api/contacts

We we now check the page we made in Part 3, we can see the title is pullign the name of the first contact in Dynamics. See: http://localhost:3000

Making it easier to fetch data

The statement we used above to pull the data from dynamics is quite verbose. To make our code cleaner to write, read and maintain, we will create a helper file to access the Contacts table.

  1. Create a new folder called backend/dataverse. The full path will be src/lib/backend/dataverse.
  2. Create a new file called ContactRepository.ts. This will be our helper file to access the Contacts table
  3. Add the following code the the ContactRepository.ts file

ContactRepository.ts

import dynamicsApi from "@/lib/DynamicsApi";
import { DataverseDto, DataverseRepository, SessionUser } from "@excopartners/power-data-lib";

export const CONTACT_SELECT: string[] = [
	"firstname",
	"lastname",
	"emailaddress1",
];
~/* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 
*      M A P P I N G
* 
* * * * * * * * * * * * * * * * * * * * * * * * * * * * */

export interface ContactDto extends DataverseDto {
	id: string
	firstname: string;
	lastname: string;
	email?: string
}
export interface ContactForm extends Omit<ContactDto, "id"> {
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 
*      R E P O   C L A S S   D E F I N I T I O N
* 
* * * * * * * * * * * * * * * * * * * * * * * * * * * * */
export class ContactRepository extends DataverseRepository<ContactDto, any, any> {

	// The table name in Dataverse
	_collection = "contacts";

	//The fields to select from the table
	_select = CONTACT_SELECT;

	_expand = undefined;
	_orderby = undefined;

	mapDto(data: any): ContactDto {
		return mapContactDto(data);
	}

}

export function mapContactDto(data: any): ContactDto {	

	return {
		id: data.contactid,
		firstname: data.firstname,
		lastname: data.lastname,
		email: data.emailaddress1,
	};
}


/* * * * * * * * * * * * * * * * * * * * * * * *
* 
*      R E P O   E X P O R T
* 
* * * * * * * * * * * * * * * * * * * * * * * */

export default function contactRepository(session: SessionUser) {
	const contactRepository = new ContactRepository(
		{
			session: session,
			hasReadAccess: async () => true,
			hasUpdateAccess: async () => false,
			hasDeleteAccess: async () => false,
			hasCreateAccess: async () => false
		}, 
		dynamicsApi
	);
	return contactRepository;
}

There is a bit in this file but it is all boiler plate that once setup, makes the rest of our code much cleaner.

It extends our DataverseRepository abscract class which brings in heaps of helper functions to access data. It also does some mapping messy Dynamics data field names to a nice strictly typed Typescript definition ContactDto.

Now let's go back to our API file from Part 3. /src/app/api/users/route.ts and change the code that accessed Dynamics from

//Get the top 2 contact records from dynamics
const contacts = await dynamicsApi.dynamicsWebApi.retrieveMultiple({
	collection: "contacts",
	top: 2
});

return NextResponse.json(contacts.value, {status: 200});

To:

const contacts = await contactRepository({}).fetchAll();

return NextResponse.json(contacts, {status: 200});

Security

The Digital Runtime libraries include support record-level security. This is a very important part of the library and must be handled carefully to make sure we don't create data vulnerabilities.

The security model takes a pessimistic view of accessing data and only allows access if the programmer explicitly permits it.

The underlying App User that has access to the database, which we set up in Part 4 with the DATAVERSE_CLIENT_ID setting, has, generally, access to all the data. For accessing data upstream of our application, ie. to the public internet, we need to tie the authenticated user to the data access layer so we can introduce security at the lowest level possible. This is the same method used by Power Pages.

Open up the ContactRepository from Part 4 and look for the section that exports the repository:

export default function contactRepository(session: SessionUser) {
		const contactRepository = new ContactRepository(
			{
				session: session,
				hasReadAccess: async () => true,
				hasUpdateAccess: async () => false,
				hasDeleteAccess: async () => false,
				hasCreateAccess: async () => false
			}, 
			dynamicsApi
		);
		return contactRepository;
	}

This code has four required parameters defining methods to confirm access across Read, Update, Delete and Create. The default is false, meaning no access in all cases except for Read, which we set to true in Part 4. This means all authenticated users can read all contact records but cannot Update, Delete or Create.

Let's update the security to manage which users have access to data. Change the hasReadAccess parameter as follows:

export default function contactRepository(session: SessionUser) {
		const contactRepository = new ContactRepository(
			{
				session: session,
				hasReadAccess: hasContactReadAccess,
				hasUpdateAccess: async () => false,
				hasDeleteAccess: async () => false,
				hasCreateAccess: async () => false
			}, 
			dynamicsApi
		);
		return contactRepository;
	}

Now we need to implement the hadContactReadAccess function. Add the following code to the file:

export async function hasContactReadAccess(contact: ContactDto, session?: SessionUser) {

	//If no session (ie. no authenticated user), return false 
	if (!session || !session.user?.id) return false;

	//Get the authenticated user record
	const authenticatedUser = await externalUserRepository(session).fetchByAuthId(session.user.id);

	//Return true if the contact matches the contact of the authenticated user
	return !!authenticatedUser && authenticatedUser.contact.id === contact.id;

}

This method first checks if there is an authenticated user (this will be specific to each implementation). It then gets the ExternalUser record, from the dataverse defining the authendicated user. We use a table ExternalUser for this purpose. The library will case this record, so it will be fast. Finally, it checks that the authenticated user contact is the same as the contact to be read. It is it, return true.

Security Policy

The method above creates a lot of flexibility for implementing custom security rule however, it can be difficult to maintain, test and control. The recommended approach to implmenting security is to define a security policy which makes the definition of security more declaritive.

Replace the code above with a reference to a security policy:

export default function contactRepository(session: SessionUser) {
		const contactRepository = new ContactRepository(
			{
				session: session,
				securityPolicy: contactSecurityPolicy
			}, 
			dynamicsApi
		);
		return contactRepository;
	}

Now define a security policy:


//Define an interface for your custom security set, specifying the type definitions of the User and User Roles your project
export interface MySecurityPolicySet<D extends DataverseDto> extends SecurityPolicySet<D, ExternalUserDto, RoleType> {}

//Define a security policy for contacts
const contactSecurityPolicy: MySecurityPolicySet<ContactDto> = {
		
	read: {

		[RoleType.ADMIN]: () => {
			//All admin roles get access
			return true;
		},
		default: (contact, user) => {
			//Check if user contact id patched contact
			return contact.id === user.contact.id;
		}
	},

	update: {
		
		[RoleType.ADMIN]: () => {
			//All admin roles get access
			return true;
		},
		default: ( contact: ContactDto, user: ExternalUserDto ) => {
			//Check if user contact id patched contact
			return contact.id === user.contact.id;
		}
	},

	fetchAuthenticatedUser: async (session: SessionUser) => {
		return await externalUserRepository(session).fetchByAuthId(session.user.id);
	}

};

The security policy defines the following:

  1. A fetchAuthenticatedUser which lets you define how to get a reference to your User record, in our case from the ExternalUser table.
  2. Defines a set of policy for each operation of create, read, update, delete. Only read and update are shown above which means create and delete are not supported for any user.
  3. Implements a function for user role type, and a default function, which lets the system tests if the user has access to the record.

In the above policy, the default case will always be tested first and only moves on the the role type cases if default returns false.