// // Copyright © 2023 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import { type Employee } from '@hcengineering/contact' import core, { type AttachedData, type Class, type Ref, type TxOperations, Blob, Mixin } from '@hcengineering/core' import { type ControlledDocument, type Document, type DocumentCategory, type DocumentMeta, type DocumentSpace, type DocumentTemplate, type HierarchyDocument, type Project, type ProjectDocument, DocumentState } from './types' import { makeRank } from '@hcengineering/rank' import documents from './plugin' import { getDocumentId, getFirstRank, TEMPLATE_PREFIX } from './utils' async function getParentPath (client: TxOperations, parent: Ref): Promise>> { const parentDocObj = await client.findOne(documents.class.ProjectDocument, { _id: parent }) if (parentDocObj === undefined) { console.warn(`Couldn't find the parent project document object with id: ${parent}`) return [] } const parentMeta = await client.findOne(documents.class.ProjectMeta, { _id: parentDocObj.attachedTo }) if (parentMeta === undefined) { console.warn(`Couldn't find the parent document meta with id: ${parentDocObj.attachedTo}`) return [] } return [parentMeta.meta, ...parentMeta.path] } export async function createControlledDocFromTemplate ( client: TxOperations, templateId: Ref | undefined, documentId: Ref, spec: AttachedData, space: Ref, project: Ref | undefined, parent: Ref | undefined, docClass: Ref> = documents.class.ControlledDocument ): Promise<{ seqNumber: number, success: boolean }> { if (templateId == null) { return { seqNumber: -1, success: false } } // Try fast path first (assumes template sequence is in sync) let { seqNumber, prefix, content, category } = await useDocumentTemplate(client, templateId, false) let actualCode = getDocumentId({ prefix, seqNumber }) let { success, documentMetaId } = await createControlledDocMetadata( client, templateId, documentId, space, project, parent, prefix, seqNumber, actualCode, spec.title ) // If creation failed due to seqNumber conflict, retry with full uniqueness check if (!success) { // Retry with expensive check to find actual max seqNumber const retryResult = await useDocumentTemplate(client, templateId, true) seqNumber = retryResult.seqNumber prefix = retryResult.prefix content = retryResult.content category = retryResult.category actualCode = getDocumentId({ prefix, seqNumber }) const retryMetadata = await createControlledDocMetadata( client, templateId, documentId, space, project, parent, prefix, seqNumber, actualCode, spec.title ) success = retryMetadata.success documentMetaId = retryMetadata.documentMetaId } if (!success) { return { seqNumber: -1, success: false } } await client.addCollection( docClass, space, documentMetaId, documents.class.DocumentMeta, 'documents', { ...spec, category, template: templateId, seqNumber, prefix, state: DocumentState.Draft, content }, documentId ) return { seqNumber, success: true } } /** * Calculate the next available seqNumber by checking existing documents with the template. */ async function calculateNextSeqNumberWithCheck ( client: TxOperations, templateId: Ref, currentTemplateSequence: number ): Promise { const existingDocs = await client.findAll( documents.class.Document, { template: templateId }, { projection: { seqNumber: 1 } } ) const maxExistingSeqNumber = existingDocs.length > 0 ? Math.max(...existingDocs.map((doc) => doc.seqNumber ?? 0)) : -1 return Math.max(currentTemplateSequence, maxExistingSeqNumber) + 1 } export async function useDocumentTemplate ( client: TxOperations, templateId: Ref, checkExisting: boolean = false ): Promise<{ seqNumber: number, prefix: string, content: Ref | null, category: Ref }> { const template = await client.findOne(documents.mixin.DocumentTemplate, { _id: templateId }) if (template === undefined) { return { seqNumber: -1, prefix: '', content: null, category: '' as Ref } } let nextSeqNumber: number if (checkExisting) { nextSeqNumber = await calculateNextSeqNumberWithCheck(client, templateId, template.sequence) } else { nextSeqNumber = template.sequence + 1 } // Update template sequence to nextSeqNumber in a single atomic operation await client.updateMixin(templateId, documents.class.Document, template.space, documents.mixin.DocumentTemplate, { sequence: nextSeqNumber }) const prefix = template.docPrefix return { seqNumber: nextSeqNumber, prefix, content: template.content, category: template.category as Ref } } export async function createControlledDocMetadata ( client: TxOperations, templateId: Ref, documentId: Ref, space: Ref, project: Ref | undefined, parent: Ref | undefined, prefix: string, seqNumber: number, specCode: string, specTitle: string, metaId?: Ref ): Promise<{ success: boolean seqNumber: number documentMetaId: Ref projectDocumentId: Ref }> { const projectId = project ?? documents.ids.NoProject const ops = client.apply('create-qms-document') ops.notMatch(documents.class.Document, { template: templateId, seqNumber }) ops.notMatch(documents.class.Document, { code: specCode }) const documentMetaId = await ops.createDoc( documents.class.DocumentMeta, space, { documents: 0, title: `${specCode} ${specTitle}` }, metaId ) let path: Array> = [] if (parent !== undefined) { path = await getParentPath(client, parent) } const parentMeta = path[0] ?? documents.ids.NoParent const lastRank = await getFirstRank(client, space, projectId, parentMeta) const projectMetaId = await ops.createDoc(documents.class.ProjectMeta, space, { project: projectId, meta: documentMetaId, path, parent: parentMeta, documents: 0, rank: makeRank(lastRank, undefined) }) const projectDocumentId = await client.addCollection( documents.class.ProjectDocument, space, projectMetaId, documents.class.ProjectMeta, 'documents', { project: projectId, initial: projectId, document: documentId } ) const success = await ops.commit() if (!success.result) { console.warn('createControlledDocMetadata: ops.commit() failed', { templateId, documentId, space, project, parent, prefix, seqNumber, specCode, specTitle, documentMetaId, projectDocumentId }) } return { success: success.result, seqNumber, documentMetaId, projectDocumentId } } export async function createDocumentTemplate ( client: TxOperations, _class: Ref>, space: Ref, _mixin: Ref>, project: Ref | undefined, parent: Ref | undefined, templateId: Ref, prefix: string, spec: Omit, 'prefix'>, category: Ref, author?: Ref ): Promise<{ seqNumber: number, success: boolean }> { const { success, seqNumber, code, documentMetaId } = await createDocumentTemplateMetadata( client, _class, space, _mixin, project, parent, templateId, prefix, spec.code ?? '', spec.title ) if (!success) { return { seqNumber: -1, success: false } } const ops = client.apply() await ops.addCollection( _class, space, documentMetaId, documents.class.DocumentMeta, 'documents', { ...spec, code, seqNumber, category, prefix: TEMPLATE_PREFIX, author, owner: author, content: spec.content ?? null }, templateId ) await ops.createMixin(templateId, documents.class.Document, space, _mixin, { sequence: 0, docPrefix: prefix }) const commit = await ops.commit() if (!commit.result) { console.warn('createDocumentTemplate: ops.commit() failed', { _class, space, _mixin, project, parent, templateId, prefix, category, author }) } return { seqNumber, success: commit.result } } export async function createDocumentTemplateMetadata ( client: TxOperations, _class: Ref>, space: Ref, _mixin: Ref>, project: Ref | undefined, parent: Ref | undefined, templateId: Ref, prefix: string, specCode: string, specTitle: string, metaId?: Ref ): Promise<{ success: boolean seqNumber: number code: string documentMetaId: Ref projectDocumentId: Ref }> { const projectId = project ?? documents.ids.NoProject const incResult = await client.updateDoc( core.class.Sequence, documents.space.Documents, documents.sequence.Templates, { $inc: { sequence: 1 } }, true ) const seqNumber = (incResult as any).object.sequence as number const code = specCode === '' ? `${TEMPLATE_PREFIX}-${seqNumber}` : specCode let path: Array> = [] if (parent !== undefined) { path = await getParentPath(client, parent) } const ops = client.apply('create-qms-document') ops.notMatch(documents.class.Document, { template: { $exists: false }, seqNumber }) ops.notMatch(documents.class.Document, { code }) ops.notMatch(documents.mixin.DocumentTemplate, { docPrefix: prefix }) const documentMetaId = await ops.createDoc( documents.class.DocumentMeta, space, { documents: 0, title: `${code} ${specTitle}` }, metaId ) const parentMeta = path[0] ?? documents.ids.NoParent const lastRank = await getFirstRank(client, space, projectId, parentMeta) const projectMetaId = await ops.createDoc(documents.class.ProjectMeta, space, { project: projectId, meta: documentMetaId, path, parent: path[0] ?? documents.ids.NoParent, documents: 0, rank: makeRank(lastRank, undefined) }) const projectDocumentId = await client.addCollection( documents.class.ProjectDocument, space, projectMetaId, documents.class.ProjectMeta, 'documents', { project: projectId, initial: projectId, document: templateId } ) const success = await ops.commit() if (!success.result) { console.warn('createDocumentTemplateMetadata: ops.commit() failed', { _class, space, _mixin, project, parent, templateId, prefix, specCode, specTitle, seqNumber, code, documentMetaId, projectDocumentId }) } return { success: success.result, seqNumber, code, documentMetaId, projectDocumentId } } export async function createNewFolder ( client: TxOperations, space: Ref, project: Ref | undefined, parent: Ref | undefined, title: string ): Promise<{ success: boolean documentMetaId: Ref projectDocumentId: Ref }> { const projectId = project ?? documents.ids.NoProject const ops = client.apply() const documentMetaId = await ops.createDoc(documents.class.DocumentMeta, space, { documents: 0, title }) let path: Array> = [] if (parent !== undefined) { path = await getParentPath(client, parent) } const parentMeta = path[0] ?? documents.ids.NoParent const lastRank = await getFirstRank(client, space, projectId, parentMeta) const projectMetaId = await ops.createDoc(documents.class.ProjectMeta, space, { project: projectId, meta: documentMetaId, path, parent: parentMeta, documents: 0, rank: makeRank(lastRank, undefined) }) const projectDocumentId = await client.addCollection( documents.class.ProjectDocument, space, projectMetaId, documents.class.ProjectMeta, 'documents', { project: projectId, initial: projectId, document: documents.ids.Folder } ) const success = await ops.commit() if (!success.result) { console.warn('createNewFolder: ops.commit() failed', { space, project, parent, title, documentMetaId, projectDocumentId }) } return { success: success.result, documentMetaId, projectDocumentId } }