Skip to content

Commit

Permalink
Feature client profile updates api (#10)
Browse files Browse the repository at this point in the history
* update client profile

* save client profile updates ongoing

* save ongoing

* update profile ongoing

* fetch updates ongoing

* parse updates ongoing

* changed db

* custom field access changes

* save updates

* get updates

* profile update history ongoing

* update history ongoing

* update history order fix

* clean migrations

* settings

* cleanup

* Bug fixes and PR changes

* setting changes
  • Loading branch information
sajjanstha committed Jan 29, 2024
1 parent 4a93d1b commit e5dab64
Show file tree
Hide file tree
Showing 24 changed files with 561 additions and 58 deletions.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
-- CreateEnum
CREATE TYPE "Permission" AS ENUM ('VIEW', 'EDIT');

-- CreateTable
CREATE TABLE "CustomFieldAccess" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"customFieldId" UUID,
"permissions" JSONB,
"customFieldId" UUID NOT NULL,
"portalId" TEXT NOT NULL,
"permissions" "Permission"[],
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,

CONSTRAINT "CustomFieldAccess_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "CustomFieldAccess_customFieldId_key" ON "CustomFieldAccess"("customFieldId");
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "ClientProfileUpdates" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"clientId" UUID NOT NULL,
"companyId" UUID NOT NULL,
"customFields" JSONB NOT NULL,
"changedFields" JSONB NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,

CONSTRAINT "ClientProfileUpdates_pkey" PRIMARY KEY ("id")
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "Setting" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"portalId" TEXT NOT NULL,
"profileLinks" JSONB NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,

CONSTRAINT "Setting_pkey" PRIMARY KEY ("id")
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `portalId` to the `ClientProfileUpdates` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "ClientProfileUpdates" ADD COLUMN "portalId" TEXT NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `profileLinks` on the `Setting` table. All the data in the column will be lost.
- Added the required column `data` to the `Setting` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Setting" DROP COLUMN "profileLinks",
ADD COLUMN "data" JSONB NOT NULL;
30 changes: 25 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,38 @@ generator client {
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}

enum Permission {
VIEW
EDIT
VIEW
EDIT
}

model CustomFieldAccess {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
customFieldId String @db.Uuid
portalId String
permissions Permission[]
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt @ignore @db.Timestamptz()
}

model ClientProfileUpdates {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
customFieldId String @db.Uuid
companyId String @db.Uuid
permission Permission
clientId String @db.Uuid
companyId String @db.Uuid
portalId String
customFields Json @db.JsonB
changedFields Json @db.JsonB
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt @ignore @db.Timestamptz()
}

model Setting {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
portalId String
data Json @db.JsonB
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt @ignore @db.Timestamptz()
}
116 changes: 116 additions & 0 deletions src/app/api/client-profile-updates/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { NextRequest, NextResponse } from 'next/server';
import { ClientProfileUpdatesRequestSchema, ParsedClientProfileUpdatesResponse } from '@/types/clientProfileUpdates';
import { CopilotAPI } from '@/utils/copilotApiUtils';
import { handleError, respondError } from '@/utils/common';
import { ClientProfileUpdatesService } from '@/app/api/client-profile-updates/services/clientProfileUpdates.service';
import { ClientResponse, CompanyResponse } from '@/types/common';
import { z } from 'zod';
import { createLookup, getObjectDifference, getSelectedOptions } from '@/lib/helper';

export async function POST(request: NextRequest) {
const data = await request.json();
const clientProfileUpdateRequest = ClientProfileUpdatesRequestSchema.safeParse(data);
if (!clientProfileUpdateRequest.success) {
return NextResponse.json(clientProfileUpdateRequest.error.issues, { status: 422 });
}
try {
//todo: check access
const copilotClient = new CopilotAPI(clientProfileUpdateRequest.data.token);
const client: ClientResponse = await copilotClient.getClient(clientProfileUpdateRequest.data.clientId);
const clientUpdateResponse = await copilotClient.updateClient(clientProfileUpdateRequest.data.clientId, {
customFields: clientProfileUpdateRequest.data.form,
});
const changedFields = getObjectDifference(clientUpdateResponse.customFields ?? {}, client.customFields ?? {});
if (Object.keys(changedFields).length === 0) {
return NextResponse.json({});
}

const service = new ClientProfileUpdatesService();
await service.save({
clientId: clientProfileUpdateRequest.data.clientId,
companyId: clientProfileUpdateRequest.data.companyId,
portalId: clientProfileUpdateRequest.data.portalId,
customFields: clientUpdateResponse.customFields ?? {},
changedFields,
});

return NextResponse.json({});
} catch (error) {
return handleError(error);
}
}

export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token');
const portalId = request.nextUrl.searchParams.get('portalId');

if (!token) {
return respondError('Missing token', 422);
}
if (!portalId) {
return respondError('Missing portalId', 422);
}

try {
const copilotClient = new CopilotAPI(z.string().parse(token));

const [currentUser, clients, companies, portalCustomFields] = await Promise.all([
copilotClient.me(),
copilotClient.getClients(),
copilotClient.getCompanies(),
copilotClient.getCustomFields(),
]);
//todo:: filter companyIds based on currentUser restrictions
const clientProfileUpdates = await new ClientProfileUpdatesService().findMany(portalId, []);

const clientLookup = createLookup(clients.data, 'id');
const companyLookup = createLookup(companies.data, 'id');

const parsedClientProfileUpdates: ParsedClientProfileUpdatesResponse[] = clientProfileUpdates.map((update) => {
const client = clientLookup[update.clientId];
const company = companyLookup[update.companyId];

const customFields = portalCustomFields.data?.map((portalCustomField) => {
const value = update.customFields[portalCustomField.key] ?? null;
const options = getSelectedOptions(portalCustomField, value);

return {
name: portalCustomField.name,
type: portalCustomField.type,
key: portalCustomField.key,
value: options.length > 0 ? options : value,
isChanged: !!update.changedFields[portalCustomField.key],
};
});

return {
id: update.id,
client: getClientDetails(client),
company: getCompanyDetails(company),
lastUpdated: update.createdAt,
customFields,
};
});

return NextResponse.json(parsedClientProfileUpdates);
} catch (error) {
return handleError(error);
}
}

