diff --git a/apps/api/src/app/mapping/usecases/do-mapping/do-mapping.usecase.ts b/apps/api/src/app/mapping/usecases/do-mapping/do-mapping.usecase.ts index c1021ad2d..9ff187810 100644 --- a/apps/api/src/app/mapping/usecases/do-mapping/do-mapping.usecase.ts +++ b/apps/api/src/app/mapping/usecases/do-mapping/do-mapping.usecase.ts @@ -1,27 +1,15 @@ import { Injectable } from '@nestjs/common'; import { Defaults, UploadStatusEnum } from '@impler/shared'; -import { ColumnEntity, ColumnRepository, MappingEntity, MappingRepository, UploadRepository } from '@impler/dal'; +import { ColumnEntity, MappingEntity, MappingRepository, UploadRepository } from '@impler/dal'; import { DoMappingCommand } from './do-mapping.command'; @Injectable() export class DoMapping { - constructor( - private columnRepository: ColumnRepository, - private mappingRepository: MappingRepository, - private uploadRepository: UploadRepository - ) {} + constructor(private mappingRepository: MappingRepository, private uploadRepository: UploadRepository) {} async execute(command: DoMappingCommand) { - const columns = await this.columnRepository.find( - { - _templateId: command._templateId, - }, - 'key alternateKeys sequence', - { - sort: 'sequence', - } - ); - const mapping = this.buildMapping(columns, command.headings, command._uploadId); + const uploadInfo = await this.uploadRepository.findById(command._uploadId, 'customSchema'); + const mapping = this.buildMapping(JSON.parse(uploadInfo.customSchema), command.headings, command._uploadId); const createdHeadings = await this.mappingRepository.createMany(mapping); await this.uploadRepository.update({ _id: command._uploadId }, { status: UploadStatusEnum.MAPPING }); diff --git a/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts b/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts index 8ee4de3b0..8b7e2ccfa 100644 --- a/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts +++ b/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts @@ -7,16 +7,9 @@ import addKeywords from 'ajv-keywords'; import { Injectable, BadRequestException } from '@nestjs/common'; import Ajv, { AnySchemaObject, ErrorObject, ValidateFunction } from 'ajv'; -import { - ColumnRepository, - UploadRepository, - MappingRepository, - ColumnEntity, - ValidatorRepository, - FileRepository, -} from '@impler/dal'; import { StorageService } from '@impler/shared/dist/services/storage'; import { ColumnTypesEnum, FileMimeTypesEnum, UploadStatusEnum } from '@impler/shared'; +import { UploadRepository, MappingRepository, ColumnEntity, ValidatorRepository, FileRepository } from '@impler/dal'; import { APIMessages } from '@shared/constants'; import { FileNameService } from '@shared/services'; @@ -82,7 +75,6 @@ export class DoReview { constructor( private uploadRepository: UploadRepository, private storageService: StorageService, - private columnRepository: ColumnRepository, private mappingRepository: MappingRepository, private validatorRepository: ValidatorRepository, private fileNameService: FileNameService, @@ -96,11 +88,7 @@ export class DoReview { throw new BadRequestException(APIMessages.UPLOAD_NOT_FOUND); } const mappings = await this.mappingRepository.getMappingWithColumnInfo(_uploadId); - const columns = await this.columnRepository.find( - { _templateId: uploadInfo._templateId }, - 'isRequired isUnique selectValues type regex' - ); - const schema = this.buildAJVSchema(columns, mappings); + const schema = this.buildAJVSchema(JSON.parse(uploadInfo.customSchema), mappings); const validator = ajv.compile(schema); const uploadedFileInfo = await this.fileRepository.findById(uploadInfo._uploadedFileId); diff --git a/apps/api/src/app/shared/helpers/common.helper.ts b/apps/api/src/app/shared/helpers/common.helper.ts index c28d87ce9..ff0be43fe 100644 --- a/apps/api/src/app/shared/helpers/common.helper.ts +++ b/apps/api/src/app/shared/helpers/common.helper.ts @@ -14,6 +14,14 @@ export function validateNotFound(data: any, entityName: 'upload'): boolean { } } +export function mergeObjects(obj1: any, obj2: any, keysToMerge: string[]) { + for (const key of keysToMerge) { + if (obj2.hasOwnProperty(key)) { + obj1[key] = obj2[key]; + } + } +} + export function paginateRecords(data: any[], page: number, limit: number): PaginationResult { if (!page || Number(page) < Defaults.ONE) page = Defaults.ONE; else page = Number(page); diff --git a/apps/api/src/app/upload/dtos/upload-request.dto.ts b/apps/api/src/app/upload/dtos/upload-request.dto.ts index fd14b31db..384254471 100644 --- a/apps/api/src/app/upload/dtos/upload-request.dto.ts +++ b/apps/api/src/app/upload/dtos/upload-request.dto.ts @@ -16,6 +16,14 @@ export class UploadRequestDto { @IsString() authHeaderValue: string; + @ApiProperty({ + description: 'Custom schema if provided by user', + required: false, + }) + @IsOptional() + @IsJSON() + schema: string; + @ApiProperty({ description: 'Payload to send during webhook call', required: false, diff --git a/apps/api/src/app/upload/upload.controller.ts b/apps/api/src/app/upload/upload.controller.ts index e84214c9f..e7e7a52f3 100644 --- a/apps/api/src/app/upload/upload.controller.ts +++ b/apps/api/src/app/upload/upload.controller.ts @@ -57,6 +57,7 @@ export class UploadController { templateId, extra: body.extra, authHeaderValue: body.authHeaderValue, + schema: body.schema, }) ); } diff --git a/apps/api/src/app/upload/usecases/make-upload-entry/add-upload-entry.command.ts b/apps/api/src/app/upload/usecases/make-upload-entry/add-upload-entry.command.ts index 39e42b23c..b9c991e4a 100644 --- a/apps/api/src/app/upload/usecases/make-upload-entry/add-upload-entry.command.ts +++ b/apps/api/src/app/upload/usecases/make-upload-entry/add-upload-entry.command.ts @@ -1,36 +1,19 @@ -import { IsDefined, IsString, IsOptional, IsJSON, IsArray, IsNumber } from 'class-validator'; -import { BaseCommand } from '@shared/commands/base.command'; - -export class AddUploadEntryCommand extends BaseCommand { - @IsDefined() - @IsString() +export class AddUploadEntryCommand { _templateId: string; - @IsOptional() - @IsString() _allDataFileId?: string; - @IsDefined() - @IsString() _uploadedFileId: string; - @IsDefined() - @IsString() uploadId: string; - @IsOptional() - @IsJSON() extra?: string; - @IsOptional() - @IsString() authHeaderValue?: string; - @IsOptional() - @IsArray() headings?: string[]; - @IsOptional() - @IsNumber() totalRecords?: number; + + schema?: string; } diff --git a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts index db66bb580..bfd1f4374 100644 --- a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts +++ b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts @@ -13,6 +13,10 @@ export class MakeUploadEntryCommand extends BaseCommand { @IsJSON() extra?: string; + @IsOptional() + @IsString() + schema?: string; + @IsOptional() @IsString() authHeaderValue?: string; diff --git a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts index a482d3e77..a0c7177d6 100644 --- a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts +++ b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts @@ -1,7 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { FileMimeTypesEnum, UploadStatusEnum, Defaults } from '@impler/shared'; -import { CommonRepository, FileEntity, FileRepository, TemplateRepository, UploadRepository } from '@impler/dal'; +import { FileMimeTypesEnum, UploadStatusEnum, Defaults, ISchemaItem } from '@impler/shared'; +import { + ColumnEntity, + ColumnRepository, + CommonRepository, + FileEntity, + FileRepository, + TemplateRepository, + UploadRepository, +} from '@impler/dal'; +import { mergeObjects } from '@shared/helpers/common.helper'; import { AddUploadEntryCommand } from './add-upload-entry.command'; import { MakeUploadEntryCommand } from './make-upload-entry.command'; import { StorageService } from '@impler/shared/dist/services/storage'; @@ -15,10 +24,11 @@ export class MakeUploadEntry { private fileRepository: FileRepository, private storageService: StorageService, private fileNameService: FileNameService, + private columnRepository: ColumnRepository, private templateRepository: TemplateRepository ) {} - async execute({ file, templateId, extra, authHeaderValue }: MakeUploadEntryCommand) { + async execute({ file, templateId, extra, authHeaderValue, schema }: MakeUploadEntryCommand) { const fileOriginalName = file.originalname; let csvFile: string | Express.Multer.File = file; if (file.mimetype === FileMimeTypesEnum.EXCEL || file.mimetype === FileMimeTypesEnum.EXCELX) { @@ -30,6 +40,41 @@ export class MakeUploadEntry { throw new Error('Invalid file type'); } + const columns = await this.columnRepository.find( + { + _templateId: templateId, + }, + 'key isRequired isUnique selectValues type regex sequence', + { + sort: 'sequence', + } + ); + let parsedSchema: ISchemaItem[], combinedSchema: string; + try { + parsedSchema = JSON.parse(schema); + } catch (error) {} + if (Array.isArray(parsedSchema) && parsedSchema.length > 0) { + const formattedColumns: Record = columns.reduce((acc, column) => { + acc[column.key] = { ...column }; + + return acc; + }, {}); + parsedSchema.forEach((schemaItem) => { + if (formattedColumns.hasOwnProperty(schemaItem.key)) { + mergeObjects(formattedColumns[schemaItem.key], schemaItem, [ + 'isRequired', + 'isUnique', + 'selectValues', + 'type', + 'regex', + ]); + } + }); + combinedSchema = JSON.stringify(Object.values(formattedColumns)); + } else { + combinedSchema = JSON.stringify(columns); + } + const fileService = new CSVFileService2(); const fileHeadings = await fileService.getFileHeaders(csvFile); const uploadId = this.commonRepository.generateMongoId().toString(); @@ -44,16 +89,15 @@ export class MakeUploadEntry { } ); - return this.addUploadEntry( - AddUploadEntryCommand.create({ - _templateId: templateId, - _uploadedFileId: fileEntity._id, - uploadId, - extra, - authHeaderValue, - headings: fileHeadings, - }) - ); + return this.addUploadEntry({ + _templateId: templateId, + _uploadedFileId: fileEntity._id, + uploadId, + extra, + authHeaderValue, + schema: combinedSchema, + headings: fileHeadings, + }); } private async makeFileEntry( @@ -86,6 +130,7 @@ export class MakeUploadEntry { extra, authHeaderValue, headings, + schema, totalRecords, }: AddUploadEntryCommand) { return this.uploadRepository.create({ @@ -94,6 +139,7 @@ export class MakeUploadEntry { _templateId, _allDataFileId, extra: extra, + customSchema: schema, headings: Array.isArray(headings) ? headings : [], status: UploadStatusEnum.UPLOADED, authHeaderValue: authHeaderValue, diff --git a/apps/widget/src/components/Common/Container/Container.tsx b/apps/widget/src/components/Common/Container/Container.tsx index 62f98461e..010378ef9 100644 --- a/apps/widget/src/components/Common/Container/Container.tsx +++ b/apps/widget/src/components/Common/Container/Container.tsx @@ -121,6 +121,7 @@ export function Container({ children }: PropsWithChildren<{}>) { > []; // api-context api: ApiService; @@ -20,8 +21,19 @@ interface IProviderProps { } export function Provider(props: PropsWithChildren) { - const { api, data, title, projectId, templateId, accessToken, extra, authHeaderValue, children, primaryColor } = - props; + const { + api, + data, + title, + projectId, + templateId, + accessToken, + extra, + authHeaderValue, + children, + primaryColor, + schema, + } = props; return ( ) { authHeaderValue={authHeaderValue} > - + {children} diff --git a/apps/widget/src/design-system/Table/Table.style.ts b/apps/widget/src/design-system/Table/Table.style.ts index 4b7cd8eeb..46c01e2ff 100644 --- a/apps/widget/src/design-system/Table/Table.style.ts +++ b/apps/widget/src/design-system/Table/Table.style.ts @@ -24,8 +24,6 @@ export const getHeadingStyles = (theme: MantineTheme): React.CSSProperties => ({ export const getInvalidColumnStyles = (theme: MantineTheme): React.CSSProperties => ({ backgroundColor: colors.lightDanger, - display: 'flex', - gap: 2, justifyContent: 'space-between', }); diff --git a/apps/widget/src/hooks/Phase1/usePhase1.ts b/apps/widget/src/hooks/Phase1/usePhase1.ts index 40b3ab63e..7d5afe1c6 100644 --- a/apps/widget/src/hooks/Phase1/usePhase1.ts +++ b/apps/widget/src/hooks/Phase1/usePhase1.ts @@ -17,7 +17,7 @@ interface IUsePhase1Props { export function usePhase1({ goNext }: IUsePhase1Props) { const { api } = useAPIState(); - const { setUploadInfo, setTemplateInfo, data } = useAppState(); + const { setUploadInfo, setTemplateInfo, schema, data } = useAppState(); const [templates, setTemplates] = useState([]); const [isDownloadInProgress, setIsDownloadInProgress] = useState(false); const { projectId, templateId, authHeaderValue, extra } = useImplerState(); @@ -150,6 +150,7 @@ export function usePhase1({ goNext }: IUsePhase1Props) { ...submitData, authHeaderValue, extra, + schema, }); } }; diff --git a/apps/widget/src/store/app.context.tsx b/apps/widget/src/store/app.context.tsx index 88626d73e..4e52ea3ca 100644 --- a/apps/widget/src/store/app.context.tsx +++ b/apps/widget/src/store/app.context.tsx @@ -8,7 +8,7 @@ interface AppContextProviderProps const AppContext = createContext(null); -const AppContextProvider = ({ children, primaryColor, title, data }: AppContextProviderProps) => { +const AppContextProvider = ({ children, primaryColor, title, data, schema }: AppContextProviderProps) => { const [templateInfo, setTemplateInfo] = useState({} as ITemplate); const [uploadInfo, setUploadInfo] = useState({} as IUpload); @@ -18,7 +18,7 @@ const AppContextProvider = ({ children, primaryColor, title, data }: AppContextP return ( {children} diff --git a/apps/widget/src/types/component.types.ts b/apps/widget/src/types/component.types.ts index 4c48c9132..0d44279da 100644 --- a/apps/widget/src/types/component.types.ts +++ b/apps/widget/src/types/component.types.ts @@ -32,4 +32,5 @@ export interface IFormvalues { export interface IUploadValues extends IFormvalues { authHeaderValue?: string; extra?: string; + schema?: string; } diff --git a/apps/widget/src/types/store.types.ts b/apps/widget/src/types/store.types.ts index 933c9425b..b32fd9435 100644 --- a/apps/widget/src/types/store.types.ts +++ b/apps/widget/src/types/store.types.ts @@ -20,6 +20,7 @@ export interface IAppStore { uploadInfo: IUpload; reset: () => void; primaryColor: string; + schema?: string; setTemplateInfo: (templateInfo: ITemplate) => void; setUploadInfo: (uploadInfo: IUpload) => void; } diff --git a/libs/dal/src/repositories/upload/upload.entity.ts b/libs/dal/src/repositories/upload/upload.entity.ts index 1d553b94b..95170cdd4 100644 --- a/libs/dal/src/repositories/upload/upload.entity.ts +++ b/libs/dal/src/repositories/upload/upload.entity.ts @@ -30,4 +30,6 @@ export class UploadEntity { extra: string; processInvalidRecords: boolean; + + customSchema: string; } diff --git a/libs/dal/src/repositories/upload/upload.schema.ts b/libs/dal/src/repositories/upload/upload.schema.ts index 175055192..53cd07a7f 100644 --- a/libs/dal/src/repositories/upload/upload.schema.ts +++ b/libs/dal/src/repositories/upload/upload.schema.ts @@ -41,6 +41,7 @@ const uploadSchema = new Schema( authHeaderValue: String, status: String, extra: String, + customSchema: String, processInvalidRecords: { type: Boolean, default: false, diff --git a/libs/shared/src/types/column/column.types.ts b/libs/shared/src/types/column/column.types.ts index a24b060e5..d8caec65a 100644 --- a/libs/shared/src/types/column/column.types.ts +++ b/libs/shared/src/types/column/column.types.ts @@ -7,3 +7,12 @@ export enum ColumnTypesEnum { 'SELECT' = 'Select', 'ANY' = 'Any', } + +export interface ISchemaItem { + key: string; + isRequired?: boolean; + isUnique?: boolean; + selectValues?: string[]; + type?: ColumnTypesEnum; + regex?: string; +} diff --git a/libs/shared/src/types/widget/widget.types.ts b/libs/shared/src/types/widget/widget.types.ts index 17bdc4880..2d20d654d 100644 --- a/libs/shared/src/types/widget/widget.types.ts +++ b/libs/shared/src/types/widget/widget.types.ts @@ -8,6 +8,7 @@ export interface IShowPayload { primaryColor?: string; colorScheme?: string; title?: string; + schema?: string; data?: Record[]; } export interface IOption { diff --git a/packages/client/src/api/api.service.ts b/packages/client/src/api/api.service.ts index 821b281b4..5c20cf1b5 100644 --- a/packages/client/src/api/api.service.ts +++ b/packages/client/src/api/api.service.ts @@ -49,12 +49,14 @@ export class ApiService { file: File; authHeaderValue?: string; extra?: string; + schema?: string; }) { const formData = new FormData(); formData.append('file', data.file); if (data.authHeaderValue) formData.append('authHeaderValue', data.authHeaderValue); if (data.extra) formData.append('extra', data.extra); + if (data.schema) formData.append('schema', data.schema); return this.httpClient.post(`/upload/${data.templateId}`, formData, { 'Content-Type': 'multipart/form-data', diff --git a/packages/react/src/hooks/useImpler.ts b/packages/react/src/hooks/useImpler.ts index 8680c7066..b74316bc1 100644 --- a/packages/react/src/hooks/useImpler.ts +++ b/packages/react/src/hooks/useImpler.ts @@ -1,10 +1,12 @@ import { useCallback, useEffect, useState } from 'react'; import { logError } from '../utils/logger'; -import { EventTypesEnum, IShowPayload, IUpload } from '@impler/shared'; +import { EventTypesEnum, IShowPayload, IUpload, ISchemaItem } from '@impler/shared'; import { EventCalls, UploadTemplateData, UploadData } from '../components/button/Button.types'; interface ShowWidgetProps { colorScheme?: 'light' | 'dark'; + schema?: ISchemaItem[]; + data?: Record[]; } interface UseImplerProps { @@ -13,7 +15,6 @@ interface UseImplerProps { templateId?: string; accessToken?: string; primaryColor?: string; - data?: Record[]; extra?: string | Record; authHeaderValue?: string | (() => string) | (() => Promise); onUploadStart?: (value: UploadTemplateData) => void; @@ -30,7 +31,6 @@ export function useImpler({ authHeaderValue, title, extra, - data, onUploadComplete, onWidgetClose, onUploadStart, @@ -74,12 +74,15 @@ export function useImpler({ else initWidget(); }, [accessToken, templateId, initWidget]); - const showWidget = async ({ colorScheme }: ShowWidgetProps) => { + const showWidget = async ({ colorScheme, data, schema }: ShowWidgetProps) => { if (isImplerInitiated) { const payload: IShowPayload = { templateId, data, }; + if (Array.isArray(schema) && schema.length > 0) { + payload.schema = JSON.stringify(schema); + } if (title) payload.title = title; if (colorScheme) payload.colorScheme = colorScheme; else {