This commit is contained in:
@@ -20,12 +20,12 @@ namespace AyaNova.Util
|
||||
/////////// CHANGE THIS ON NEW SCHEMA UPDATE ////////////////////
|
||||
|
||||
//!!!!WARNING: BE SURE TO UPDATE THE DbUtil::EmptyBizDataFromDatabaseForSeedingOrImporting WHEN NEW TABLES ADDED!!!!
|
||||
private const int DESIRED_SCHEMA_LEVEL = 15;
|
||||
private const int DESIRED_SCHEMA_LEVEL = 1;
|
||||
|
||||
internal const long EXPECTED_COLUMN_COUNT = 820;
|
||||
internal const long EXPECTED_INDEX_COUNT = 132;
|
||||
internal const long EXPECTED_CHECK_CONSTRAINTS = 367;
|
||||
internal const long EXPECTED_FOREIGN_KEY_CONSTRAINTS = 78;
|
||||
internal const long EXPECTED_COLUMN_COUNT = 850;
|
||||
internal const long EXPECTED_INDEX_COUNT = 133;
|
||||
internal const long EXPECTED_CHECK_CONSTRAINTS = 374;
|
||||
internal const long EXPECTED_FOREIGN_KEY_CONSTRAINTS = 86;
|
||||
internal const long EXPECTED_VIEWS = 6;
|
||||
internal const long EXPECTED_ROUTINES = 2;
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace AyaNova.Util
|
||||
}
|
||||
|
||||
|
||||
//Create schema table (v1)
|
||||
//Create schema table (v0)
|
||||
if (!aySchemaVersionExists)
|
||||
{
|
||||
log.LogDebug("aschemaversion table not found, creating now");
|
||||
@@ -198,12 +198,12 @@ namespace AyaNova.Util
|
||||
cm.CommandText = "CREATE TABLE aschemaversion (schema INTEGER NOT NULL, id TEXT NOT NULL);";
|
||||
await cm.ExecuteNonQueryAsync();
|
||||
|
||||
cm.CommandText = $"insert into aschemaversion (schema, id) values (1,'{AyaNova.Util.Hasher.GenerateSalt()}');";
|
||||
cm.CommandText = $"insert into aschemaversion (schema, id) values (0,'{AyaNova.Util.Hasher.GenerateSalt()}');";
|
||||
await cm.ExecuteNonQueryAsync();
|
||||
|
||||
await ct.Database.CloseConnectionAsync();
|
||||
startingSchema = 1;
|
||||
currentSchema = 1;
|
||||
startingSchema = 0;
|
||||
currentSchema = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -244,9 +244,9 @@ namespace AyaNova.Util
|
||||
//************* SCHEMA UPDATES ******************
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// FOUNDATIONAL TABLES
|
||||
// v8 initial release TABLES
|
||||
//
|
||||
if (currentSchema < 2)
|
||||
if (currentSchema < 1)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
@@ -458,29 +458,9 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
|
||||
//Prime the db with the default SuperUser account
|
||||
await AyaNova.Biz.PrimeData.PrimeSuperUserAccount(ct);
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//LICENSE table
|
||||
if (currentSchema < 3)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
|
||||
//Add user table
|
||||
await ExecQueryAsync("CREATE TABLE alicense (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, dbid TEXT, key TEXT NOT NULL)");
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//WIDGET table for development testing
|
||||
if (currentSchema < 4)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
await ExecQueryAsync("CREATE TABLE alicense (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, dbid TEXT, key TEXT NOT NULL)");
|
||||
|
||||
//Add widget table
|
||||
//id, TEXT, longtext, boolean, currency,
|
||||
@@ -488,16 +468,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
+ "startdate TIMESTAMP, enddate TIMESTAMP, dollaramount DECIMAL(38,18), active BOOL NOT NULL, usertype int4, count INTEGER,"
|
||||
+ "notes TEXT, userid BIGINT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY)");
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// FileAttachment table
|
||||
if (currentSchema < 5)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE afileattachment (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
|
||||
+ "attachtoobjectid BIGINT NOT NULL, attachtoatype INTEGER NOT NULL, "
|
||||
+ "storedfilename TEXT NOT NULL, displayfilename TEXT NOT NULL, contenttype TEXT, lastmodified TIMESTAMP NOT NULL, notes TEXT, exists BOOL NOT NULL, size BIGINT NOT NULL)");
|
||||
@@ -510,85 +480,24 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
//note always query (where clause) in this same order for best performance
|
||||
await ExecQueryAsync("CREATE INDEX idx_afileattachment_attachtoobjectid_attachtoatype ON afileattachment (attachtoobjectid, attachtoatype );");
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// OPS LRO tables
|
||||
if (currentSchema < 6)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE aopsjob (gid uuid PRIMARY KEY, name TEXT NOT NULL, created TIMESTAMP NOT NULL, exclusive BOOL NOT NULL, "
|
||||
+ "startafter TIMESTAMP NOT NULL, jobtype INTEGER NOT NULL, subtype INTEGER, objectid BIGINT, atype INTEGER, jobstatus INTEGER NOT NULL, jobinfo TEXT)");
|
||||
await ExecQueryAsync("CREATE TABLE aopsjoblog (gid uuid PRIMARY KEY, jobid uuid NOT NULL, created TIMESTAMP NOT NULL, statustext TEXT NOT NULL)");
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//DATALISTSAVEDFILTER / DATALISTCOLUMNVIEW
|
||||
if (currentSchema < 7)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE adatalistsavedfilter (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, userid BIGINT NOT NULL, name TEXT NOT NULL, public BOOL NOT NULL, "
|
||||
+ "defaultfilter BOOL NOT NULL, listkey VARCHAR(255) NOT NULL, filter TEXT)");
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE adatalistcolumnview (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, userid BIGINT NOT NULL, "
|
||||
+ "listkey VARCHAR(255) NOT NULL, columns TEXT, sort TEXT)");
|
||||
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// TAGS repository
|
||||
if (currentSchema < 8)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE atag (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, refcount BIGINT NOT NULL)");
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//FORMCUSTOM table
|
||||
if (currentSchema < 9)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE aformcustom (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
|
||||
+ "formkey VARCHAR(255) NOT NULL, template TEXT, UNIQUE(formkey))");
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//PICKLISTTEMPLATE table
|
||||
if (currentSchema < 10)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE apicklisttemplate (id INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ "template TEXT)");
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//MULTIPLE BIZ OBJECT tables
|
||||
if (currentSchema < 11)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
|
||||
//SERVICERATE
|
||||
await ExecQueryAsync("CREATE TABLE aservicerate (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, "
|
||||
@@ -629,7 +538,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
await ExecQueryAsync("CREATE INDEX idx_areview_duedate ON areview (duedate);");
|
||||
await ExecQueryAsync("CREATE INDEX idx_areview_completeddate ON areview (completeddate);");
|
||||
|
||||
|
||||
//SERVICE BANK
|
||||
//Note: I'm allowing negative balances so this code differs slightly from the example it was drawn from https://dba.stackexchange.com/a/19368
|
||||
await ExecQueryAsync("CREATE TABLE aservicebank (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, "
|
||||
@@ -666,13 +574,11 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
+ ")");
|
||||
await ExecQueryAsync("ALTER TABLE auser ADD FOREIGN KEY (customerid) REFERENCES acustomer(id)");
|
||||
|
||||
|
||||
//CUSTOMER NOTES
|
||||
await ExecQueryAsync("CREATE TABLE acustomernote (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
|
||||
+ "customerid BIGINT NOT NULL REFERENCES acustomer(id), userid BIGINT NOT NULL REFERENCES auser(id), "
|
||||
+ "notedate TIMESTAMP NOT NULL, notes TEXT, tags VARCHAR(255) ARRAY )");
|
||||
|
||||
|
||||
//CONTRACTSERVICERATE
|
||||
await ExecQueryAsync("CREATE TABLE acontractservicerate (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, contractid BIGINT NOT NULL REFERENCES acontract ON DELETE CASCADE, "
|
||||
+ "servicerateid BIGINT NOT NULL REFERENCES aservicerate ON DELETE CASCADE"
|
||||
@@ -695,7 +601,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
await ExecQueryAsync("CREATE TABLE acontracttravelrateoverride (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, contractid BIGINT NOT NULL REFERENCES acontract ON DELETE CASCADE, "
|
||||
+ " overridepct DECIMAL(8,5) NOT NULL, overridetype INTEGER NOT NULL CONSTRAINT chk_overridetype_valid CHECK (overridetype > 0 AND overridetype < 3), tags VARCHAR(255) ARRAY)");
|
||||
|
||||
|
||||
//HEADOFFICE
|
||||
await ExecQueryAsync("CREATE TABLE aheadoffice (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY,"
|
||||
@@ -707,7 +612,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
await ExecQueryAsync("ALTER TABLE acustomer ADD column headofficeid BIGINT NULL REFERENCES aheadoffice");
|
||||
await ExecQueryAsync("ALTER TABLE auser ADD FOREIGN KEY (headofficeid) REFERENCES aheadoffice(id)");
|
||||
|
||||
|
||||
//VENDOR
|
||||
await ExecQueryAsync("CREATE TABLE avendor (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, webaddress TEXT, popupnotes TEXT, accountnumber TEXT, "
|
||||
@@ -715,7 +619,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
+ "postaddress TEXT, postcity TEXT, postregion TEXT, postcountry TEXT, postcode TEXT, address TEXT, city TEXT, region TEXT, country TEXT, latitude DECIMAL(9,6), longitude DECIMAL(9,6))");
|
||||
await ExecQueryAsync("ALTER TABLE auser ADD FOREIGN KEY (vendorid) REFERENCES avendor(id)");
|
||||
|
||||
|
||||
//PART
|
||||
await ExecQueryAsync("CREATE TABLE apart (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, "
|
||||
@@ -816,7 +719,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
+ "CONSTRAINT chk_contract_valid CHECK((contractid IS NULL AND contractexpires IS NULL) OR (contractid IS NOT NULL AND contractexpires IS NOT NULL)) "
|
||||
+ " )");
|
||||
|
||||
|
||||
//LOANUNIT
|
||||
await ExecQueryAsync("CREATE TABLE aloanunit (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, "
|
||||
@@ -824,14 +726,11 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
+ "ratehour DECIMAL(38,18) NOT NULL, ratehalfday DECIMAL(38,18) NOT NULL, rateday DECIMAL(38,18) NOT NULL, rateweek DECIMAL(38,18) NOT NULL, ratemonth DECIMAL(38,18) NOT NULL, rateyear DECIMAL(38,18) NOT NULL "
|
||||
+ ")");
|
||||
|
||||
|
||||
|
||||
//----------
|
||||
//WORKORDER STATUS
|
||||
await ExecQueryAsync("CREATE TABLE aworkorderstatus (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, color VARCHAR(12) NOT NULL default '#000000', selectroles INTEGER NOT NULL, removeroles INTEGER NOT NULL, completed BOOL NOT NULL, locked BOOL NOT NULL)");
|
||||
|
||||
|
||||
//WORKORDER
|
||||
await ExecQueryAsync("CREATE TABLE aworkorder (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, serial BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, customerid BIGINT NOT NULL REFERENCES acustomer (id), "
|
||||
@@ -911,10 +810,8 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
await ExecQueryAsync("CREATE TABLE aworkorderitemunit (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, workorderitemid BIGINT NOT NULL REFERENCES aworkorderitem (id), "
|
||||
+ "notes TEXT, customfields TEXT, tags VARCHAR(255) ARRAY)");
|
||||
|
||||
|
||||
//----------
|
||||
|
||||
|
||||
//WORKORDERTEMPLATE
|
||||
await ExecQueryAsync("CREATE TABLE aworkordertemplate (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY )");
|
||||
@@ -968,18 +865,7 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
await ExecQueryAsync("ALTER TABLE aworkorder ADD column fromquoteid BIGINT REFERENCES aquote (id), ADD column frompmid BIGINT REFERENCES apm (id), ADD column fromcsrid BIGINT REFERENCES acustomerservicerequest (id)");
|
||||
|
||||
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// NOTIFICATIONS tables
|
||||
if (currentSchema < 12)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
//NOTIFICATION
|
||||
await ExecQueryAsync("CREATE TABLE anotifysubscription (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
|
||||
+ "userid BIGINT NOT NULL REFERENCES auser (id), ayatype INTEGER NOT NULL, eventtype INTEGER NOT NULL, advancenotice INTERVAL NOT NULL, "
|
||||
+ "idvalue BIGINT NOT NULL, decvalue DECIMAL(38,18) NOT NULL, agevalue INTERVAL NOT NULL, deliverymethod INTEGER NOT NULL, "
|
||||
@@ -992,7 +878,6 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
//basically remove this comment once certain don't need these fields (close to release or after)
|
||||
//idvalue BIGINT NOT NULL, decvalue DECIMAL(38,18) NOT NULL,
|
||||
|
||||
|
||||
await ExecQueryAsync("CREATE TABLE anotification (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, userid BIGINT NOT NULL REFERENCES auser (id), "
|
||||
+ "created TIMESTAMP NOT NULL, ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, name TEXT NOT NULL, eventtype INTEGER NOT NULL, "
|
||||
+ "notifysubscriptionid BIGINT NOT NULL REFERENCES anotifysubscription(id) ON DELETE CASCADE, message TEXT, fetched BOOL NOT NULL)");
|
||||
@@ -1001,41 +886,20 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
+ "ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, eventtype INTEGER NOT NULL, notifysubscriptionid BIGINT NOT NULL, idvalue BIGINT NOT NULL, "
|
||||
+ "decvalue DECIMAL(38,18) NOT NULL, userid BIGINT NOT NULL REFERENCES auser (id), deliverymethod INTEGER NOT NULL, fail BOOL NOT NULL, error TEXT)");
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// LOGO table
|
||||
if (currentSchema < 13)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
//LOGO
|
||||
await ExecQueryAsync("CREATE TABLE alogo (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
|
||||
+ "large bytea, largetype TEXT, medium bytea, mediumtype TEXT, small bytea, smalltype TEXT)");
|
||||
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// REPORT table
|
||||
if (currentSchema < 14)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
//REPORTS
|
||||
await ExecQueryAsync("CREATE TABLE areport (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, "
|
||||
+ "notes TEXT, roles INTEGER NOT NULL, atype INTEGER NOT NULL, template TEXT, style TEXT, jsprerender TEXT, jshelpers TEXT, rendertype INTEGER NOT NULL, "
|
||||
+ "headertemplate TEXT, footertemplate TEXT, displayheaderfooter BOOL, paperformat INTEGER NOT NULL, landscape BOOL, marginoptionsbottom TEXT, "
|
||||
+ "marginoptionsleft TEXT, marginoptionsright TEXT, marginoptionstop TEXT, pageranges TEXT, prefercsspagesize BOOL, printbackground BOOL, scale DECIMAL(8,5) )");
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
|
||||
|
||||
//Load the stock REPORT TEMPLATES
|
||||
await AyaNova.Biz.PrimeData.PrimeReportTemplates();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//DATAFILTER / DATALISTTEMPLATE tables
|
||||
if (currentSchema < 15)
|
||||
{
|
||||
LogUpdateMessage(log);
|
||||
|
||||
//DASHBOARD
|
||||
await ExecQueryAsync("CREATE TABLE adashboardview (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, userid BIGINT NOT NULL UNIQUE, view TEXT NOT NULL)");
|
||||
await SetSchemaLevelAsync(++currentSchema);
|
||||
}
|
||||
@@ -1050,11 +914,11 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// FUTURE
|
||||
// if (currentSchema < 10)
|
||||
// if (currentSchema < xx)
|
||||
// {
|
||||
// LogUpdateMessage(log);
|
||||
|
||||
// setSchemaLevel(++currentSchema);
|
||||
// exec queries here to do updates
|
||||
// await SetSchemaLevelAsync(++currentSchema);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user