function getClientDetails(client: ClientResponse) {
return {
id: client.id,
name: `${client.givenName} ${client.familyName}`,
email: client.email,
avatarImageUrl: client.avatarImageUrl,
};
}

function getCompanyDetails(company: CompanyResponse) {
return {
id: company.id,
name: company.name,
iconImageUrl: company.iconImageUrl,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { PrismaClient } from '@prisma/client';
import DBClient from '@/lib/db';
import {
ClientProfileUpdates,
ClientProfileUpdatesResponse,
ClientProfileUpdatesResponseSchema,
UpdateHistory,
} from '@/types/clientProfileUpdates';

export class ClientProfileUpdatesService {
private prismaClient: PrismaClient = DBClient.getInstance();

async save(requestData: ClientProfileUpdates): Promise<void> {
await this.prismaClient.clientProfileUpdates.create({
data: {
clientId: requestData.clientId,
companyId: requestData.companyId,
portalId: requestData.portalId,
customFields: requestData.customFields,
changedFields: requestData.changedFields,
},
});
}

// Company Filter is not applied if companyIds is empty
async findMany(portalId: string, companyIds: Array<string>): Promise<ClientProfileUpdatesResponse> {
let clientProfileUpdates = [];
if (companyIds.length > 0) {
clientProfileUpdates = await this.prismaClient.clientProfileUpdates.findMany({
where: {
portalId: portalId,
companyId: {
in: companyIds,
},
},
});
} else {
clientProfileUpdates = await this.prismaClient.clientProfileUpdates.findMany({
where: {
portalId: portalId,
},
});
}

return ClientProfileUpdatesResponseSchema.parse(clientProfileUpdates);
}

async getUpdateHistory(customFieldKey: string, clientId: string, lastUpdated: Date): Promise<UpdateHistory[]> {
return this.prismaClient.$queryRaw`
SELECT "changedFields"
FROM "ClientProfileUpdates"
WHERE "clientId" = ${clientId}::uuid
AND "createdAt" <= ${lastUpdated}
AND "changedFields" ->> ${customFieldKey} IS NOT NULL
ORDER BY "createdAt" DESC;
`;
}
}
16 changes: 8 additions & 8 deletions src/app/api/custom-field-access/route.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { CustomFieldAccessRequestSchema } from '@/types/customFieldAccess';
import { CustomFieldAccessService } from '@/app/api/custom-field-access/services/customFieldAccess.service';
import { errorHandler } from '@/utils/common';
import { respondError } from '@/utils/common';
import { CopilotAPI } from '@/utils/copilotApiUtils';
import { z } from 'zod';

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const token = searchParams.get('token');
const companyId = searchParams.get('companyId');
const portalId = searchParams.get('portalId');
if (!token) {
return errorHandler('Missing token', 422);
return respondError('Missing token', 422);
}
if (!companyId) {
return errorHandler('Missing companyId', 422);
if (!portalId) {
return respondError('Missing portalId', 422);
}
const copilotClient = new CopilotAPI(z.string().parse(token));
const customFields = (await copilotClient.getCustomFields()).data;

const customFieldAccessService = new CustomFieldAccessService();
const customFieldAccesses = await customFieldAccessService.findAll(z.string().uuid().parse(companyId));
const customFieldAccesses = await customFieldAccessService.findAll(z.string().parse(portalId));

const customFieldsWithAccess = customFields?.map((customField) => {
const customFieldAccess = customFieldAccesses.find((access) => access.customFieldId === customField.id);
return {
...customField,
permission: customFieldAccess ? customFieldAccess.permission : null,
permission: customFieldAccess ? customFieldAccess.permissions : [],
};
});

Expand All @@ -36,7 +36,7 @@ export async function PUT(request: NextRequest) {
const data = await request.json();
const customFieldAccess = CustomFieldAccessRequestSchema.safeParse(data);
if (!customFieldAccess.success) {
return NextResponse.json(customFieldAccess.error.issues);
return NextResponse.json(customFieldAccess.error.issues, { status: 422 });
}
const customFieldAccessService = new CustomFieldAccessService();
await customFieldAccessService.save(customFieldAccess.data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export class CustomFieldAccessService {
customFieldId: {
in: customFieldIds,
},
companyId: requestData.companyId,
portalId: requestData.portalId,
},
},
});
const accesses = requestData.accesses.map((access) => {
return {
...access,
companyId: requestData.companyId,
portalId: requestData.portalId,
};
});
const createCustomFields = this.prismaClient.customFieldAccess.createMany({
Expand All @@ -34,10 +34,10 @@ export class CustomFieldAccessService {
await this.prismaClient.$transaction([deleteCustomFields, createCustomFields]);
}

async findAll(companyId: string): Promise<CustomFieldAccessResponse> {
async findAll(portalId: string): Promise<CustomFieldAccessResponse> {
const customFieldAccesses = await this.prismaClient.customFieldAccess.findMany({
where: {
companyId: companyId,
portalId: portalId,
},
});

Expand Down
Loading

0 comments on commit e5dab64

Please sign in to comment.