import { Component , deserialize , isComponentInstance , ensureComponentClass , assertIsComponentClass , assertIsComponentType , getComponentNameFromComponentClassType , getComponentNameFromComponentInstanceType , NormalizedIdentifierDescriptor , AttributeSelector , createAttributeSelectorFromNames , pickFromAttributeSelector , mergeAttributeSelectors , isIdentifierAttributeInstance , isPrimaryIdentifierAttributeInstance , isComponentValueTypeInstance } from '@layr/component' ; import { StorableComponent , isStorableClass , isStorableInstance , assertIsStorableClass , Query , SortDescriptor , SortDirection , Operator , looksLikeOperator , normalizeOperatorForValue } from '@layr/storable' ; import { isPlainObject , deleteUndefinedProperties , assertNoUnknownOptions , PromiseLikeValue } from 'core-helpers' ; import { serialize as simpleSerialize , deserialize as simpleDeserialize } from 'simple-serialization' ; import mapKeys from 'lodash/mapKeys' ; import { Document , AttributeValue , Projection , buildProjection , DocumentPatch , buildDocumentPatch } from './document' ; import type { Expression } from './expression' ; import type { Path } from './path' ; import { isStoreInstance } from './utilities' ; export type CreateDocumentParams = { collectionName : string ; identifierDescriptor : NormalizedIdentifierDescriptor ; document : Document ; } ; export type ReadDocumentParams = { collectionName : string ; identifierDescriptor : NormalizedIdentifierDescriptor ; projection ? : Projection ; } ; export type UpdateDocumentParams = { collectionName : string ; identifierDescriptor : NormalizedIdentifierDescriptor ; documentPatch : DocumentPatch ; } ; export type DeleteDocumentParams = { collectionName : string ; identifierDescriptor : NormalizedIdentifierDescriptor ; } ; export type FindDocumentsParams = { collectionName : string ; expressions : Expression [ ] ; projection ? : Projection ; sort ? : SortDescriptor ; skip ? : number ; limit ? : number ; } ; export type CountDocumentsParams = { collectionName : string ; expressions : Expression [ ] ; } ; export type MigrateCollectionParams = { collectionName : string ; collectionSchema : CollectionSchema ; silent ? : boolean ; } ; export type MigrateCollectionResult = { name : string ; createdIndexes : string [ ] ; droppedIndexes : string [ ] ; } ; export type CollectionSchema = { indexes : CollectionIndex [ ] ; } ; export type CollectionIndex = { attributes : { [ name ] : SortDirection ; } ; isPrimary : boolean ; isUnique : boolean ; } ; export type TraceEntry = { operation : string ; params : any [ ] ; result ? : any ; error ? : any ; } ; /**
 * An abstract class from which classes such as [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) or [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store) are constructed. Unless you build a custom store, you probably won't have to use this class directly.
 */ export abstract class Store { constructor ( options = { } ) { assertNoUnknownOptions ( options ) ; } getURL ( ) { return undefined ; } _rootComponents = new Set < typeof Component > ( ) ; param rootComponent example category registerRootComponent ( rootComponent ) { assertIsComponentClass ( rootComponent ) ; this . _rootComponents . add ( rootComponent ) ; for ( const component of rootComponent . traverseComponents ( ) ) { if ( isStorableClass ( component ) && ! component . isEmbedded ( ) ) { this . registerStorable ( component ) ; } } } returns category getRootComponents ( ) { return this . _rootComponents . values ( ) ; } _storables = new Map < string , typeof StorableComponent > ( ) ; param name returns example category getStorable ( name ) { const storable = this . _getStorable ( name ) ; if ( storable !== undefined ) { return storable ; } throw new Error ( `The storable component '${ name }' is not registered in the store` ) ; } param name returns example category hasStorable ( name ) { return this . _getStorable ( name ) !== undefined ; } _getStorable ( name ) { return this . _storables . get ( name ) ; } getStorableOfType ( type ) { const storable = this . _getStorableOfType ( type ) ; if ( storable !== undefined ) { return storable ; } throw new Error ( `The storable component of type '${ type }' is not registered in the store` ) ; } _getStorableOfType ( type ) { const isComponentClassType = assertIsComponentType ( type ) === 'componentClassType' ; const componentName = isComponentClassType ? getComponentNameFromComponentClassType ( type ) : getComponentNameFromComponentInstanceType ( type ) ; const component = this . _getStorable ( componentName ) ; if ( component === undefined ) { return undefined ; } return isComponentClassType ? component : component . prototype ; } param storable example category registerStorable ( storable ) { assertIsStorableClass ( storable ) ; if ( storable . isEmbedded ( ) ) { throw new Error ( `Cannot register an embedded storable component (${ storable . describeComponent ( ) })` ) ; } if ( storable . hasStore ( ) ) { if ( storable . getStore ( ) === this ) { return ; } throw new Error ( `Cannot register a storable component that is already registered in another store (${ storable . describeComponent ( ) })` ) ; } const storableName = storable . getComponentName ( ) ; const existingStorable = this . _storables . get ( storableName ) ; if ( existingStorable !== undefined ) { throw new Error ( `A storable component with the same name is already registered (${ existingStorable . describeComponent ( ) })` ) ; } storable . __setStore ( this ) ; this . _storables . set ( storableName , storable ) ; } returns category getStorables ( ) { return this . _storables . values ( ) ; } _getCollectionNameFromStorable ( storable ) { return ensureComponentClass ( storable ) . getComponentName ( ) ; } async load ( storable , options = { } ) { return await this . _runOperation ( 'load' , [ storable , options ] , async ( ) => { let { attributeSelector = true , throwIfMissing = true } = options ; const identifierDescriptor = storable . getIdentifierDescriptor ( ) ; const identifierAttributeSelector = createAttributeSelectorFromNames ( Object . keys ( identifierDescriptor ) ) ; attributeSelector = mergeAttributeSelectors ( attributeSelector , identifierAttributeSelector ) ; const collectionName = this . _getCollectionNameFromStorable ( storable ) ; const documentIdentifierDescriptor = this . toDocument ( storable , identifierDescriptor ) ; const documentAttributeSelector = this . toDocument ( storable , attributeSelector ) ; const projection = buildProjection ( documentAttributeSelector ) ; let document = await this . readDocument ( { collectionName , identifierDescriptor : documentIdentifierDescriptor , projection } ) ; if ( document !== undefined ) { document = pickFromAttributeSelector ( document , documentAttributeSelector , { includeAttributeNames : [ '__component' ] } ) ; const serializedStorable = this . fromDocument ( storable , document ) ; const loadedStorable = storable . deserialize ( serializedStorable , { source : 'store' } ) ; return loadedStorable ; } if ( ! throwIfMissing ) { return undefined ; } throw Object . assign ( new Error ( `Cannot load a component that is missing from the store (${ storable . describeComponent ( ) }, ${ ensureComponentClass ( storable ) . describeIdentifierDescriptor ( identifierDescriptor ) })` ) , { code : 'COMPONENT_IS_MISSING_FROM_STORE' , expose : true } ) ; } ) ; } async save ( storable , options = { } ) { return await this . _runOperation ( 'save' , [ storable , options ] , async ( ) => { const isNew = storable . isNew ( ) ; const { attributeSelector = true , throwIfMissing = ! isNew , throwIfExists = isNew } = options ; if ( throwIfMissing === true && throwIfExists === true ) { throw new Error ( "The 'throwIfMissing' and 'throwIfExists' options cannot be both set to true" ) ; } storable . _assertArrayItemsAreFullyLoaded ( attributeSelector ) ; storable . validate ( attributeSelector ) ; const collectionName = this . _getCollectionNameFromStorable ( storable ) ; const identifierDescriptor = storable . getIdentifierDescriptor ( ) ; const documentIdentifierDescriptor = this . toDocument ( storable , identifierDescriptor ) ; const serializedStorable = storable . serialize ( { attributeSelector , includeIsNewMarks : false } ) ! ; const document = this . toDocument ( storable , serializedStorable ) ; let wasSaved ; try { if ( isNew ) { deleteUndefinedProperties ( document ) ; wasSaved = await this . createDocument ( { collectionName , identifierDescriptor : documentIdentifierDescriptor , document } ) ; } else { const documentPatch = buildDocumentPatch ( document ) ; wasSaved = await this . updateDocument ( { collectionName , identifierDescriptor : documentIdentifierDescriptor , documentPatch } ) ; } } catch ( error ) { const { code , indexName } = error ; if ( code === 'DUPLICATE_KEY_ERROR' ) { throw Object . assign ( new Error ( `Cannot save a component with an attribute value that should be unique but already exists in the store (${ storable . describeComponent ( ) }, ${ ensureComponentClass ( storable ) . describeIdentifierDescriptor ( identifierDescriptor ) }, index: '${ indexName }')` ) , { code : 'UNIQUE_ATTRIBUTE_ALREADY_EXISTS_IN_STORE' , indexName , expose : true } ) ; } throw error ; } if ( ! wasSaved ) { if ( throwIfMissing ) { throw Object . assign ( new Error ( `Cannot save a non-new component that is missing from the store (${ storable . describeComponent ( ) }, ${ ensureComponentClass ( storable ) . describeIdentifierDescriptor ( identifierDescriptor ) })` ) , { code : 'COMPONENT_IS_MISSING_FROM_STORE' , expose : true } ) ; } if ( throwIfExists ) { throw Object . assign ( new Error ( `Cannot save a new component that already exists in the store (${ storable . describeComponent ( ) }, ${ ensureComponentClass ( storable ) . describeIdentifierDescriptor ( identifierDescriptor ) })` ) , { code : 'COMPONENT_ALREADY_EXISTS_IN_STORE' , expose : true } ) ; } return undefined ; } if ( isNew ) { storable . markAsNotNew ( ) ; } storable . traverseAttributes ( ( attribute ) => { attribute . setValueSource ( 'store' ) ; } , { attributeSelector , setAttributesOnly : true } ) ; return storable ; } ) ; } async delete ( storable , options = { } ) { return await this . _runOperation ( 'delete' , [ storable , options ] , async ( ) => { const { throwIfMissing = true } = options ; const collectionName = this . _getCollectionNameFromStorable ( storable ) ; const identifierDescriptor = storable . getIdentifierDescriptor ( ) ; const documentIdentifierDescriptor = this . toDocument ( storable , identifierDescriptor ) ; const wasDeleted = await this . deleteDocument ( { collectionName , identifierDescriptor : documentIdentifierDescriptor } ) ; if ( ! wasDeleted ) { if ( throwIfMissing ) { throw Object . assign ( new Error ( `Cannot delete a component that is missing from the store (${ storable . describeComponent ( ) }, ${ ensureComponentClass ( storable ) . describeIdentifierDescriptor ( identifierDescriptor ) })` ) , { code : 'COMPONENT_IS_MISSING_FROM_STORE' , expose : true } ) ; } return undefined ; } return storable ; } ) ; } async find ( storable , query = { } , options = { } ) { return await this . _runOperation ( 'find' , [ storable , query , options ] , async ( ) => { const { sort = { } , skip , limit } = options ; const storablePrototype = storable . prototype ; const collectionName = this . _getCollectionNameFromStorable ( storablePrototype ) ; const serializedQuery = simpleSerialize ( query ) ; const documentExpressions = this . toDocumentExpressions ( storablePrototype , serializedQuery ) ; const documentSort = this . toDocument ( storablePrototype , sort ) ; const primaryIdentifierAttribute = storablePrototype . getPrimaryIdentifierAttribute ( ) ; const attributeSelector = { [ primaryIdentifierAttribute . getName ( ) ] : true } ; const documentAttributeSelector = this . toDocument ( storablePrototype , attributeSelector ) ; const projection = buildProjection ( documentAttributeSelector ) ; let documents = await this . findDocuments ( { collectionName , expressions : documentExpressions , projection , sort : documentSort , skip , limit } ) ; documents = documents . map ( ( document ) => pickFromAttributeSelector ( document , documentAttributeSelector , { includeAttributeNames : [ '__component' ] } ) ) ; const serializedStorables = documents . map ( ( document ) => this . fromDocument ( storable , document ) ) ; const foundStorables = [ ] ; for ( const serializedStorable of serializedStorables ) { foundStorables . push ( ( await deserialize ( serializedStorable , { rootComponent : storable , source : 'store' } ) ) as StorableComponent ) ; } return foundStorables ; } ) ; } async count ( storable , query = { } ) { return await this . _runOperation ( 'count' , [ storable , query ] , async ( ) => { const storablePrototype = storable . prototype ; const collectionName = this . _getCollectionNameFromStorable ( storablePrototype ) ; const serializedQuery = simpleSerialize ( query ) ; const documentExpressions = this . toDocumentExpressions ( storablePrototype , serializedQuery ) ; const documentsCount = await this . countDocuments ( { collectionName , expressions : documentExpressions } ) ; return documentsCount ; } ) ; } async _runOperation < PromiseResult extends Promise < unknown > , Result = PromiseLikeValue < PromiseResult > > ( operation , params , func ) { const trace = this . _trace ; try { const result = await func ( ) ; if ( trace !== undefined ) { trace . push ( { operation , params , result } ) ; } return result as Result ; } catch ( error ) { if ( trace !== undefined ) { trace . push ( { operation , params , error } ) ; } throw error ; } } _trace : TraceEntry [ ] | undefined ; getTrace ( ) { const trace = this . _trace ; if ( trace === undefined ) { throw new Error ( 'The store is not currently tracing' ) ; } return trace ; } startTrace ( ) { this . _trace = [ ] ; } stopTrace ( ) { this . _trace = undefined ; } abstract migrateCollection ( { collectionName , collectionSchema , silent } ) ; abstract createDocument ( { collectionName , identifierDescriptor , document } ) ; abstract readDocument ( { collectionName , identifierDescriptor , projection } ) ; abstract updateDocument ( { collectionName , identifierDescriptor , documentPatch } ) ; abstract deleteDocument ( { collectionName , identifierDescriptor } ) ; abstract findDocuments ( { collectionName , expressions , projection , sort , skip , limit } ) ; abstract countDocuments ( { collectionName , expressions } ) ; toDocument < Value > ( _storable , value ) { return simpleDeserialize ( value ) as Value ; } toDocumentExpressions ( storable , query ) { const documentQuery = this . toDocument ( storable , query ) ; const build = function ( query , expressions , path ) { for ( const [ name , value ] of Object . entries ( query ) ) { if ( looksLikeOperator ( name ) ) { const operator = name ; if ( operator === '$and' || operator === '$or' || operator === '$nor' ) { handleOperator ( operator , value , expressions , path , { query } ) ; continue ; } throw new Error ( `A query cannot contain the operator '${ operator }' at its root (query: ${ JSON . stringify ( query ) })` ) ; } const subpath = path !== '' ? `${ path }.${ name }` : name ; handleValue ( value , expressions , subpath , { query } ) ; } } ; const handleValue = function ( value , expressions , subpath , { query } ) { if ( ! isPlainObject ( value ) ) { expressions . push ( [ subpath , '$equal' , value ] ) ; return ; } const object = value ; let objectContainsAttributes = false ; let objectContainsOperators = false ; for ( const name of Object . keys ( object ) ) { if ( looksLikeOperator ( name ) ) { objectContainsOperators = true ; } else { objectContainsAttributes = true ; } } if ( objectContainsAttributes ) { if ( objectContainsOperators ) { throw new Error ( `A subquery cannot contain both an attribute and an operator (subquery: ${ JSON . stringify ( object ) })` ) ; } const subquery = object ; build ( subquery , expressions , subpath ) ; return ; } if ( objectContainsOperators ) { const operators = object ; for ( const [ operator , value ] of Object . entries ( operators ) ) { handleOperator ( operator , value , expressions , subpath , { query } ) ; } } } ; const handleOperator = function ( operator , value , expressions , path , { query } ) { const normalizedOperator = normalizeOperatorForValue ( operator , value , { query } ) ; if ( normalizedOperator === '$some' || normalizedOperator === '$every' || normalizedOperator === '$not' ) { const subexpressions = [ ] ; handleValue ( value , subexpressions , '' , { query } ) ; expressions . push ( [ path , normalizedOperator , subexpressions ] ) ; return ; } if ( normalizedOperator === '$and' || normalizedOperator === '$or' || normalizedOperator === '$nor' ) { const values = value as ( AttributeValue | object ) [ ] ; const operatorExpressions = values . map ( ( value ) => { const subexpressions = [ ] ; handleValue ( value , subexpressions , '' , { query } ) ; return subexpressions ; } ) ; expressions . push ( [ path , normalizedOperator , operatorExpressions ] ) ; return ; } if ( isPlainObject ( value ) ) { throw new Error ( `Unexpected object encountered in a query (query: ${ JSON . stringify ( query ) })` ) ; } expressions . push ( [ path , normalizedOperator , value ] ) ; } ; const documentExpressions = [ ] ; build ( documentQuery , documentExpressions , '' ) ; return documentExpressions ; } fromDocument ( _storable , document ) { return simpleSerialize ( document ) ; } param options . silent examplelink category async migrateStorables ( options = { } ) { const { silent = false } = options ; const allResults = { collections : [ ] } ; for ( const storable of this . getStorables ( ) ) { const result = await this . migrateStorable ( storable , { silent } ) ; allResults . collections . push ( result ) ; } return allResults ; } async migrateStorable ( storable , options = { } ) { const { silent = false } = options ; const collectionName = this . _getCollectionNameFromStorable ( storable . prototype ) ; const collectionSchema = this . getCollectionSchema ( storable . prototype ) ; for ( const index of collectionSchema . indexes ) { index . attributes = this . toDocument ( storable . prototype , index . attributes ) ; } const result = await this . migrateCollection ( { collectionName , collectionSchema , silent } ) ; return result ; } getCollectionSchema ( storable ) { const schema = { indexes : this . _getCollectionIndexes ( storable ) } ; return schema ; } _getCollectionIndexes ( storable ) { const indexes = [ ] ; const resolveAttributeName = ( name ) => { const attribute = storable . getAttribute ( name ) ; const scalarType = attribute . getValueType ( ) . getScalarType ( ) ; if ( isComponentValueTypeInstance ( scalarType ) ) { const component = scalarType . getComponent ( attribute ) ; if ( isComponentInstance ( component ) && ! component . constructor . isEmbedded ( ) && component . hasPrimaryIdentifierAttribute ( ) ) { return name + '.' + component . getPrimaryIdentifierAttribute ( { autoFork : false } ) . getName ( ) ; } } return name ; } ; for ( const attribute of storable . getAttributes ( ) ) { if ( isIdentifierAttributeInstance ( attribute ) ) { const isEmbedded = storable . constructor . isEmbedded ( ) ; const attributes = { [ attribute . getName ( ) ] : 'asc' as SortDirection } ; const isPrimary = ! isEmbedded && isPrimaryIdentifierAttributeInstance ( attribute ) ; const isUnique = ! isEmbedded ; indexes . push ( { attributes , isPrimary , isUnique } ) ; continue ; } const name = attribute . getName ( ) ; const resolvedName = resolveAttributeName ( name ) ; if ( name !== resolvedName ) { const attributes = { [ resolvedName ] : 'asc' as SortDirection } ; const isPrimary = false ; const isUnique = false ; indexes . push ( { attributes , isPrimary , isUnique } ) ; continue ; } const scalarType = attribute . getValueType ( ) . getScalarType ( ) ; if ( isComponentValueTypeInstance ( scalarType ) ) { const component = scalarType . getComponent ( attribute ) ; if ( isStorableInstance ( component ) && component . constructor . isEmbedded ( ) ) { const subindexes = this . _getCollectionIndexes ( component ) ; for ( const { attributes , isPrimary , isUnique } of subindexes ) { indexes . push ( { attributes : mapKeys ( attributes , ( _value , subname ) => name + '.' + subname ) , isPrimary , isUnique } ) ; } } } } for ( const index of storable . getIndexes ( { autoFork : false } ) ) { const attributes = mapKeys ( index . getAttributes ( ) , ( _value , name ) => resolveAttributeName ( name ) ) ; const isPrimary = false ; const isUnique = Boolean ( index . getOptions ( ) . isUnique ) ; indexes . push ( { attributes , isPrimary , isUnique } ) ; } return indexes ; } static isStore ( value ) { return isStoreInstance ( value ) ; } }