2020-12-09 22:43:49 +11:00
using System ;
2017-12-07 16:45:25 +01:00
using System.Collections.Generic ;
using System.Linq ;
using System.Text.RegularExpressions ;
2020-09-17 09:42:55 +02:00
using Microsoft.Extensions.Logging ;
2017-12-07 16:45:25 +01:00
using NPoco ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Cache ;
using Umbraco.Cms.Core.Events ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.Editors ;
2021-05-11 14:33:49 +02:00
using Umbraco.Cms.Core.Notifications ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Persistence ;
using Umbraco.Cms.Core.Persistence.Querying ;
using Umbraco.Cms.Core.Persistence.Repositories ;
using Umbraco.Cms.Core.PropertyEditors ;
2021-02-15 11:41:12 +01:00
using Umbraco.Cms.Core.Scoping ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Serialization ;
using Umbraco.Cms.Core.Services ;
2021-02-12 13:36:50 +01:00
using Umbraco.Cms.Infrastructure.Persistence.Dtos ;
using Umbraco.Cms.Infrastructure.Persistence.Factories ;
using Umbraco.Cms.Infrastructure.Persistence.Querying ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2021-02-09 10:22:42 +01:00
using static Umbraco . Cms . Core . Persistence . SqlExtensionsStatics ;
2017-12-07 16:45:25 +01:00
2021-02-12 13:36:50 +01:00
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
2017-12-07 16:45:25 +01:00
{
internal sealed class ContentRepositoryBase
{
/// <summary>
/// This is used for unit tests ONLY
/// </summary>
2021-03-11 19:35:43 +11:00
public static bool ThrowOnWarning { get ; set ; } = false ;
2017-12-07 16:45:25 +01:00
}
2020-12-22 10:30:16 +11:00
public abstract class ContentRepositoryBase < TId , TEntity , TRepository > : EntityRepositoryBase < TId , TEntity > , IContentRepository < TId , TEntity >
2019-10-23 19:08:03 +11:00
where TEntity : class , IContentBase
2017-12-07 16:45:25 +01:00
where TRepository : class , IRepository
{
2019-10-23 19:08:03 +11:00
private readonly Lazy < PropertyEditorCollection > _propertyEditors ;
2019-12-04 16:14:33 +00:00
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories ;
2021-04-20 12:17:11 +02:00
private readonly IEventAggregator _eventAggregator ;
2019-12-10 08:37:19 +01:00
2019-10-23 19:08:03 +11:00
/// <summary>
2019-10-28 12:53:08 +01:00
///
2019-10-23 19:08:03 +11:00
/// </summary>
/// <param name="scopeAccessor"></param>
/// <param name="cache"></param>
/// <param name="logger"></param>
/// <param name="languageRepository"></param>
/// <param name="propertyEditors">
/// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors
/// </param>
2019-12-11 08:13:51 +01:00
protected ContentRepositoryBase (
IScopeAccessor scopeAccessor ,
2020-09-17 09:42:55 +02:00
AppCaches cache ,
2020-12-22 10:30:16 +11:00
ILogger < EntityRepositoryBase < TId , TEntity > > logger ,
2019-12-11 08:13:51 +01:00
ILanguageRepository languageRepository ,
IRelationRepository relationRepository ,
IRelationTypeRepository relationTypeRepository ,
Lazy < PropertyEditorCollection > propertyEditors ,
DataValueReferenceFactoryCollection dataValueReferenceFactories ,
2021-04-20 12:17:11 +02:00
IDataTypeService dataTypeService ,
IEventAggregator eventAggregator )
2017-12-14 17:04:44 +01:00
: base ( scopeAccessor , cache , logger )
2018-04-21 09:57:28 +02:00
{
2019-12-10 08:37:19 +01:00
DataTypeService = dataTypeService ;
2018-04-21 09:57:28 +02:00
LanguageRepository = languageRepository ;
2019-10-23 19:08:03 +11:00
RelationRepository = relationRepository ;
2019-10-24 16:48:21 +11:00
RelationTypeRepository = relationTypeRepository ;
2019-10-23 19:08:03 +11:00
_propertyEditors = propertyEditors ;
2019-12-04 16:14:33 +00:00
_dataValueReferenceFactories = dataValueReferenceFactories ;
2021-04-20 12:17:11 +02:00
_eventAggregator = eventAggregator ;
2018-04-21 09:57:28 +02:00
}
2017-12-07 16:45:25 +01:00
protected abstract TRepository This { get ; }
2021-03-11 19:35:43 +11:00
/// <summary>
/// Gets the node object type for the repository's entity
/// </summary>
protected abstract Guid NodeObjectTypeId { get ; }
2018-04-21 09:57:28 +02:00
protected ILanguageRepository LanguageRepository { get ; }
2019-12-10 08:37:19 +01:00
protected IDataTypeService DataTypeService { get ; }
2019-10-23 19:08:03 +11:00
protected IRelationRepository RelationRepository { get ; }
2019-10-24 16:48:21 +11:00
protected IRelationTypeRepository RelationTypeRepository { get ; }
2018-04-21 09:57:28 +02:00
2019-10-23 19:08:03 +11:00
protected PropertyEditorCollection PropertyEditors = > _propertyEditors . Value ;
2018-02-01 14:14:45 +01:00
2017-12-07 16:45:25 +01:00
#region Versions
// gets a specific version
public abstract TEntity GetVersion ( int versionId ) ;
// gets all versions, current first
public abstract IEnumerable < TEntity > GetAllVersions ( int nodeId ) ;
2018-10-22 08:45:30 +02:00
// gets all versions, current first
public virtual IEnumerable < TEntity > GetAllVersionsSlim ( int nodeId , int skip , int take )
= > GetAllVersions ( nodeId ) . Skip ( skip ) . Take ( take ) ;
2017-12-07 16:45:25 +01:00
// gets all version ids, current first
public virtual IEnumerable < int > GetVersionIds ( int nodeId , int maxRows )
{
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . GetVersionIds , tsql = >
2017-12-07 16:45:25 +01:00
tsql . Select < ContentVersionDto > ( x = > x . Id )
. From < ContentVersionDto > ( )
. Where < ContentVersionDto > ( x = > x . NodeId = = SqlTemplate . Arg < int > ( "nodeId" ) )
. OrderByDescending < ContentVersionDto > ( x = > x . Current ) // current '1' comes before others '0'
. AndByDescending < ContentVersionDto > ( x = > x . VersionDate ) // most recent first
) ;
return Database . Fetch < int > ( SqlSyntax . SelectTop ( template . Sql ( nodeId ) , maxRows ) ) ;
}
// deletes a specific version
public virtual void DeleteVersion ( int versionId )
{
2019-01-26 09:42:14 -05:00
// TODO: test object node type?
2017-12-07 16:45:25 +01:00
// get the version we want to delete
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . GetVersion , tsql = >
2017-12-07 16:45:25 +01:00
tsql . Select < ContentVersionDto > ( ) . From < ContentVersionDto > ( ) . Where < ContentVersionDto > ( x = > x . Id = = SqlTemplate . Arg < int > ( "versionId" ) )
) ;
var versionDto = Database . Fetch < ContentVersionDto > ( template . Sql ( new { versionId } ) ) . FirstOrDefault ( ) ;
// nothing to delete
if ( versionDto = = null )
return ;
// don't delete the current version
if ( versionDto . Current )
throw new InvalidOperationException ( "Cannot delete the current version." ) ;
PerformDeleteVersion ( versionDto . NodeId , versionId ) ;
}
// deletes all versions of an entity, older than a date.
public virtual void DeleteVersions ( int nodeId , DateTime versionDate )
{
2019-01-26 09:42:14 -05:00
// TODO: test object node type?
2017-12-07 16:45:25 +01:00
// get the versions we want to delete, excluding the current one
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . GetVersions , tsql = >
2017-12-07 16:45:25 +01:00
tsql . Select < ContentVersionDto > ( ) . From < ContentVersionDto > ( ) . Where < ContentVersionDto > ( x = > x . NodeId = = SqlTemplate . Arg < int > ( "nodeId" ) & & ! x . Current & & x . VersionDate < SqlTemplate . Arg < DateTime > ( "versionDate" ) )
) ;
var versionDtos = Database . Fetch < ContentVersionDto > ( template . Sql ( new { nodeId , versionDate } ) ) ;
foreach ( var versionDto in versionDtos )
PerformDeleteVersion ( versionDto . NodeId , versionDto . Id ) ;
}
// actually deletes a version
protected abstract void PerformDeleteVersion ( int id , int versionId ) ;
#endregion
#region Count
/// <summary>
/// Count descendants of an item.
/// </summary>
public int CountDescendants ( int parentId , string contentTypeAlias = null )
{
var pathMatch = parentId = = - 1
? "-1,"
: "," + parentId + "," ;
var sql = SqlContext . Sql ( )
. SelectCount ( )
. From < NodeDto > ( ) ;
if ( contentTypeAlias . IsNullOrWhiteSpace ( ) )
{
sql
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId )
. Where < NodeDto > ( x = > x . Path . Contains ( pathMatch ) ) ;
}
else
{
sql
. InnerJoin < ContentDto > ( )
. On < NodeDto , ContentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < ContentTypeDto > ( )
. On < ContentTypeDto , ContentDto > ( left = > left . NodeId , right = > right . ContentTypeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId )
. Where < NodeDto > ( x = > x . Path . Contains ( pathMatch ) )
. Where < ContentTypeDto > ( x = > x . Alias = = contentTypeAlias ) ;
}
return Database . ExecuteScalar < int > ( sql ) ;
}
/// <summary>
/// Count children of an item.
/// </summary>
public int CountChildren ( int parentId , string contentTypeAlias = null )
{
var sql = SqlContext . Sql ( )
. SelectCount ( )
. From < NodeDto > ( ) ;
if ( contentTypeAlias . IsNullOrWhiteSpace ( ) )
{
sql
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId )
. Where < NodeDto > ( x = > x . ParentId = = parentId ) ;
}
else
{
sql
. InnerJoin < ContentDto > ( )
. On < NodeDto , ContentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < ContentTypeDto > ( )
. On < ContentTypeDto , ContentDto > ( left = > left . NodeId , right = > right . ContentTypeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId )
. Where < NodeDto > ( x = > x . ParentId = = parentId )
. Where < ContentTypeDto > ( x = > x . Alias = = contentTypeAlias ) ;
}
return Database . ExecuteScalar < int > ( sql ) ;
}
/// <summary>
/// Count items.
/// </summary>
public int Count ( string contentTypeAlias = null )
{
var sql = SqlContext . Sql ( )
. SelectCount ( )
. From < NodeDto > ( ) ;
if ( contentTypeAlias . IsNullOrWhiteSpace ( ) )
{
sql
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId ) ;
}
else
{
sql
. InnerJoin < ContentDto > ( )
. On < NodeDto , ContentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < ContentTypeDto > ( )
. On < ContentTypeDto , ContentDto > ( left = > left . NodeId , right = > right . ContentTypeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId )
. Where < ContentTypeDto > ( x = > x . Alias = = contentTypeAlias ) ;
}
return Database . ExecuteScalar < int > ( sql ) ;
}
#endregion
#region Tags
/// <summary>
2018-02-01 14:14:45 +01:00
/// Updates tags for an item.
2017-12-07 16:45:25 +01:00
/// </summary>
2020-11-17 20:27:10 +01:00
protected void SetEntityTags ( IContentBase entity , ITagRepository tagRepo , IJsonSerializer serializer )
2017-12-07 16:45:25 +01:00
{
2018-02-01 14:14:45 +01:00
foreach ( var property in entity . Properties )
{
2019-12-11 08:13:51 +01:00
var tagConfiguration = property . GetTagConfiguration ( PropertyEditors , DataTypeService ) ;
2018-11-20 13:24:06 +01:00
if ( tagConfiguration = = null ) continue ; // not a tags property
if ( property . PropertyType . VariesByCulture ( ) )
{
var tags = new List < ITag > ( ) ;
foreach ( var pvalue in property . Values )
{
2020-11-17 20:27:10 +01:00
var tagsValue = property . GetTagsValue ( PropertyEditors , DataTypeService , serializer , pvalue . Culture ) ;
2018-11-20 13:24:06 +01:00
var languageId = LanguageRepository . GetIdByIsoCode ( pvalue . Culture ) ;
var cultureTags = tagsValue . Select ( x = > new Tag { Group = tagConfiguration . Group , Text = x , LanguageId = languageId } ) ;
tags . AddRange ( cultureTags ) ;
}
tagRepo . Assign ( entity . Id , property . PropertyTypeId , tags ) ;
}
else
{
2020-11-17 20:27:10 +01:00
var tagsValue = property . GetTagsValue ( PropertyEditors , DataTypeService , serializer ) ; // strings
2018-11-20 13:24:06 +01:00
var tags = tagsValue . Select ( x = > new Tag { Group = tagConfiguration . Group , Text = x } ) ;
tagRepo . Assign ( entity . Id , property . PropertyTypeId , tags ) ;
}
2018-02-01 14:14:45 +01:00
}
2017-12-07 16:45:25 +01:00
}
2019-01-26 09:42:14 -05:00
// TODO: should we do it when un-publishing? or?
2017-12-07 16:45:25 +01:00
/// <summary>
2018-02-01 14:14:45 +01:00
/// Clears tags for an item.
2017-12-07 16:45:25 +01:00
/// </summary>
2018-02-01 14:14:45 +01:00
protected void ClearEntityTags ( IContentBase entity , ITagRepository tagRepo )
2017-12-07 16:45:25 +01:00
{
2018-02-01 14:14:45 +01:00
tagRepo . RemoveAll ( entity . Id ) ;
2017-12-07 16:45:25 +01:00
}
#endregion
2018-09-18 11:53:33 +02:00
private Sql < ISqlContext > PreparePageSql ( Sql < ISqlContext > sql , Sql < ISqlContext > filterSql , Ordering ordering )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
// non-filtering, non-ordering = nothing to do
if ( filterSql = = null & & ordering . IsEmpty ) return sql ;
2017-12-07 16:45:25 +01:00
// preserve original
var psql = new Sql < ISqlContext > ( sql . SqlContext , sql . SQL , sql . Arguments ) ;
// apply filter
if ( filterSql ! = null )
psql . Append ( filterSql ) ;
// non-sorting, we're done
2018-09-18 11:53:33 +02:00
if ( ordering . IsEmpty )
2017-12-07 16:45:25 +01:00
return psql ;
2018-09-18 11:53:33 +02:00
// else apply ordering
ApplyOrdering ( ref psql , ordering ) ;
2017-12-07 16:45:25 +01:00
// no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique.
// if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column
// is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831
2018-10-18 14:16:54 +02:00
var ( dbfield , _ ) = SqlContext . VisitDto < NodeDto > ( x = > x . NodeId ) ;
2018-09-18 11:53:33 +02:00
if ( ordering . IsCustomField | | ! ordering . OrderBy . InvariantEquals ( "id" ) )
2017-12-07 16:45:25 +01:00
{
2019-01-21 15:39:19 +01:00
psql . OrderBy ( GetAliasedField ( dbfield , sql ) ) ;
2017-12-07 16:45:25 +01:00
}
// create prepared sql
// ensure it's single-line as NPoco PagingHelper has issues with multi-lines
2018-09-18 11:53:33 +02:00
psql = Sql ( psql . SQL . ToSingleLine ( ) , psql . Arguments ) ;
2018-11-05 13:59:55 +11:00
2018-10-04 13:12:49 +02:00
// replace the magic culture parameter (see DocumentRepository.GetBaseQuery())
if ( ! ordering . Culture . IsNullOrWhiteSpace ( ) )
{
for ( var i = 0 ; i < psql . Arguments . Length ; i + + )
{
if ( psql . Arguments [ i ] is string s & & s = = "[[[ISOCODE]]]" )
{
psql . Arguments [ i ] = ordering . Culture ;
}
}
}
2017-12-07 16:45:25 +01:00
return psql ;
}
2018-09-18 11:53:33 +02:00
private void ApplyOrdering ( ref Sql < ISqlContext > sql , Ordering ordering )
{
if ( sql = = null ) throw new ArgumentNullException ( nameof ( sql ) ) ;
if ( ordering = = null ) throw new ArgumentNullException ( nameof ( ordering ) ) ;
var orderBy = ordering . IsCustomField
? ApplyCustomOrdering ( ref sql , ordering )
: ApplySystemOrdering ( ref sql , ordering ) ;
2018-09-19 17:50:43 +02:00
// beware! NPoco paging code parses the query to isolate the ORDER BY fragment,
// using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything
// else in orderBy is going to break NPoco / not be detected
// beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar]
// to [bar] only, so we MUST use aliases, cannot use [table].[field]
// beware! pre-2012 SqlServer is using a convoluted syntax for paging, which
// includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...",
// so anything added here MUST also be part of the inner SELECT statement, ie
// the original statement, AND must be using the proper alias, as the inner SELECT
// will hide the original table.field names entirely
2018-09-18 11:53:33 +02:00
if ( ordering . Direction = = Direction . Ascending )
sql . OrderBy ( orderBy ) ;
else
sql . OrderByDescending ( orderBy ) ;
}
protected virtual string ApplySystemOrdering ( ref Sql < ISqlContext > sql , Ordering ordering )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
// id is invariant
if ( ordering . OrderBy . InvariantEquals ( "id" ) )
2018-09-19 17:50:43 +02:00
return GetAliasedField ( SqlSyntax . GetFieldName < NodeDto > ( x = > x . NodeId ) , sql ) ;
2018-09-18 11:53:33 +02:00
// sort order is invariant
if ( ordering . OrderBy . InvariantEquals ( "sortOrder" ) )
2018-09-19 17:50:43 +02:00
return GetAliasedField ( SqlSyntax . GetFieldName < NodeDto > ( x = > x . SortOrder ) , sql ) ;
2018-09-18 11:53:33 +02:00
// path is invariant
if ( ordering . OrderBy . InvariantEquals ( "path" ) )
2018-09-19 17:50:43 +02:00
return GetAliasedField ( SqlSyntax . GetFieldName < NodeDto > ( x = > x . Path ) , sql ) ;
2018-09-18 11:53:33 +02:00
// note: 'owner' is the user who created the item as a whole,
// we don't have an 'owner' per culture (should we?)
if ( ordering . OrderBy . InvariantEquals ( "owner" ) )
{
var joins = Sql ( )
. InnerJoin < UserDto > ( "ownerUser" ) . On < NodeDto , UserDto > ( ( node , user ) = > node . UserId = = user . Id , aliasRight : "ownerUser" ) ;
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be selected + aliased
sql = Sql ( InsertBefore ( sql , "FROM" , ", " + SqlSyntax . GetFieldName < UserDto > ( x = > x . UserName , "ownerUser" ) + " AS ordering " ) , sql . Arguments ) ;
2018-09-18 11:53:33 +02:00
sql = InsertJoins ( sql , joins ) ;
2018-09-19 17:50:43 +02:00
return "ordering" ;
2018-09-18 11:53:33 +02:00
}
// note: each version culture variation has a date too,
// maybe we would want to use it instead?
if ( ordering . OrderBy . InvariantEquals ( "versionDate" ) | | ordering . OrderBy . InvariantEquals ( "updateDate" ) )
2018-09-19 17:50:43 +02:00
return GetAliasedField ( SqlSyntax . GetFieldName < ContentVersionDto > ( x = > x . VersionDate ) , sql ) ;
2018-09-18 11:53:33 +02:00
// create date is invariant (we don't keep each culture's creation date)
if ( ordering . OrderBy . InvariantEquals ( "createDate" ) )
2018-09-19 17:50:43 +02:00
return GetAliasedField ( SqlSyntax . GetFieldName < NodeDto > ( x = > x . CreateDate ) , sql ) ;
2018-09-18 11:53:33 +02:00
// name is variant
if ( ordering . OrderBy . InvariantEquals ( "name" ) )
{
// no culture = can only work on the invariant name
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be aliased
2018-09-18 11:53:33 +02:00
if ( ordering . Culture . IsNullOrWhiteSpace ( ) )
2018-09-19 17:50:43 +02:00
return GetAliasedField ( SqlSyntax . GetFieldName < NodeDto > ( x = > x . Text ) , sql ) ;
2018-09-18 11:53:33 +02:00
2018-10-18 14:16:54 +02:00
// "variantName" alias is defined in DocumentRepository.GetBaseQuery
2019-01-26 09:42:14 -05:00
// TODO: what if it is NOT a document but a ... media or whatever?
2018-10-18 14:16:54 +02:00
// previously, we inserted the join+select *here* so we were sure to have it,
// but now that's not the case anymore!
2018-11-05 13:59:55 +11:00
return "variantName" ;
2018-09-18 11:53:33 +02:00
}
2019-04-12 15:04:46 +02:00
// content type alias is invariant
2020-04-07 01:02:08 +10:00
if ( ordering . OrderBy . InvariantEquals ( "contentTypeAlias" ) )
2019-04-12 15:04:46 +02:00
{
var joins = Sql ( )
. InnerJoin < ContentTypeDto > ( "ctype" ) . On < ContentDto , ContentTypeDto > ( ( content , contentType ) = > content . ContentTypeId = = contentType . NodeId , aliasRight : "ctype" ) ;
// see notes in ApplyOrdering: the field MUST be selected + aliased
sql = Sql ( InsertBefore ( sql , "FROM" , ", " + SqlSyntax . GetFieldName < ContentTypeDto > ( x = > x . Alias , "ctype" ) + " AS ordering " ) , sql . Arguments ) ;
sql = InsertJoins ( sql , joins ) ;
return "ordering" ;
}
2018-09-18 11:53:33 +02:00
// previously, we'd accept anything and just sanitize it - not anymore
throw new NotSupportedException ( $"Ordering by {ordering.OrderBy} not supported." ) ;
2017-12-07 16:45:25 +01:00
}
2018-09-18 11:53:33 +02:00
private string ApplyCustomOrdering ( ref Sql < ISqlContext > sql , Ordering ordering )
2017-12-07 16:45:25 +01:00
{
// sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through value
// from 'current' content version for the given order by field
var sortedInt = string . Format ( SqlContext . SqlSyntax . ConvertIntegerToOrderableString , "intValue" ) ;
2018-09-18 11:53:33 +02:00
var sortedDecimal = string . Format ( SqlContext . SqlSyntax . ConvertDecimalToOrderableString , "decimalValue" ) ;
2017-12-07 16:45:25 +01:00
var sortedDate = string . Format ( SqlContext . SqlSyntax . ConvertDateToOrderableString , "dateValue" ) ;
var sortedString = "COALESCE(varcharValue,'')" ; // assuming COALESCE is ok for all syntaxes
// needs to be an outer join since there's no guarantee that any of the nodes have values for this property
var innerSql = Sql ( ) . Select ( $ @ "CASE
WHEN intValue IS NOT NULL THEN { sortedInt }
WHEN decimalValue IS NOT NULL THEN { sortedDecimal }
WHEN dateValue IS NOT NULL THEN { sortedDate }
ELSE { sortedString }
END AS customPropVal ,
cver . nodeId AS customPropNodeId ")
. From < ContentVersionDto > ( "cver" )
2018-09-18 11:53:33 +02:00
. InnerJoin < PropertyDataDto > ( "opdata" )
. On < ContentVersionDto , PropertyDataDto > ( ( version , pdata ) = > version . Id = = pdata . VersionId , "cver" , "opdata" )
. InnerJoin < PropertyTypeDto > ( "optype" ) . On < PropertyDataDto , PropertyTypeDto > ( ( pdata , ptype ) = > pdata . PropertyTypeId = = ptype . Id , "opdata" , "optype" )
. LeftJoin < LanguageDto > ( ) . On < PropertyDataDto , LanguageDto > ( ( pdata , lang ) = > pdata . LanguageId = = lang . Id , "opdata" )
2017-12-07 16:45:25 +01:00
. Where < ContentVersionDto > ( x = > x . Current , "cver" ) // always query on current (edit) values
2018-09-18 11:53:33 +02:00
. Where < PropertyTypeDto > ( x = > x . Alias = = ordering . OrderBy , "optype" )
. Where < PropertyDataDto , LanguageDto > ( ( opdata , lang ) = > opdata . LanguageId = = null | | lang . IsoCode = = ordering . Culture , "opdata" ) ;
// merge arguments
var argsList = sql . Arguments . ToList ( ) ;
var innerSqlString = ParameterHelper . ProcessParams ( innerSql . SQL , innerSql . Arguments , argsList ) ;
2017-12-07 16:45:25 +01:00
2018-09-18 11:53:33 +02:00
// create the outer join complete sql fragment
2017-12-07 16:45:25 +01:00
var outerJoinTempTable = $ @ "LEFT OUTER JOIN ({innerSqlString}) AS customPropData
2021-02-09 10:22:42 +01:00
ON customPropData . customPropNodeId = { Cms . Core . Constants . DatabaseSchema . Tables . Node } . id "; // trailing space is important!
2017-12-07 16:45:25 +01:00
2018-09-18 11:53:33 +02:00
// insert this just above the first WHERE
var newSql = InsertBefore ( sql . SQL , "WHERE" , outerJoinTempTable ) ;
2017-12-07 16:45:25 +01:00
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be selected + aliased
newSql = InsertBefore ( newSql , "FROM" , ", customPropData.customPropVal AS ordering " ) ; // trailing space is important!
2017-12-07 16:45:25 +01:00
2018-09-18 11:53:33 +02:00
// create the new sql
sql = Sql ( newSql , argsList . ToArray ( ) ) ;
2017-12-07 16:45:25 +01:00
// and order by the custom field
2018-09-18 11:53:33 +02:00
// this original code means that an ascending sort would first expose all NULL values, ie items without a value
2018-09-19 17:50:43 +02:00
return "ordering" ;
// note: adding an extra sorting criteria on
// "(CASE WHEN customPropData.customPropVal IS NULL THEN 1 ELSE 0 END")
// would ensure that items without a value always come last, both in ASC and DESC-ending sorts
2017-12-07 16:45:25 +01:00
}
2018-09-18 11:53:33 +02:00
public abstract IEnumerable < TEntity > GetPage ( IQuery < TEntity > query ,
long pageIndex , int pageSize , out long totalRecords ,
IQuery < TEntity > filter ,
Ordering ordering ) ;
2020-04-07 16:42:21 +10:00
public ContentDataIntegrityReport CheckDataIntegrity ( ContentDataIntegrityReportOptions options )
2020-04-07 01:02:08 +10:00
{
2020-04-07 16:42:21 +10:00
var report = new Dictionary < int , ContentDataIntegrityReportEntry > ( ) ;
2020-04-07 01:02:08 +10:00
var sql = SqlContext . Sql ( )
. Select < NodeDto > ( )
. From < NodeDto > ( )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId )
. OrderBy < NodeDto > ( x = > x . Level , x = > x . ParentId , x = > x . SortOrder ) ;
2020-04-07 16:42:21 +10:00
var nodesToRebuild = new Dictionary < int , List < NodeDto > > ( ) ;
var validNodes = new Dictionary < int , NodeDto > ( ) ;
2021-02-09 10:22:42 +01:00
var rootIds = new [ ] { Cms . Core . Constants . System . Root , Cms . Core . Constants . System . RecycleBinContent , Cms . Core . Constants . System . RecycleBinMedia } ;
2020-04-07 17:38:40 +10:00
var currentParentIds = new HashSet < int > ( rootIds ) ;
2020-04-07 01:02:08 +10:00
var prevParentIds = currentParentIds ;
var lastLevel = - 1 ;
// use a forward cursor (query)
foreach ( var node in Database . Query < NodeDto > ( sql ) )
{
if ( node . Level ! = lastLevel )
{
// changing levels
prevParentIds = currentParentIds ;
currentParentIds = null ;
lastLevel = node . Level ;
}
if ( currentParentIds = = null )
{
// we're reset
currentParentIds = new HashSet < int > ( ) ;
}
currentParentIds . Add ( node . NodeId ) ;
2020-04-07 17:38:40 +10:00
// paths parts without the roots
2021-01-22 15:02:25 +13:00
var pathParts = node . Path . Split ( Constants . CharArrays . Comma ) . Where ( x = > ! rootIds . Contains ( int . Parse ( x ) ) ) . ToArray ( ) ;
2020-04-07 01:02:08 +10:00
if ( ! prevParentIds . Contains ( node . ParentId ) )
{
2020-04-07 16:42:21 +10:00
// invalid, this will be because the level is wrong (which prob means path is wrong too)
report . Add ( node . NodeId , new ContentDataIntegrityReportEntry ( ContentDataIntegrityReport . IssueType . InvalidPathAndLevelByParentId ) ) ;
AppendNodeToFix ( nodesToRebuild , node ) ;
2020-04-07 01:02:08 +10:00
}
2020-04-07 17:38:40 +10:00
else if ( pathParts . Length = = 0 )
2020-04-07 01:02:08 +10:00
{
// invalid path
2020-04-07 16:42:21 +10:00
report . Add ( node . NodeId , new ContentDataIntegrityReportEntry ( ContentDataIntegrityReport . IssueType . InvalidPathEmpty ) ) ;
AppendNodeToFix ( nodesToRebuild , node ) ;
2020-04-07 01:02:08 +10:00
}
2020-04-07 17:38:40 +10:00
else if ( pathParts . Length ! = node . Level )
2020-04-07 01:02:08 +10:00
{
// invalid, either path or level is wrong
2020-04-07 16:42:21 +10:00
report . Add ( node . NodeId , new ContentDataIntegrityReportEntry ( ContentDataIntegrityReport . IssueType . InvalidPathLevelMismatch ) ) ;
AppendNodeToFix ( nodesToRebuild , node ) ;
2020-04-07 01:02:08 +10:00
}
else if ( pathParts [ pathParts . Length - 1 ] ! = node . NodeId . ToString ( ) )
{
// invalid path
2020-04-07 16:42:21 +10:00
report . Add ( node . NodeId , new ContentDataIntegrityReportEntry ( ContentDataIntegrityReport . IssueType . InvalidPathById ) ) ;
AppendNodeToFix ( nodesToRebuild , node ) ;
2020-04-07 01:02:08 +10:00
}
2020-04-07 17:38:40 +10:00
else if ( ! rootIds . Contains ( node . ParentId ) & & pathParts [ pathParts . Length - 2 ] ! = node . ParentId . ToString ( ) )
2020-04-07 01:02:08 +10:00
{
// invalid path
2020-04-07 16:42:21 +10:00
report . Add ( node . NodeId , new ContentDataIntegrityReportEntry ( ContentDataIntegrityReport . IssueType . InvalidPathByParentId ) ) ;
AppendNodeToFix ( nodesToRebuild , node ) ;
2020-04-07 01:02:08 +10:00
}
2020-04-07 16:42:21 +10:00
else
{
// it's valid!
2020-04-07 01:02:08 +10:00
2020-04-07 16:42:21 +10:00
// don't track unless we are configured to fix
if ( options . FixIssues )
validNodes . Add ( node . NodeId , node ) ;
}
}
2020-04-07 01:02:08 +10:00
var updated = new List < NodeDto > ( ) ;
2020-04-07 16:42:21 +10:00
if ( options . FixIssues )
2020-04-07 01:02:08 +10:00
{
2020-04-07 16:42:21 +10:00
// iterate all valid nodes to see if these are parents for invalid nodes
foreach ( var ( nodeId , node ) in validNodes )
2020-04-07 01:02:08 +10:00
{
2020-04-07 16:42:21 +10:00
if ( ! nodesToRebuild . TryGetValue ( nodeId , out var invalidNodes ) ) continue ;
2020-04-07 01:02:08 +10:00
2020-04-07 16:42:21 +10:00
// now we can try to rebuild the invalid paths.
2020-04-07 01:02:08 +10:00
2020-04-07 16:42:21 +10:00
foreach ( var invalidNode in invalidNodes )
2020-04-07 01:02:08 +10:00
{
2020-04-07 16:42:21 +10:00
invalidNode . Level = ( short ) ( node . Level + 1 ) ;
invalidNode . Path = node . Path + "," + invalidNode . NodeId ;
updated . Add ( invalidNode ) ;
2020-04-07 01:02:08 +10:00
}
}
2020-04-07 16:42:21 +10:00
foreach ( var node in updated )
{
Database . Update ( node ) ;
2020-04-08 10:38:02 +10:00
if ( report . TryGetValue ( node . NodeId , out var entry ) )
entry . Fixed = true ;
2020-04-07 16:42:21 +10:00
}
2020-04-07 01:02:08 +10:00
}
2020-04-07 16:42:21 +10:00
return new ContentDataIntegrityReport ( report ) ;
2020-04-07 16:53:54 +10:00
}
2020-04-07 16:42:21 +10:00
2020-04-07 16:53:54 +10:00
private static void AppendNodeToFix ( IDictionary < int , List < NodeDto > > nodesToRebuild , NodeDto node )
{
if ( nodesToRebuild . TryGetValue ( node . ParentId , out var childIds ) )
childIds . Add ( node ) ;
else
nodesToRebuild [ node . ParentId ] = new List < NodeDto > { node } ;
2020-04-07 01:02:08 +10:00
}
2018-09-18 11:53:33 +02:00
// here, filter can be null and ordering cannot
2017-12-12 15:04:13 +01:00
protected IEnumerable < TEntity > GetPage < TDto > ( IQuery < TEntity > query ,
2017-12-07 16:45:25 +01:00
long pageIndex , int pageSize , out long totalRecords ,
Func < List < TDto > , IEnumerable < TEntity > > mapDtos ,
2018-09-18 11:53:33 +02:00
Sql < ISqlContext > filter ,
Ordering ordering )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
if ( ordering = = null ) throw new ArgumentNullException ( nameof ( ordering ) ) ;
2017-12-07 16:45:25 +01:00
// start with base query, and apply the supplied IQuery
2018-09-18 11:53:33 +02:00
if ( query = = null ) query = Query < TEntity > ( ) ;
2017-12-07 16:45:25 +01:00
var sql = new SqlTranslator < TEntity > ( GetBaseQuery ( QueryType . Many ) , query ) . Translate ( ) ;
// sort and filter
2018-09-18 11:53:33 +02:00
sql = PreparePageSql ( sql , filter , ordering ) ;
2017-12-07 16:45:25 +01:00
// get a page of DTOs and the total count
var pagedResult = Database . Page < TDto > ( pageIndex + 1 , pageSize , sql ) ;
totalRecords = Convert . ToInt32 ( pagedResult . TotalItems ) ;
// map the DTOs and return
return mapDtos ( pagedResult . Items ) ;
}
protected IDictionary < int , PropertyCollection > GetPropertyCollections < T > ( List < TempContent < T > > temps )
where T : class , IContentBase
{
var versions = new List < int > ( ) ;
foreach ( var temp in temps )
{
versions . Add ( temp . VersionId ) ;
if ( temp . PublishedVersionId > 0 )
versions . Add ( temp . PublishedVersionId ) ;
}
if ( versions . Count = = 0 ) return new Dictionary < int , PropertyCollection > ( ) ;
// get all PropertyDataDto for all definitions / versions
var allPropertyDataDtos = Database . FetchByGroups < PropertyDataDto , int > ( versions , 2000 , batch = >
SqlContext . Sql ( )
. Select < PropertyDataDto > ( )
. From < PropertyDataDto > ( )
. WhereIn < PropertyDataDto > ( x = > x . VersionId , batch ) )
. ToList ( ) ;
// get PropertyDataDto distinct PropertyTypeDto
var allPropertyTypeIds = allPropertyDataDtos . Select ( x = > x . PropertyTypeId ) . Distinct ( ) . ToList ( ) ;
var allPropertyTypeDtos = Database . FetchByGroups < PropertyTypeDto , int > ( allPropertyTypeIds , 2000 , batch = >
SqlContext . Sql ( )
2018-02-01 14:14:45 +01:00
. Select < PropertyTypeDto > ( r = > r . Select ( x = > x . DataTypeDto ) )
2017-12-07 16:45:25 +01:00
. From < PropertyTypeDto > ( )
2018-02-01 14:14:45 +01:00
. InnerJoin < DataTypeDto > ( ) . On < PropertyTypeDto , DataTypeDto > ( ( left , right ) = > left . DataTypeId = = right . NodeId )
2017-12-07 16:45:25 +01:00
. WhereIn < PropertyTypeDto > ( x = > x . Id , batch ) ) ;
// index the types for perfs, and assign to PropertyDataDto
var indexedPropertyTypeDtos = allPropertyTypeDtos . ToDictionary ( x = > x . Id , x = > x ) ;
foreach ( var a in allPropertyDataDtos )
a . PropertyTypeDto = indexedPropertyTypeDtos [ a . PropertyTypeId ] ;
// now we have
2019-01-22 18:03:39 -05:00
// - the definitions
2017-12-07 16:45:25 +01:00
// - all property data dtos
2019-07-30 19:08:37 +10:00
// - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems)
2017-12-07 16:45:25 +01:00
// and we need to build the proper property collections
2019-07-30 19:08:37 +10:00
return GetPropertyCollections ( temps , allPropertyDataDtos ) ;
2017-12-07 16:45:25 +01:00
}
2019-07-30 19:08:37 +10:00
private IDictionary < int , PropertyCollection > GetPropertyCollections < T > ( List < TempContent < T > > temps , IEnumerable < PropertyDataDto > allPropertyDataDtos )
2017-12-07 16:45:25 +01:00
where T : class , IContentBase
{
var result = new Dictionary < int , PropertyCollection > ( ) ;
2019-11-18 13:03:24 +01:00
var compositionPropertiesIndex = new Dictionary < int , IPropertyType [ ] > ( ) ;
2017-12-07 16:45:25 +01:00
// index PropertyDataDto per versionId for perfs
// merge edited and published dtos
var indexedPropertyDataDtos = new Dictionary < int , List < PropertyDataDto > > ( ) ;
foreach ( var dto in allPropertyDataDtos )
{
var versionId = dto . VersionId ;
if ( indexedPropertyDataDtos . TryGetValue ( versionId , out var list ) = = false )
indexedPropertyDataDtos [ versionId ] = list = new List < PropertyDataDto > ( ) ;
list . Add ( dto ) ;
}
foreach ( var temp in temps )
{
// compositionProperties is the property types for the entire composition
// use an index for perfs
if ( compositionPropertiesIndex . TryGetValue ( temp . ContentType . Id , out var compositionProperties ) = = false )
compositionPropertiesIndex [ temp . ContentType . Id ] = compositionProperties = temp . ContentType . CompositionPropertyTypes . ToArray ( ) ;
// map the list of PropertyDataDto to a list of Property
var propertyDataDtos = new List < PropertyDataDto > ( ) ;
if ( indexedPropertyDataDtos . TryGetValue ( temp . VersionId , out var propertyDataDtos1 ) )
{
propertyDataDtos . AddRange ( propertyDataDtos1 ) ;
if ( temp . VersionId = = temp . PublishedVersionId ) // dirty corner case
2018-04-21 09:57:28 +02:00
propertyDataDtos . AddRange ( propertyDataDtos1 . Select ( x = > x . Clone ( - 1 ) ) ) ;
2017-12-07 16:45:25 +01:00
}
if ( temp . VersionId ! = temp . PublishedVersionId & & indexedPropertyDataDtos . TryGetValue ( temp . PublishedVersionId , out var propertyDataDtos2 ) )
propertyDataDtos . AddRange ( propertyDataDtos2 ) ;
2018-04-21 09:57:28 +02:00
var properties = PropertyFactory . BuildEntities ( compositionProperties , propertyDataDtos , temp . PublishedVersionId , LanguageRepository ) . ToList ( ) ;
2017-12-07 16:45:25 +01:00
if ( result . ContainsKey ( temp . VersionId ) )
{
if ( ContentRepositoryBase . ThrowOnWarning )
2018-08-16 12:00:12 +01:00
throw new InvalidOperationException ( $"The query returned multiple property sets for content {temp.Id}, {temp.ContentType.Name}" ) ;
2020-09-16 09:58:07 +02:00
Logger . LogWarning ( "The query returned multiple property sets for content {ContentId}, {ContentTypeName}" , temp . Id , temp . ContentType . Name ) ;
2017-12-07 16:45:25 +01:00
}
result [ temp . VersionId ] = new PropertyCollection ( properties ) ;
}
return result ;
}
2018-09-19 17:50:43 +02:00
protected string InsertBefore ( Sql < ISqlContext > s , string atToken , string insert )
= > InsertBefore ( s . SQL , atToken , insert ) ;
2018-09-18 11:53:33 +02:00
protected string InsertBefore ( string s , string atToken , string insert )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
var pos = s . InvariantIndexOf ( atToken ) ;
if ( pos < 0 ) throw new Exception ( $"Could not find token \" { atToken } \ "." ) ;
return s . Insert ( pos , insert ) ;
}
protected Sql < ISqlContext > InsertJoins ( Sql < ISqlContext > sql , Sql < ISqlContext > joins )
{
var joinsSql = joins . SQL ;
var args = sql . Arguments ;
2017-12-07 16:45:25 +01:00
2018-09-18 11:53:33 +02:00
// merge args if any
if ( joins . Arguments . Length > 0 )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
var argsList = args . ToList ( ) ;
joinsSql = ParameterHelper . ProcessParams ( joinsSql , joins . Arguments , argsList ) ;
args = argsList . ToArray ( ) ;
2017-12-07 16:45:25 +01:00
}
2018-09-18 11:53:33 +02:00
return Sql ( InsertBefore ( sql . SQL , "WHERE" , joinsSql ) , args ) ;
2017-12-07 16:45:25 +01:00
}
2018-09-19 17:50:43 +02:00
private string GetAliasedField ( string field , Sql sql )
{
// get alias, if aliased
//
// regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)"
// and, if found & a group's field matches the field name, returns the alias
//
// so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]"
// then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]"
var matches = SqlContext . SqlSyntax . AliasRegex . Matches ( sql . SQL ) ;
var match = matches . Cast < Match > ( ) . FirstOrDefault ( m = > m . Groups [ 1 ] . Value . InvariantEquals ( field ) ) ;
return match = = null ? field : match . Groups [ 2 ] . Value ;
}
2018-09-18 11:53:33 +02:00
protected string GetQuotedFieldName ( string tableName , string fieldName )
2017-12-07 16:45:25 +01:00
{
return SqlContext . SqlSyntax . GetQuotedTableName ( tableName ) + "." + SqlContext . SqlSyntax . GetQuotedColumnName ( fieldName ) ;
}
2021-04-20 12:17:11 +02:00
#region UnitOfWork Notification
2017-12-07 16:45:25 +01:00
2020-12-09 22:43:49 +11:00
/ *
2021-04-20 12:17:11 +02:00
* The reason why EntityRefreshNotification is published from the repository and not the service is because
* the published state of the IContent must be "Publishing" when the event is raised for the cache to handle it correctly .
* This state is changed half way through the repository method , meaning that if we publish the notification
* after the state will be "Published" and the cache won ' t handle it correctly ,
* It wont call OnRepositoryRefreshed with the published flag set to true , the same is true for unpublishing
* where it wont remove the entity from the nucache .
* We can ' t publish the notification before calling Save method on the repository either ,
* because that method sets certain fields such as create date , update date , etc . . .
2020-12-09 22:43:49 +11:00
* /
2018-10-03 19:03:22 +02:00
2020-12-09 22:43:49 +11:00
/// <summary>
2021-04-20 12:17:11 +02:00
/// Publishes a notification, used to publish <see cref="EntityRefreshNotification{T}"/> for caching purposes.
2020-12-09 22:43:49 +11:00
/// </summary>
2021-04-20 12:17:11 +02:00
protected void OnUowRefreshedEntity ( INotification notification ) = > _eventAggregator . Publish ( notification ) ;
2018-04-28 09:55:36 +02:00
2017-12-07 16:45:25 +01:00
2021-04-20 12:17:11 +02:00
protected void OnUowRemovingEntity ( IContentBase entity ) = > _eventAggregator . Publish ( new ScopedEntityRemoveNotification ( entity , new EventMessages ( ) ) ) ;
2017-12-07 16:45:25 +01:00
#endregion
#region Classes
protected class TempContent
{
public TempContent ( int id , int versionId , int publishedVersionId , IContentTypeComposition contentType )
{
Id = id ;
VersionId = versionId ;
PublishedVersionId = publishedVersionId ;
ContentType = contentType ;
}
/// <summary>
/// Gets or sets the identifier of the content.
/// </summary>
public int Id { get ; set ; }
/// <summary>
/// Gets or sets the version identifier of the content.
/// </summary>
public int VersionId { get ; set ; }
/// <summary>
/// Gets or sets the published version identifier of the content.
/// </summary>
public int PublishedVersionId { get ; set ; }
/// <summary>
/// Gets or sets the content type.
/// </summary>
public IContentTypeComposition ContentType { get ; set ; }
/// <summary>
/// Gets or sets the identifier of the template 1 of the content.
/// </summary>
public int? Template1Id { get ; set ; }
/// <summary>
/// Gets or sets the identifier of the template 2 of the content.
/// </summary>
public int? Template2Id { get ; set ; }
}
protected class TempContent < T > : TempContent
where T : class , IContentBase
{
public TempContent ( int id , int versionId , int publishedVersionId , IContentTypeComposition contentType , T content = null )
: base ( id , versionId , publishedVersionId , contentType )
{
Content = content ;
}
/// <summary>
/// Gets or sets the associated actual content.
/// </summary>
public T Content { get ; set ; }
}
/// <summary>
/// For Paging, repositories must support returning different query for the query type specified
/// </summary>
/// <param name="queryType"></param>
/// <returns></returns>
protected abstract Sql < ISqlContext > GetBaseQuery ( QueryType queryType ) ;
#endregion
#region Utilities
protected virtual string EnsureUniqueNodeName ( int parentId , string nodeName , int id = 0 )
{
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . EnsureUniqueNodeName , tsql = > tsql
2018-04-24 17:59:26 +02:00
. Select < NodeDto > ( x = > Alias ( x . NodeId , "id" ) , x = > Alias ( x . Text , "name" ) )
2017-12-07 16:45:25 +01:00
. From < NodeDto > ( )
2020-12-13 00:28:52 +01:00
. Where < NodeDto > ( x = > x . NodeObjectType = = SqlTemplate . Arg < Guid > ( "nodeObjectType" ) & & x . ParentId = = SqlTemplate . Arg < int > ( "parentId" ) )
) ;
2017-12-07 16:45:25 +01:00
var sql = template . Sql ( NodeObjectTypeId , parentId ) ;
var names = Database . Fetch < SimilarNodeName > ( sql ) ;
return SimilarNodeName . GetUniqueName ( names , id , nodeName ) ;
}
protected virtual int GetNewChildSortOrder ( int parentId , int first )
{
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . GetSortOrder , tsql = > tsql
2020-12-13 00:28:52 +01:00
. Select ( "MAX(sortOrder)" )
. From < NodeDto > ( )
. Where < NodeDto > ( x = > x . NodeObjectType = = SqlTemplate . Arg < Guid > ( "nodeObjectType" ) & & x . ParentId = = SqlTemplate . Arg < int > ( "parentId" ) )
2017-12-07 16:45:25 +01:00
) ;
2020-12-13 00:28:52 +01:00
var sql = template . Sql ( NodeObjectTypeId , parentId ) ;
var sortOrder = Database . ExecuteScalar < int? > ( sql ) ;
return ( sortOrder + 1 ) ? ? first ;
2017-12-07 16:45:25 +01:00
}
protected virtual NodeDto GetParentNodeDto ( int parentId )
{
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . GetParentNode , tsql = > tsql
2020-12-13 00:28:52 +01:00
. Select < NodeDto > ( )
. From < NodeDto > ( )
. Where < NodeDto > ( x = > x . NodeId = = SqlTemplate . Arg < int > ( "parentId" ) )
2017-12-07 16:45:25 +01:00
) ;
2020-12-13 00:28:52 +01:00
var sql = template . Sql ( parentId ) ;
var nodeDto = Database . First < NodeDto > ( sql ) ;
return nodeDto ;
2017-12-07 16:45:25 +01:00
}
protected virtual int GetReservedId ( Guid uniqueId )
{
2021-02-09 10:22:42 +01:00
var template = SqlContext . Templates . Get ( Cms . Core . Constants . SqlTemplates . VersionableRepository . GetReservedId , tsql = > tsql
2020-12-13 00:28:52 +01:00
. Select < NodeDto > ( x = > x . NodeId )
. From < NodeDto > ( )
2021-02-09 10:22:42 +01:00
. Where < NodeDto > ( x = > x . UniqueId = = SqlTemplate . Arg < Guid > ( "uniqueId" ) & & x . NodeObjectType = = Cms . Core . Constants . ObjectTypes . IdReservation )
2017-12-07 16:45:25 +01:00
) ;
2020-12-13 00:28:52 +01:00
var sql = template . Sql ( new { uniqueId } ) ;
var id = Database . ExecuteScalar < int? > ( sql ) ;
2017-12-07 16:45:25 +01:00
return id ? ? 0 ;
}
#endregion
#region Recycle bin
public abstract int RecycleBinId { get ; }
public virtual IEnumerable < TEntity > GetRecycleBin ( )
{
return Get ( Query < TEntity > ( ) . Where ( entity = > entity . Trashed ) ) ;
}
#endregion
2019-10-23 19:08:03 +11:00
protected void PersistRelations ( TEntity entity )
{
2019-12-05 11:18:18 +00:00
// Get all references from our core built in DataEditors/Property Editors
// Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection
2019-10-24 16:48:21 +11:00
var trackedRelations = new List < UmbracoEntityReference > ( ) ;
2019-12-05 11:18:18 +00:00
trackedRelations . AddRange ( _dataValueReferenceFactories . GetAllReferences ( entity . Properties , PropertyEditors ) ) ;
2019-10-24 16:48:21 +11:00
2019-10-25 14:33:40 +11:00
//First delete all auto-relations for this entity
2021-02-09 10:22:42 +01:00
RelationRepository . DeleteByParent ( entity . Id , Cms . Core . Constants . Conventions . RelationTypes . AutomaticRelationTypes ) ;
2019-10-24 16:48:21 +11:00
2019-10-25 14:33:40 +11:00
if ( trackedRelations . Count = = 0 ) return ;
2019-10-24 16:48:21 +11:00
2019-10-28 12:53:08 +01:00
trackedRelations = trackedRelations . Distinct ( ) . ToList ( ) ;
2019-10-24 16:48:21 +11:00
var udiToGuids = trackedRelations . Select ( x = > x . Udi as GuidUdi )
. ToDictionary ( x = > ( Udi ) x , x = > x . Guid ) ;
//lookup in the DB all INT ids for the GUIDs and chuck into a dictionary
var keyToIds = Database . Fetch < NodeIdKey > ( Sql ( ) . Select < NodeDto > ( x = > x . NodeId , x = > x . UniqueId ) . From < NodeDto > ( ) . WhereIn < NodeDto > ( x = > x . UniqueId , udiToGuids . Values ) )
. ToDictionary ( x = > x . UniqueId , x = > x . NodeId ) ;
var allRelationTypes = RelationTypeRepository . GetMany ( Array . Empty < int > ( ) )
. ToDictionary ( x = > x . Alias , x = > x ) ;
2019-11-06 16:45:28 +11:00
var toSave = trackedRelations . Select ( rel = >
{
if ( ! allRelationTypes . TryGetValue ( rel . RelationTypeAlias , out var relationType ) )
throw new InvalidOperationException ( $"The relation type {rel.RelationTypeAlias} does not exist" ) ;
2019-10-24 16:48:21 +11:00
2019-11-06 16:45:28 +11:00
if ( ! udiToGuids . TryGetValue ( rel . Udi , out var guid ) )
return null ; // This shouldn't happen!
2019-10-24 16:48:21 +11:00
2019-11-06 16:45:28 +11:00
if ( ! keyToIds . TryGetValue ( guid , out var id ) )
return null ; // This shouldn't happen!
2019-10-28 12:53:08 +01:00
2020-10-09 09:32:32 +02:00
return new ReadOnlyRelation ( entity . Id , id , relationType . Id ) ;
2019-11-06 16:45:28 +11:00
} ) . WhereNotNull ( ) ;
2020-10-09 09:35:30 +02:00
// Save bulk relations
2020-10-08 14:37:17 +02:00
RelationRepository . SaveBulk ( toSave ) ;
2020-10-09 09:35:30 +02:00
2019-10-24 16:48:21 +11:00
}
2020-10-22 05:30:35 +11:00
/// <summary>
/// Inserts property values for the content entity
/// </summary>
/// <param name="entity"></param>
/// <param name="publishedVersionId"></param>
/// <param name="edited"></param>
/// <param name="editedCultures"></param>
/// <remarks>
/// Used when creating a new entity
/// </remarks>
protected void InsertPropertyValues ( TEntity entity , int publishedVersionId , out bool edited , out HashSet < string > editedCultures )
{
// persist the property data
var propertyDataDtos = PropertyFactory . BuildDtos ( entity . ContentType . Variations , entity . VersionId , publishedVersionId , entity . Properties , LanguageRepository , out edited , out editedCultures ) ;
foreach ( var propertyDataDto in propertyDataDtos )
{
Database . Insert ( propertyDataDto ) ;
}
// TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
// This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
}
/// <summary>
/// Used to atomically replace the property values for the entity version specified
/// </summary>
/// <param name="entity"></param>
/// <param name="versionId"></param>
/// <param name="publishedVersionId"></param>
/// <param name="edited"></param>
/// <param name="editedCultures"></param>
2020-11-02 18:43:35 +01:00
2020-10-22 05:30:35 +11:00
protected void ReplacePropertyValues ( TEntity entity , int versionId , int publishedVersionId , out bool edited , out HashSet < string > editedCultures )
{
// Replace the property data.
// Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic
// and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues.
var propDataSql = SqlContext . Sql ( ) . Select ( "*" ) . From < PropertyDataDto > ( ) . Where < PropertyDataDto > ( x = > x . VersionId = = versionId ) . ForUpdate ( ) ;
var existingPropData = Database . Fetch < PropertyDataDto > ( propDataSql ) ;
2020-11-02 18:43:35 +01:00
var propertyTypeToPropertyData = new Dictionary < ( int propertyTypeId , int versionId , int? languageId , string segment ) , PropertyDataDto > ( ) ;
2020-10-22 05:30:35 +11:00
var existingPropDataIds = new List < int > ( ) ;
foreach ( var p in existingPropData )
{
existingPropDataIds . Add ( p . Id ) ;
2020-11-02 18:43:35 +01:00
propertyTypeToPropertyData [ ( p . PropertyTypeId , p . VersionId , p . LanguageId , p . Segment ) ] = p ;
2020-10-22 05:30:35 +11:00
}
var propertyDataDtos = PropertyFactory . BuildDtos ( entity . ContentType . Variations , entity . VersionId , publishedVersionId , entity . Properties , LanguageRepository , out edited , out editedCultures ) ;
2020-11-02 18:43:35 +01:00
2020-10-22 05:30:35 +11:00
foreach ( var propertyDataDto in propertyDataDtos )
{
2020-11-02 18:43:35 +01:00
2020-10-22 05:30:35 +11:00
// Check if this already exists and update, else insert a new one
2020-11-02 18:43:35 +01:00
if ( propertyTypeToPropertyData . TryGetValue ( ( propertyDataDto . PropertyTypeId , propertyDataDto . VersionId , propertyDataDto . LanguageId , propertyDataDto . Segment ) , out var propData ) )
2020-10-22 05:30:35 +11:00
{
propertyDataDto . Id = propData . Id ;
Database . Update ( propertyDataDto ) ;
}
else
{
// TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
// This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
Database . Insert ( propertyDataDto ) ;
}
// track which ones have been processed
existingPropDataIds . Remove ( propertyDataDto . Id ) ;
}
// For any remaining that haven't been processed they need to be deleted
if ( existingPropDataIds . Count > 0 )
{
Database . Execute ( SqlContext . Sql ( ) . Delete < PropertyDataDto > ( ) . WhereIn < PropertyDataDto > ( x = > x . Id , existingPropDataIds ) ) ;
}
2020-11-02 18:43:35 +01:00
2020-10-22 05:30:35 +11:00
}
2019-10-24 16:48:21 +11:00
private class NodeIdKey
{
[Column("id")]
public int NodeId { get ; set ; }
[Column("uniqueId")]
public Guid UniqueId { get ; set ; }
2019-10-23 19:08:03 +11:00
}
2017-12-07 16:45:25 +01:00
}
}