Skip to content

Commit

Permalink
refactor sharepoint endpoints to use graph api
Browse files Browse the repository at this point in the history
  • Loading branch information
TangoYankee authored and TylerMatteo committed Apr 12, 2024
1 parent 61feae0 commit 7e87feb
Show file tree
Hide file tree
Showing 16 changed files with 425 additions and 237 deletions.
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"prepare": "cd .. && husky install .husky"
},
"dependencies": {
"@azure/msal-node": "^2.6.5",
"@nestjs/common": "^7.6.15",
"@nestjs/core": "^7.6.15",
"@nestjs/platform-express": "^7.6.15",
Expand Down
12 changes: 7 additions & 5 deletions server/src/artifact/artifact.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ export class ArtifactService {
async getArtifactSharepointDocuments(relativeUrl, dcp_name) {
if (relativeUrl) {
try {
const folderPath = relativeUrl.split("/");
const folderName = folderPath[folderPath.length - 1];
const documents = await this.sharepointService.getSharepointFolderFiles(
`dcp_artifacts/${relativeUrl}`,
"?$expand=Files,Folders,Folders/Files,Folders/Folders/Files,Folders/Folders/Folders/Files"
this.sharepointService.driveIdMap.dcp_artifact,
folderName
);

if (documents) {
return documents.map(document => ({
name: document["Name"],
timeCreated: document["TimeCreated"],
serverRelativeUrl: document["ServerRelativeUrl"]
name: document.name,
timeCreated: document.createdDateTime,
serverRelativeUrl: `/${document.id}`
}));
}

Expand Down
12 changes: 7 additions & 5 deletions server/src/disposition/disposition.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ export class DispositionService {
async getDispositionSharepointDocuments(relativeUrl, dcp_name) {
if (relativeUrl) {
try {
const folderPath = relativeUrl.split("/");
const folderName = folderPath[folderPath.length - 1];
const documents = await this.sharepointService.getSharepointFolderFiles(
`dcp_dispositions/${relativeUrl}`,
"?$expand=Files,Folders,Folders/Files,Folders/Folders/Files,Folders/Folders/Folders/Files"
this.sharepointService.driveIdMap.dcp_communityboarddisposition,
folderName
);

if (documents) {
return documents.map(document => ({
name: document["Name"],
timeCreated: document["TimeCreated"],
serverRelativeUrl: document["ServerRelativeUrl"]
name: document.name,
timeCreated: document.createdDateTime,
serverRelativeUrl: `/${document.id}`
}));
}

Expand Down
94 changes: 53 additions & 41 deletions server/src/document/document.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ function hyphenateGUID(unhyphenatedGUID) {
].join("");
}

// "path" refers to the "relative server path", the path
// to the file itself on the sharepoint host. It has the format
//
// /sites/<site>/<entity_name>/<folder_name>/filename.pdf
//
// For example,
//
// /sites/dcpuat2/dcp_package/2021M0268_DraftEAS_1_996699F37323EB11A813001DD8309FA8/EAS%20Full%20Form.pdf
//
// Note that <folder_name> has this format:
//
// <dcp_name>_<RECORDID>
//
Expand All @@ -68,23 +58,12 @@ function hyphenateGUID(unhyphenatedGUID) {
//
// We have assurance of this after sprint 10 EAS enhancements. See
// https://dcp-paperless.visualstudio.com/dcp-paperless-dynamics/_workitems/edit/13366
function getRecordIdFromDocumentPath(path) {
const [
,
,
,
// "sites"
// environment
// entityType
folder
// fileName
] = path.split("/");

const folderSegments = folder.split("_");
const strippedRecordId = folderSegments[folderSegments.length - 1];
function getRecordIdFromFolderName(documentName: string) {
const documentSegments = documentName.split("_");
const strippedRecordId = documentSegments[documentSegments.length - 1];

// we need to re-insert hyphens to recreate the actual packageId, artifactId or ProjectactionId
// TODO: Consider asking to preseve hyphens in record ID
// TODO: Consider asking to preserve hyphens in record ID
return hyphenateGUID(strippedRecordId);
}

Expand All @@ -108,8 +87,16 @@ export class DocumentService {
) {}
// For info on the path param,
// see above documentation for the getRecordIdFromDocumentPath function
public async getPackageDocument(path) {
const recordId = getRecordIdFromDocumentPath(path);
public async getPackageDocument(fileId) {
const driveId = this.sharepointService.driveIdMap.dcp_package;
const {
parentReference
} = await this.sharepointService.getSharepointFileParentReference(
driveId,
fileId
);
const { name: parentName } = parentReference;
const recordId = getRecordIdFromFolderName(parentName);

try {
// Only documents belonging to public, submitted packages should be accessible
Expand Down Expand Up @@ -138,18 +125,26 @@ export class DocumentService {

if (!firstPackage) {
throwNoDocumentError(
`Client attempted to retrieve document ${path}, but no associated public, submitted packages were found.`
`Client attempted to retrieve document ${parentName}, but no associated public, submitted packages were found.`
);
}

return await this.sharepointService.getSharepointFile(path);
return await this.sharepointService.getSharepointFile(driveId, fileId);
} catch (e) {
throwNoDocumentError(`Unable to provide document access.`);
}
}

public async getArtifactDocument(path) {
const recordId = getRecordIdFromDocumentPath(path);
public async getArtifactDocument(fileId) {
const driveId = this.sharepointService.driveIdMap.dcp_artifact;
const {
parentReference
} = await this.sharepointService.getSharepointFileParentReference(
driveId,
fileId
);
const { name: parentName } = parentReference;
const recordId = getRecordIdFromFolderName(parentName);

try {
const {
Expand All @@ -167,18 +162,26 @@ export class DocumentService {

if (!firstArtifact) {
throwNoDocumentError(
`Client attempted to retrieve document ${path}, but no associated public, submitted artifacts were found.`
`Client attempted to retrieve document ${parentName}, but no associated public, submitted artifacts were found.`
);
}

return await this.sharepointService.getSharepointFile(path);
return await this.sharepointService.getSharepointFile(driveId, fileId);
} catch (e) {
throwNoDocumentError(`Unable to provide document access.`);
}
}

public async getProjectactionDocument(path) {
const recordId = getRecordIdFromDocumentPath(path);
public async getProjectactionDocument(fileId: string) {
const driveId = this.sharepointService.driveIdMap.dcp_projectaction;
const {
parentReference
} = await this.sharepointService.getSharepointFileParentReference(
driveId,
fileId
);
const { name: parentName } = parentReference;
const recordId = getRecordIdFromFolderName(parentName);

try {
const {
Expand All @@ -196,18 +199,27 @@ export class DocumentService {

if (!firstProjectaction) {
throwNoDocumentError(
`Client attempted to retrieve document ${path}, but no associated inactive project actions were found.`
`Client attempted to retrieve document ${parentName}, but no associated inactive project actions were found.`
);
}

return await this.sharepointService.getSharepointFile(path);
return await this.sharepointService.getSharepointFile(driveId, fileId);
} catch (e) {
throwNoDocumentError(`Unable to provide document access.`);
}
}

public async getDispositionDocument(path) {
const recordId = getRecordIdFromDocumentPath(path);
public async getDispositionDocument(fileId) {
const driveId = this.sharepointService.driveIdMap
.dcp_communityboarddisposition;
const {
parentReference
} = await this.sharepointService.getSharepointFileParentReference(
driveId,
fileId
);
const { name: parentName } = parentReference;
const recordId = getRecordIdFromFolderName(parentName);

try {
const {
Expand All @@ -227,11 +239,11 @@ export class DocumentService {

if (!firstDisposition) {
throwNoDocumentError(
`Client attempted to retrieve document ${path}, but no associated public, submitted dispositions were found.`
`Client attempted to retrieve document ${parentName}, but no associated public, submitted dispositions were found.`
);
}

return await this.sharepointService.getSharepointFile(path);
return await this.sharepointService.getSharepointFile(driveId, fileId);
} catch (e) {
throwNoDocumentError(`Unable to provide document access.`);
}
Expand Down
13 changes: 7 additions & 6 deletions server/src/package/package.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ export class PackageService {

if (relativeurl) {
try {
const folderPath = relativeurl.split("/");
const folderName = folderPath[folderPath.length - 1];
const documents = await this.sharepointService.getSharepointFolderFiles(
`dcp_package/${relativeurl}`,
"?$expand=Files,Folders,Folders/Files,Folders/Folders/Files,Folders/Folders/Folders/Files"
this.sharepointService.driveIdMap.dcp_package,
folderName
);

if (documents) {
return documents.map(document => ({
name: document["Name"],
timeCreated: document["TimeCreated"],
serverRelativeUrl: document["ServerRelativeUrl"]
name: document.name,
timeCreated: document.createdDateTime,
serverRelativeUrl: `/${document.id}`
}));
}

Expand Down
64 changes: 64 additions & 0 deletions server/src/provider/msal.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FactoryProvider, HttpException, HttpStatus } from "@nestjs/common";
import { ConfigService } from "../config/config.service";
import msal from "@azure/msal-node";

export interface MsalProviderType {
getGraphClientToken: () => Promise<msal.AuthenticationResult>;
sharePointSiteUrl: string;
}
export const MSAL = Symbol("MSAL_SERVICE");
export const MsalProvider: FactoryProvider<MsalProviderType> = {
provide: MSAL,
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const tenantId: string | undefined = config.get("TENANT_ID");
const clientId: string | undefined = config.get("SHAREPOINT_CLIENT_ID");
const clientSecret: string | undefined = config.get(
"SHAREPOINT_CLIENT_SECRET"
);
const siteId: string | undefined = config.get("SHAREPOINT_SITE_ID");
if (
tenantId === undefined ||
clientId === undefined ||
clientSecret === undefined ||
siteId === undefined
) {
throw new Error("Missing SharePoint credential");
}

const cca = new msal.ConfidentialClientApplication({
auth: {
clientId,
clientSecret,
authority: `https://login.microsoftonline.com/${tenantId}`
}
});
const graphBaseUrl = "https://graph.microsoft.com";
const scopes = [`${graphBaseUrl}/.default`];
const sharePointSiteUrl = `${graphBaseUrl}/v1.0/sites/${siteId}`;

// Method also checks for a cached token before calling security token service
// https://github.com/MicrosoftDocs/entra-docs/blob/main/docs/identity-platform/msal-acquire-cache-tokens.md#recommended-call-pattern-for-public-client-applications
const getGraphClientToken = () => {
try {
return cca.acquireTokenByClientCredential({
scopes
});
} catch {
throw new HttpException(
{
code: "GRAPH_TOKEN_ERROR",
title: "Error retrieving Graph token",
detail: "Error retrieving Graph token"
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}
};

return {
getGraphClientToken,
sharePointSiteUrl
};
}
};
21 changes: 8 additions & 13 deletions server/src/sharepoint/sharepoint.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '../config/config.module';
import { SharepointService } from './sharepoint.service';
import { Module } from "@nestjs/common";
import { ConfigModule } from "../config/config.module";
import { SharepointService } from "./sharepoint.service";
import { MsalProvider } from "../provider/msal.provider";

@Module({
imports: [
ConfigModule,
],
providers: [
SharepointService,
],
exports: [
SharepointService,
],
controllers: [],
imports: [ConfigModule],
providers: [SharepointService, MsalProvider],
exports: [SharepointService],
controllers: []
})
export class SharepointModule {}
Loading

0 comments on commit 7e87feb

Please sign in to comment.