import CryptoJS from 'crypto-js' import dayjs from 'dayjs' import IsBetween from 'dayjs/plugin/isBetween' import { castArray, chunk, difference, get, isEqual, isString, noop, partition, pick, set, uniq, } from 'lodash' import log from 'loglevel' import { action, IMapDidChange, makeObservable, observable, observe, reaction, runInAction, } from 'mobx' import { createPatch } from 'rfc6902' import { findFileNameConflicts } from '../files' import { $SchemaIdTemplate, AttributeType, DateQuery, ElementSchema, ElementType, List, Reference, SchemaId, Stage, Template, TemplateId, } from '../interfaces' import { AttributeData, AttributeDefinition, AttributeDefinitions, AttributeFileData, AttributeGroup, AttributeValue, AttributeValueMultiple, ConditionalViewConfig, ElementAttributeData, ElementBatchCreateData, ElementData, ElementFormData, ElementRaw, ElementsPrivileges, ElementsTemplates, ElementTemplateV1, ElementTypePrivilegesList, ILogicalOperator, instanceOfLocalElement, isFileData, isLocalFile, isRemoteFile, LocalElement, Operation, Operations, PlatformFile, PreparedFiles, RemoveOperation, ViewConfigCondition, } from '../interfaces/element.interface' import { DehydratedElement } from '../interfaces/offline.interface' import { CheckList, CheckPoint, Record as RecordModel, Ticket } from '../models' import { CommitState, isLocal } from '../models/_mixin/isLocal' import { isElementObject, TICKET_SPECIAL_ATTRIBUTES } from '../models/constants' import { cloneElementData, Element, getAttributeNameFromOperation, getPatchedElementFromOperations, } from '../models/element' import { getSchemaId, urlToSchemaId } from '../schema/schemaIdUrl' import { BatchCreateResponse, BatchMetadata, FilesBatchPostData, IBatchEditAttributeDataV2, } from '../services/edocu/batch' import { DEFAULT_TEMPLATE_ID } from '../services/edocu/editors' import { ElementGetQueryParams, ElementPrivileges, ElementSearchOptions, GetElementSearch, TYPE_LIST_PER_PAGE, } from '../services/edocu/elements' import { isArrayLike } from '../util/isArrayLike' import { transformType } from '../util/transformType' import { MainStore } from './mainStore' dayjs.extend(IsBetween) const MAX_PER_BATCH = 250 interface InstantiateElementOptions { setToMemory: boolean extendWithOlderData: boolean } interface SyncJobs { create: LocalElement[] update: Element[] } type SyncProgress = { done: number failed: number total: number } const comparisonFuncs: Record< ILogicalOperator, ( elementAttrValue: AttributeValue, comparedValue: AttributeValue & DateQuery, ) => boolean > = { '===': (elementAttrValue: AttributeValue, comparedValue: AttributeValue) => comparedValue === elementAttrValue, '!==': (elementAttrValue: AttributeValue, comparedValue: AttributeValue) => comparedValue !== elementAttrValue, '!!': (elementAttrValue: AttributeValue) => !!elementAttrValue, '!': (elementAttrValue: AttributeValue) => !elementAttrValue, IN: (elementAttrValue: AttributeValue, comparedValue: DateQuery) => { const isValidDate = isValidDateString(elementAttrValue as string) return ( isValidDate && comparedValue && isBetween(elementAttrValue as string, comparedValue) ) }, NOTIN: (elementAttrValue: AttributeValue, comparedValue: DateQuery) => { const isValidDate = isValidDateString(elementAttrValue as string) return ( isValidDate && comparedValue && !isBetween(elementAttrValue as string, comparedValue) ) }, CONTAINS: ( elementAttrValue: AttributeValue, comparedValue: AttributeValue, ) => { return !!( elementAttrValue && comparedValue && typeof elementAttrValue === 'string' && typeof comparedValue === 'string' && elementAttrValue.includes(comparedValue) ) }, } function isBetween(date: string, comparedObj: DateQuery) { return dayjs(date).isBetween( comparedObj.$gte || '', comparedObj.$lte || '', undefined, '[]', ) } export function isValidDateString(date: string | undefined) { return dayjs( date, [ 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]', 'YYYY-MM-DD[T]HH:mm:ss[Z]', 'YYYY-MM-DD[T]HH:mm:ss.SSSSSSSSS[Z]', ], true, ).isValid() } export class ElementStore { static listeners: (() => void)[] = [] // V1 typeData and templates - Legacy elementsTypeDataV1: Map = new Map() elementTemplatesV1: ElementsTemplates = new Map() workRecordActions: Map = new Map() // V2 schema and template elementSchemas: Map = new Map() elementTemplates: Map = new Map() privileges: ElementsPrivileges = new Map() elements: Map = new Map() syncProgress: SyncProgress | undefined = undefined socketEventIds: string[] = [] main: MainStore constructor(main: MainStore) { makeObservable(this, { elementsTypeDataV1: observable, elementTemplatesV1: observable, workRecordActions: observable.shallow, elementSchemas: observable.shallow, elementTemplates: observable.shallow, privileges: observable, elements: observable.shallow, syncProgress: observable, socketEventIds: observable, addTypeData: action, addWorkRecordActions: action, addTemplateV1Data: action, addPrivilegesData: action, getElementSchema: action, setSchema: action, saveSchema: action, addSchema: action, getTemplate: action, saveTemplate: action, setTemplate: action, create: action, createTicket: action, rehydrateElement: action, instantiateElement: action, applyPatchToElement: action, }) this.main = main observe(this.elements, (change) => this.onElementsChange(change)) reaction( () => this.main.connectivityService?.isConnected, () => this.onConnectionChange(), ) } addTypeData(type: string, typeData: AttributeDefinitions) { this.elementsTypeDataV1.set(type, typeData) } addWorkRecordActions(type: string, actions: string[]) { this.workRecordActions.set(type, actions) } async search( query: string, options?: ElementSearchOptions, ): Promise { return this.main.elements.search(query, options) } addTemplateV1Data( type: string, organization: string, template: ElementTemplateV1, ) { const typeTemplate = this.elementTemplatesV1.get(type) this.elementTemplatesV1.set(type, { ...(typeTemplate || {}), [organization]: template, }) } addPrivilegesData( type: ElementType, organization: string, privileges: ElementPrivileges, ) { const typePrivileges = this.privileges.get(type) if (typePrivileges && typePrivileges?.[organization]) { return } this.privileges.set(type, { ...(typePrivileges || {}), [organization]: privileges, }) } async get( hash: string, type: ElementType, queryParams?: ElementGetQueryParams, ): Promise { const element = this.instantiateElement( hash, { _type: type, } as ElementData, false, { setToMemory: false }, ) as T if (!element) { return null } const res = await element.loadData(false, queryParams) // only set AFTER loading data if (res?.ok) { this.elements.set(hash, element) } if (element.errored) { throw element.errored } return element } async getElementTypeData( type: string, ): Promise { const typeData = this.elementsTypeDataV1.get(type) if (typeData) { return typeData } const typeDataRequest = await this.main.elements.getTypeDataV1(type) if (!typeDataRequest.ok || !typeDataRequest.data) { return } this.addTypeData(type, typeDataRequest.data.attributes) this.addWorkRecordActions(type, typeDataRequest.data.actions) return typeDataRequest.data.attributes } async getElementTemplateV1(type: string, organization: string) { const template = this.elementTemplatesV1.get(type) if (template && template[organization]) { return template[organization] } const templateRequest = await this.main.elements.getTemplateV1( type, organization, ) if (!templateRequest.ok || !templateRequest.data) { return } this.addTemplateV1Data(type, organization, templateRequest.data) return templateRequest.data } async getElementSchema( type: ElementType, options?: { stage?: Stage; useStoreCache?: boolean; cache?: false }, ): Promise { const schemaId = getSchemaId(type) const sameStage = !options?.stage || options?.stage === this.main.config.stage const useStoreCache = sameStage && (options?.useStoreCache ?? true) const schema = this.elementSchemas.get(schemaId) if (useStoreCache && schema) { return schema } const schemaRes = await this.main.editors.getTypeSchema(schemaId, options) // Schema does not exist if (schemaRes.status === 404) { return } if (!schemaRes.ok || !schemaRes.data) { return } runInAction(() => { if (useStoreCache && schemaRes.data) { this.setSchema(schemaId, schemaRes.data) } }) return schemaRes.data } setSchema(schemaId: SchemaId, schema: ElementSchema) { this.elementSchemas.set(schemaId, schema) } async saveSchema( type: ElementType, schema: ElementSchema, options?: { stage?: Stage }, ) { const schemaId = getSchemaId(type) const useStoreCache = !options?.stage || options?.stage === this.main.config.stage const res = await this.main.editors.saveTypeSchema( schemaId, schema, options, ) if (!res.ok || !res.data) { throw res.originalError } runInAction(() => { if (useStoreCache && res.data) { this.setSchema(schemaId, res.data) } }) return res.data } async addSchema( schema: Required>, options?: { stage?: Stage }, ) { const res = await this.main.editors.addSchema(schema, options) const useStoreCache = !options?.stage || options?.stage === this.main.config.stage if (!res.ok || !res.data) { throw res.originalError } runInAction(() => { if (useStoreCache && res.data) { const schemaId = getSchemaId(res.data.$id) this.setSchema(schemaId, res.data) } }) return res.data } async getTemplate( type: ElementType, templateId = DEFAULT_TEMPLATE_ID, options?: { stage?: Stage useStoreCache?: boolean cache?: false strict?: boolean }, ) { const schemaId = getSchemaId(type) const id = getTemplateId(schemaId, templateId) const sameStage = !options?.stage || options?.stage === this.main.config.stage const useStoreCache = sameStage && (options?.useStoreCache ?? true) const template = this.elementTemplates.get(id) if (useStoreCache && template) { return template } const templateDataRequest = await this.main.editors.getTemplate( schemaId, templateId, options, ) if (!templateDataRequest.ok || !templateDataRequest.data) { return } runInAction(() => { if (useStoreCache && templateDataRequest.data) { this.setTemplate(schemaId, templateDataRequest.data, templateId) } }) return templateDataRequest.data } async saveTemplate( schemaId: SchemaId, templateId?: TemplateId, data?: Partial