mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-11 12:02:35 +02:00
added support for 0 based sequential ids to our object db.
This commit is contained in:
parent
a0c7ccfe7c
commit
c6fa3cc02b
@ -18,7 +18,7 @@ public RootDirModule(RootFolderService rootFolderService)
|
||||
|
||||
Get["/"] = x => GetRootFolders();
|
||||
Post["/"] = x => AddRootFolder();
|
||||
Delete["/{id}"] = x => DeleteRootFolder((long)x.id);
|
||||
Delete["/{id}"] = x => DeleteRootFolder((int)x.id);
|
||||
}
|
||||
|
||||
private Response AddRootFolder()
|
||||
@ -32,7 +32,7 @@ private Response GetRootFolders()
|
||||
return _rootFolderService.All().AsResponse();
|
||||
}
|
||||
|
||||
private Response DeleteRootFolder(long folderId)
|
||||
private Response DeleteRootFolder(int folderId)
|
||||
{
|
||||
_rootFolderService.Remove(folderId);
|
||||
return new Response { StatusCode = HttpStatusCode.OK };
|
||||
|
69
NzbDrone.Core.Test/Datastore/IndexProviderFixture.cs
Normal file
69
NzbDrone.Core.Test/Datastore/IndexProviderFixture.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Repository;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
[TestFixture]
|
||||
public class IndexProviderFixture : ObjectDbTest<IndexProvider>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
WithObjectDb();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_get_sequential_numbers()
|
||||
{
|
||||
var indexs = new List<int>();
|
||||
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
indexs.Add(Subject.Next(GetType()));
|
||||
}
|
||||
|
||||
indexs.Should().OnlyHaveUniqueItems();
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Test]
|
||||
public void diffrentTypes_should_get_their_own_counter()
|
||||
{
|
||||
var seriesIndex = new List<int>();
|
||||
var episodeIndex = new List<int>();
|
||||
|
||||
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
seriesIndex.Add(Subject.Next(typeof(Series)));
|
||||
}
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
episodeIndex.Add(Subject.Next(typeof(Episode)));
|
||||
}
|
||||
|
||||
seriesIndex.Should().OnlyHaveUniqueItems();
|
||||
episodeIndex.Should().OnlyHaveUniqueItems();
|
||||
|
||||
seriesIndex.Min(c => c).Should().Be(1);
|
||||
seriesIndex.Max(c => c).Should().Be(200);
|
||||
|
||||
episodeIndex.Min(c => c).Should().Be(1);
|
||||
episodeIndex.Max(c => c).Should().Be(100);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Eloquera.Client;
|
||||
using FizzWare.NBuilder;
|
||||
@ -85,13 +86,40 @@ public void should_update_nested_objects()
|
||||
[Test]
|
||||
public void new_objects_should_get_id()
|
||||
{
|
||||
testSeries.Id = 0;
|
||||
Db.Insert(testSeries);
|
||||
testSeries.Id.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void new_existing_object_should_get_new_id()
|
||||
{
|
||||
testSeries.Id = 0;
|
||||
Db.Insert(testSeries);
|
||||
Db.Insert(testSeries);
|
||||
|
||||
Db.AsQueryable<Series>().Should().HaveCount(1);
|
||||
testSeries.Id.Should().Be(1);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_assign_ids_to_nested_objects()
|
||||
{
|
||||
var nested = new NestedModel();
|
||||
|
||||
nested.List.Add(new NestedModel());
|
||||
|
||||
Db.Insert(nested);
|
||||
|
||||
nested.Id.Should().Be(1);
|
||||
nested.List.Should().OnlyContain(c => c.Id > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_have_id_when_returned_from_database()
|
||||
{
|
||||
testSeries.Id = 0;
|
||||
Db.Insert(testSeries);
|
||||
var item = Db.AsQueryable<Series>();
|
||||
|
||||
@ -122,5 +150,15 @@ public class UnknownType : BaseRepositoryModel
|
||||
{
|
||||
public string Field1 { get; set; }
|
||||
}
|
||||
|
||||
public class NestedModel : BaseRepositoryModel
|
||||
{
|
||||
public NestedModel()
|
||||
{
|
||||
List = new List<NestedModel> { this };
|
||||
}
|
||||
|
||||
public IList<NestedModel> List { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,35 @@
|
||||
|
||||
namespace NzbDrone.Core.Test.Framework
|
||||
{
|
||||
|
||||
public abstract class ObjectDbTest<TSubject> : ObjectDbTest where TSubject : class
|
||||
{
|
||||
private TSubject _subject;
|
||||
|
||||
[SetUp]
|
||||
public void CoreTestSetup()
|
||||
{
|
||||
_subject = null;
|
||||
}
|
||||
|
||||
protected TSubject Subject
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_subject == null)
|
||||
{
|
||||
_subject = Mocker.Resolve<TSubject>();
|
||||
}
|
||||
|
||||
return _subject;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ObjectDbTest : CoreTest
|
||||
{
|
||||
|
||||
private EloqueraDb _db;
|
||||
protected EloqueraDb Db
|
||||
{
|
||||
@ -32,6 +59,7 @@ protected void WithObjectDb(bool memory = true)
|
||||
}
|
||||
|
||||
Mocker.SetConstant(Db);
|
||||
Mocker.SetConstant(Db.Db);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
|
@ -146,6 +146,7 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Datastore\IndexProviderFixture.cs" />
|
||||
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
|
||||
<Compile Include="Framework\CoreTest.cs" />
|
||||
<Compile Include="Framework\ObjectDbTest.cs" />
|
||||
|
@ -30,6 +30,30 @@
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.Integeration\.ServiceIntegerationFixture\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.QualityProfileTest\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ProviderTests\.TvDbProviderTest\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ProviderTests\.MediaFileProviderTest\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ProviderTests\.EpisodeProviderTests\.EpisodeProviderTest_DeleteInvalidEpisodes\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ProviderTests\.DecisionEngineTests\.UpgradeHistorySpecificationFixture\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ProviderTests\.DecisionEngineTests\.QualityAllowedByProfileSpecificationFixture\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ParserTests\.QualityParserFixture\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
<RegexTestSelector>
|
||||
<RegularExpression>NzbDrone\.Core\.Test\.ParserTests\.ParserFixture\..*</RegularExpression>
|
||||
</RegexTestSelector>
|
||||
</IgnoredTests>
|
||||
<AdditionalFilesToInclude>..\NzbDrone.Core\bin\Debug\Eloquera.Server.exe</AdditionalFilesToInclude>
|
||||
<HiddenWarnings>PostBuildEventDisabled</HiddenWarnings>
|
||||
|
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using Eloquera.Client;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
@ -9,6 +6,8 @@ namespace NzbDrone.Core.Datastore
|
||||
public abstract class BaseRepositoryModel
|
||||
{
|
||||
[ID]
|
||||
public long Id;
|
||||
private long _eqId;
|
||||
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ namespace NzbDrone.Core.Datastore
|
||||
public interface IBasicRepository<TModel>
|
||||
{
|
||||
List<TModel> All();
|
||||
TModel Get(long rootFolderId);
|
||||
TModel Get(int rootFolderId);
|
||||
TModel Add(TModel rootFolder);
|
||||
void Delete(long rootFolderId);
|
||||
void Delete(int rootFolderId);
|
||||
}
|
||||
|
||||
public class BasicRepository<TModel> : IBasicRepository<TModel> where TModel : BaseRepositoryModel, new()
|
||||
@ -25,7 +25,7 @@ public List<TModel> All()
|
||||
return EloqueraDb.AsQueryable<TModel>().ToList();
|
||||
}
|
||||
|
||||
public TModel Get(long id)
|
||||
public TModel Get(int id)
|
||||
{
|
||||
return EloqueraDb.AsQueryable<TModel>().Single(c => c.Id == id);
|
||||
}
|
||||
@ -35,7 +35,7 @@ public TModel Add(TModel model)
|
||||
return EloqueraDb.Insert(model);
|
||||
}
|
||||
|
||||
public void Delete(long id)
|
||||
public void Delete(int id)
|
||||
{
|
||||
var itemToDelete = Get(id);
|
||||
EloqueraDb.Delete(itemToDelete);
|
||||
|
@ -1,49 +1,56 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Eloquera.Client;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class EloqueraDb : IDisposable
|
||||
{
|
||||
private readonly DB _db;
|
||||
private readonly IdService _idService;
|
||||
public DB Db { get; private set; }
|
||||
|
||||
public EloqueraDb(DB db)
|
||||
public EloqueraDb(DB db, IdService idService)
|
||||
{
|
||||
_db = db;
|
||||
_idService = idService;
|
||||
Db = db;
|
||||
}
|
||||
|
||||
public IEnumerable<T> AsQueryable<T>()
|
||||
{
|
||||
return _db.Query<T>();
|
||||
return Db.Query<T>();
|
||||
}
|
||||
|
||||
public T Insert<T>(T obj) where T : BaseRepositoryModel
|
||||
{
|
||||
obj.Id = _db.Store(obj);
|
||||
_idService.EnsureIds(obj, new HashSet<object>());
|
||||
Db.Store(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
public IList<T> InsertMany<T>(IEnumerable<T> objects) where T : BaseRepositoryModel
|
||||
public IList<T> InsertMany<T>(IList<T> objects) where T : BaseRepositoryModel
|
||||
{
|
||||
_idService.EnsureIds(objects, new HashSet<object>());
|
||||
return DoMany(objects, Insert);
|
||||
}
|
||||
|
||||
public T Update<T>(T obj)
|
||||
{
|
||||
_db.Store(obj);
|
||||
Db.Store(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
public IList<T> UpdateMany<T>(IEnumerable<T> objects)
|
||||
public IList<T> UpdateMany<T>(IList<T> objects)
|
||||
{
|
||||
_idService.EnsureIds(objects, new HashSet<object>());
|
||||
return DoMany(objects, Update);
|
||||
}
|
||||
|
||||
public void Delete<T>(T obj) where T : new()
|
||||
{
|
||||
_db.Delete(obj);
|
||||
Db.Delete(obj);
|
||||
}
|
||||
|
||||
public void DeleteMany<T>(IEnumerable<T> objects) where T : new()
|
||||
@ -59,9 +66,10 @@ private IList<T> DoMany<T>(IEnumerable<T> objects, Func<T, T> function)
|
||||
return objects.Select(function).ToList();
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
Db.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,18 +53,18 @@ private EloqueraDb InternalCreate(string connectionString, string databaseName)
|
||||
|
||||
//This seemse to cause Invalid Cast Exceptions... WTF
|
||||
//db.RefreshMode = ObjectRefreshMode.AlwaysReturnUpdatedValues;
|
||||
|
||||
|
||||
RegisterTypeRules();
|
||||
RegisterTypes(db);
|
||||
|
||||
return new EloqueraDb(db);
|
||||
return new EloqueraDb(db, new IdService(new IndexProvider(db)));
|
||||
}
|
||||
|
||||
private void RegisterTypeRules()
|
||||
{
|
||||
RootFolder rootFolder = null;
|
||||
DB.TypeRules
|
||||
//.SetIDField(() => rootFolder.Id)
|
||||
//.SetIDField(() => rootFolder.Id)
|
||||
.IgnoreProperty(() => rootFolder.FreeSpace)
|
||||
.IgnoreProperty(() => rootFolder.UnmappedFolders);
|
||||
|
||||
|
96
NzbDrone.Core/Datastore/IdService.cs
Normal file
96
NzbDrone.Core/Datastore/IdService.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class IdService
|
||||
{
|
||||
private readonly IndexProvider _indexProvider;
|
||||
|
||||
private static readonly ConcurrentDictionary<string, IList<PropertyInfo>> propertyCache = new ConcurrentDictionary<string, IList<PropertyInfo>>();
|
||||
|
||||
public IdService(IndexProvider indexProvider)
|
||||
{
|
||||
_indexProvider = indexProvider;
|
||||
}
|
||||
|
||||
public void EnsureIds<T>(T obj, HashSet<object> context)
|
||||
{
|
||||
//context is use to prevent infinite loop if objects are recursively looped.
|
||||
if (obj == null || context.Contains(obj))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Add(obj);
|
||||
|
||||
var modelBase = obj as BaseRepositoryModel;
|
||||
|
||||
if (modelBase != null && modelBase.Id == 0)
|
||||
{
|
||||
modelBase.Id = _indexProvider.Next(obj.GetType());
|
||||
}
|
||||
|
||||
foreach (var propertyInfo in GetPotentialProperties(obj.GetType()))
|
||||
{
|
||||
var propValue = propertyInfo.GetValue(obj, null);
|
||||
|
||||
var list = propValue as IEnumerable;
|
||||
|
||||
if (list != null)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
EnsureIds(item, context);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureIds(propValue, context);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private IList<PropertyInfo> GetPotentialProperties(Type type)
|
||||
{
|
||||
IList<PropertyInfo> result;
|
||||
if (!propertyCache.TryGetValue(type.FullName, out result))
|
||||
{
|
||||
result = type.GetProperties().Where(ShouldCrawl).ToList();
|
||||
propertyCache.TryAdd(type.FullName, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ShouldCrawl(PropertyInfo propertyInfo)
|
||||
{
|
||||
return propertyInfo.CanRead && ShouldCrawl(propertyInfo.PropertyType);
|
||||
}
|
||||
|
||||
private bool ShouldCrawl(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericArg = type.GetGenericArguments()[0];
|
||||
|
||||
//skip if generic argument type isn't interesting
|
||||
if (!ShouldCrawl(genericArg))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var listType = typeof(IList<>).MakeGenericType(genericArg);
|
||||
return listType.IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
return type.IsClass && type.FullName.StartsWith("NzbDrone");
|
||||
}
|
||||
}
|
||||
}
|
81
NzbDrone.Core/Datastore/IndexProvider.cs
Normal file
81
NzbDrone.Core/Datastore/IndexProvider.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Eloquera.Client;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IProvideIndex
|
||||
{
|
||||
int Next(Type type);
|
||||
}
|
||||
|
||||
public class IndexProvider : IProvideIndex
|
||||
{
|
||||
private readonly DB _db;
|
||||
|
||||
private static object _lock = new object();
|
||||
|
||||
public IndexProvider(DB db)
|
||||
{
|
||||
_db = db;
|
||||
|
||||
if (db.IsTypeRegistered(typeof(IndexList)))
|
||||
{
|
||||
db.RegisterType(typeof(IndexList));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_db.Query<IndexList>().Count();
|
||||
|
||||
}
|
||||
catch (EloqueraException ex)
|
||||
{
|
||||
_db.Store(new IndexList());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public int Next(Type type)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
var key = type.Name;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var indexList = _db.Query<IndexList>().Single();
|
||||
|
||||
var indexInfo = indexList.SingleOrDefault(c => c.Type == key);
|
||||
|
||||
if (indexInfo == null)
|
||||
{
|
||||
indexInfo = new IndexInfo { Type = key };
|
||||
indexList.Add(indexInfo);
|
||||
}
|
||||
|
||||
indexInfo.Index++;
|
||||
|
||||
_db.Store(indexList);
|
||||
|
||||
return indexInfo.Index;
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexList : List<IndexInfo> { }
|
||||
|
||||
public class IndexInfo
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -236,6 +236,8 @@
|
||||
<Compile Include="Datastore\ConnectionFactory.cs" />
|
||||
<Compile Include="Datastore\EloqueraDb.cs" />
|
||||
<Compile Include="Datastore\EloqueraDbFactory.cs" />
|
||||
<Compile Include="Datastore\IdService.cs" />
|
||||
<Compile Include="Datastore\IndexProvider.cs" />
|
||||
<Compile Include="Datastore\MigrationLogger.cs" />
|
||||
<Compile Include="Datastore\MigrationsHelper.cs" />
|
||||
<Compile Include="Datastore\CustomeMapper.cs" />
|
||||
|
@ -13,7 +13,7 @@ public interface IRootFolderService
|
||||
{
|
||||
List<RootFolder> All();
|
||||
RootFolder Add(RootFolder rootDir);
|
||||
void Remove(long rootDirId);
|
||||
void Remove(int rootDirId);
|
||||
List<String> GetUnmappedFolders(string path);
|
||||
Dictionary<string, ulong> FreeSpaceOnDrives();
|
||||
}
|
||||
@ -63,7 +63,7 @@ public virtual RootFolder Add(RootFolder rootFolder)
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
public virtual void Remove(long rootDirId)
|
||||
public virtual void Remove(int rootDirId)
|
||||
{
|
||||
_rootFolderRepository.Delete(rootDirId);
|
||||
}
|
||||
|
@ -150,7 +150,7 @@
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>1306</DevelopmentServerPort>
|
||||
<DevelopmentServerPort>28501</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:1306/</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
|
@ -366,7 +366,7 @@
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>17584</DevelopmentServerPort>
|
||||
<DevelopmentServerPort>28496</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:62182/</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<SolutionConfiguration>
|
||||
<FileVersion>1</FileVersion>
|
||||
<AutoEnableOnStartup>False</AutoEnableOnStartup>
|
||||
<AutoEnableOnStartup>True</AutoEnableOnStartup>
|
||||
<AllowParallelTestExecution>true</AllowParallelTestExecution>
|
||||
<AllowTestsToRunInParallelWithThemselves>true</AllowTestsToRunInParallelWithThemselves>
|
||||
<FrameworkUtilisationTypeForNUnit>UseDynamicAnalysis</FrameworkUtilisationTypeForNUnit>
|
||||
|
Loading…
Reference in New Issue
Block a user