// // Copyright © 2020, 2021 Anticrm Platform Contributors. // // 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 { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref, Space } from './classes' import core from './component' import { Hierarchy } from './hierarchy' import { isPredicate } from './predicate' import { FindResult, DocumentQuery } from './storage' function toHex (value: number, chars: number): string { const result = value.toString(16) if (result.length < chars) { return '0'.repeat(chars - result.length) + result } return result } let counter = (Math.random() * (1 << 24)) | 0 const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4) function timestamp (): string { const time = (Date.now() / 1000) | 0 return toHex(time, 8) } function count (): string { const val = counter++ & 0xffffff return toHex(val, 6) } /** * @public * @returns */ export function generateId (): Ref { return (timestamp() + random + count()) as Ref } let currentAccount: Account /** * @public * @returns */ export function getCurrentAccount (): Account { return currentAccount } /** * @public * @param account - */ export function setCurrentAccount (account: Account): void { currentAccount = account } /** * @public */ export function escapeLikeForRegexp (value: string): string { return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') } /** * @public */ export function toFindResult (docs: T[], total?: number): FindResult { const length = total ?? docs.length return Object.assign(docs, { total: length }) } /** * @public */ export interface WorkspaceId { name: string productId: string } /** * @public * * Combine workspace with productId, if not equal '' */ export function getWorkspaceId (workspace: string, productId: string = ''): WorkspaceId { return { name: workspace, productId } } /** * @public */ export function toWorkspaceString (id: WorkspaceId, sep = '@'): string { return id.name + (id.productId === '' ? '' : sep + id.productId) } const attributesPrefix = 'attributes.' /** * @public */ export interface IndexKeyOptions { _class?: Ref> docId?: Ref extra?: string[] relative?: boolean refAttribute?: string } /** * @public */ export function docUpdKey (name: string, opt?: IndexKeyOptions): string { return attributesPrefix + docKey(name, opt) } /** * @public */ export function docKey (name: string, opt?: IndexKeyOptions): string { const extra = opt?.extra !== undefined && opt?.extra?.length > 0 ? `#${opt.extra?.join('#') ?? ''}` : '' let key = (opt?.docId !== undefined ? opt.docId.split('.').join('_') + '|' : '') + (opt?._class === undefined ? name : `${opt?._class}%${name}${extra}`) if (opt?.refAttribute !== undefined) { key = `${opt?.refAttribute}->${key}` } if (opt?.refAttribute !== undefined || (opt?.relative !== undefined && opt?.relative)) { key = '|' + key } return key } /** * @public */ export function extractDocKey (key: string): { _class?: Ref> attr: string docId?: Ref extra: string[] } { let k = key if (k.startsWith(attributesPrefix)) { k = k.slice(attributesPrefix.length) } let docId: Ref | undefined let _class: Ref> | undefined let attr = '' const docSepPos = k.indexOf('|') if (docSepPos !== -1) { docId = k.substring(0, docSepPos).replace('_', '.') as Ref k = k.substring(docSepPos + 1) } const clPos = k.indexOf('%') if (clPos !== -1) { _class = k.substring(0, clPos) as Ref> attr = k.substring(clPos + 1) } else { attr = k } const extra = attr.split('#') attr = extra.splice(0, 1)[0] return { docId, attr, _class, extra } } /** * @public */ export function isFullTextAttribute (attr: AnyAttribute): boolean { return ( attr.index === IndexKind.FullText || attr.type._class === core.class.TypeAttachment || attr.type._class === core.class.EnumOf ) } /** * @public */ export function isIndexedAttribute (attr: AnyAttribute): boolean { return attr.index === IndexKind.Indexed } /** * @public */ export interface IdMap extends Map, T> {} /** * @public */ export function toIdMap (arr: T[]): IdMap { return new Map(arr.map((p) => [p._id, p])) } /** * @public */ export function concatLink (host: string, path: string): string { if (!host.endsWith('/') && !path.startsWith('/')) { return `${host}/${path}` } else if (host.endsWith('/') && path.startsWith('/')) { const newPath = path.slice(1) return `${host}${newPath}` } else { return `${host}${path}` } } /** * @public */ export function fillDefaults ( hierarchy: Hierarchy, object: DocData | T, _class: Ref> ): DocData | T { const baseClass = hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc const attributes = hierarchy.getAllAttributes(_class, baseClass) for (const attribute of attributes) { if (attribute[1].defaultValue !== undefined) { if ((object as any)[attribute[0]] === undefined) { ;(object as any)[attribute[0]] = attribute[1].defaultValue } } } return object } /** * @public */ export class AggregateValueData { constructor ( readonly name: string, readonly _id: Ref, readonly space: Ref, readonly rank?: string, readonly category?: Ref ) {} getRank (): string { return this.rank ?? '' } } /** * @public */ export class AggregateValue { constructor (readonly name: string | undefined, readonly values: AggregateValueData[]) {} } /** * @public */ export type CategoryType = number | string | undefined | Ref | AggregateValue /** * @public */ export class DocManager { protected readonly byId: IdMap constructor (protected readonly docs: Doc[]) { this.byId = toIdMap(docs) } get (ref: Ref): Doc | undefined { return this.byId.get(ref) } getDocs (): Doc[] { return this.docs } getIdMap (): IdMap { return this.byId } filter (predicate: (value: Doc) => boolean): Doc[] { return this.docs.filter(predicate) } } /** * @public */ export class RateLimitter { idCounter: number = 0 processingQueue = new Map>() last: number = 0 queue: (() => Promise)[] = [] constructor (readonly config: () => { rate: number, perSecond?: number }) {} async exec = {}>(op: (args?: B) => Promise, args?: B): Promise { const processingId = `${this.idCounter++}` const cfg = this.config() while (this.processingQueue.size > cfg.rate) { await Promise.race(this.processingQueue.values()) } try { const p = op(args) this.processingQueue.set(processingId, p as Promise) return await p } finally { this.processingQueue.delete(processingId) } } async add = {}>(op: (args?: B) => Promise, args?: B): Promise { const cfg = this.config() if (this.processingQueue.size < cfg.rate) { void this.exec(op, args) } else { await this.exec(op, args) } } async waitProcessing (): Promise { await await Promise.race(this.processingQueue.values()) } } export function mergeQueries (query1: DocumentQuery, query2: DocumentQuery): DocumentQuery { const q = Object.assign({}, query1) for (const k in query2) { if (!Object.keys(query1).includes(k)) { Object.assign(q, { [k]: query2[k] }) continue } Object.assign(q, { [k]: getInNiN(query1[k], query2[k]) }) if (isPredicate(query2[k]) || isPredicate(query1[k])) { const toIterate = isPredicate(query2[k]) ? query2[k] : query1[k] for (const x in toIterate) { if (['$lt', '$gt'].includes(x)) { const val1 = isPredicate(query1[k]) ? query1[k][x] : query1[k] const val2 = isPredicate(query2[k]) ? query2[k][x] : query2[k] if (x === '$lt') { Object.assign(q, { [k]: { $lt: val1 < val2 ? val1 : val2 } }) continue } if (x === '$gt') { Object.assign(q, { [k]: { $gt: val1 > val2 ? val1 : val2 } }) } } } } } return q } function getInNiN (query1: any, query2: any): Object { const aIn = (typeof query1 === 'object' && '$in' in query1 ? query1.$in : undefined) ?? (typeof query1 !== 'object' && query1 !== undefined ? [query1] : []) const aNIn = (typeof query1 === 'object' && '$nin' in query1 ? query1.$nin : undefined) ?? (typeof query1 === 'object' && query1.$ne !== undefined ? [query1.$ne] : []) const bIn = (typeof query2 === 'object' && '$in' in query2 ? query2.$in : undefined) ?? (typeof query2 !== 'object' && query2 !== undefined ? [query2] : []) const bNIn = (typeof query2 === 'object' && '$nin' in query2 ? query2.$nin : undefined) ?? (typeof query2 === 'object' && query2.$ne !== undefined ? [query2.$ne] : []) const finalIn = aIn.length - bIn.length < 0 ? bIn.filter((c: any) => aIn.includes(c)) : aIn.filter((c: any) => bIn.includes(c)) const finalNin = Array.from(new Set([...aNIn, ...bNIn])) if (finalIn.length === 1 && finalNin.length === 0) { return finalIn[0] } if (finalIn.length === 0 && finalNin.length === 1) { return { $ne: finalNin[0] } } const res: any = {} if (finalIn.length > 0) { res.$in = finalIn } if (finalNin.length > 0) { res.$nin = finalNin } if (aIn.length === 1 && bIn.length === 1) return [] return res }