Ving Record
Records are the functional implementation of a ving schema. They automatically generate a ton of functionality from making queries easier, to building web services, to setting up privileges and more.
Records vs Kinds
A ving record is technically 2 separate classes. The first is called a "Kind", which is technically a group or list of records. In relational database terms, think of a kind as a table. The second type is the record, which is an instance of a kind, which in relational database terms is a row within a table. Both classes exist inside the record file. See #ving/record/records/User.mjs
as an example implementation of a record, or #ving/record/VingRecord.mjs
to see the base class that all ving records inherit from.
Creating Records
You can use the CLI to automatically generate a new record file for you from a Drizzle table. So if you've created a table called Foo
then you could create a new record file like this:
./ving.mjs record --new Foo
Note that you will need a Ving Schema for
Foo
before the record can function.
That will generate the file #ving/record/records/Foo.mjs
. And in there you could add any custom functionality you may need. Or if you don't need any custom functionality, then it may work just as it is.
Once you're done adding functionality you can then generate a Rest API for it by invoing the CLI again like this:
./ving.mjs record --rest Foo
Those files will be placed in server/api/v1/foo
and you can modify them as needed, but they should work without modification. And you can access them at http://localhost:3000/api/v1/foo
.
And if you want to build a user interface for your services, you can generate that too by invoking the CLI once more.
./ving.mjs record --web Foo
Those files will be placed in pages/foo
and you can modify them as needed as well. And you can access them at http://localhost:3000/foo
.
Kind API
The Kind is akin to a relational database table. To start with you need to get a reference to it:
import {useKind} from '#ving/record/VingKind.mjs';
const users = await useKind('User');
Creating Records
You can create records many different ways. In all three methods you'll pass in a list of props
which is an object containing the values (or columns) to set on the record.
copy
Creates an in-memory copy of an existing record, but with a new id
. You'd later have to call insert()
(from the Record API) on the record to insert it into the database. This is essentially the same as passing the properties of an exsiting record to the mint()
method.
const copyOfRecord = Users.copy(existingRecord);
create
Creates a new record in the database, but doesn't validate the inputs according to the ving schema. That doesn't mean it will get inserted. If you haven't given enough information to pass the database table's own schema, then it will fail. You are just bypassing the extra validation provided by ving's schema. In general, you shouldn't use this unless you know what you are doing.
const record = await Users.create({username: 'Fred'});
createAndVerify
Creates a new record in memory, validates it against ving's schema, and then inserts it into the database. This is almost always what you want.
const record = await Users.createAndVerify({username: 'Fred'});
insert
Use this to write your own custom insert statement. Also see the insert()
method on the Record API.
const result = await Users.insert.values({username:'Fred'});
mint
Creates a record in memory. You'd later have to call insert()
(from the Record API) on the record to insert it into the database.
const record = Users.mint({username: 'Fred'});
Reading Records
There are many different ways to find a record or list of records in ving.
describeList
Instead of returning records like the rest of this list, describeList()
returns an array of objects that is suitable for using in web services. You can use it like this:
const list = await Users.describeList(params, where)
Result
{
paging: {
page: 1,
nextPage: 2,
previousPage: 1,
itemsPerPage: 10,
totalItems: 43,
totalPages: 5
},
items: [
{ // same as the describe() method from a Record
props : { id: 'xxx', ... }, // database properties
meta : { displayName : 'Freddy', ... }, // calculated properties
links: {
self : {
href : "/api/v1/user/xxx",
methods: ['GET','PUT','DELETE']
},
...
}, // urls for various web services
options : { useAsDisplayName : [
{ label : 'Username', value : 'username' },
...
]}, // options for enumerated / boolean fields
related: { }, // records with a parent/sibling relationship to this record
},
...
]
}
Parameters
- params - An object to modify the output.
- itemsPerPage - An integer between
1
and100
that defaults to10
and represents how many items should be included per page. - page - An integer between
1
and1000000
that defaults to1
and represents the current page number of the result set. - sortBy - A string that represents a valid
prop
of the record and defaults tocreatedAt
. - sortOrder - A string that defaults to
asc
but could also bedesc
if you want the order of the records to be sorted in descending order. - maxItems - An integer between
1
and100000000000
that defaults to100000000000
that limits the total number of items that can ever be paginated through. - objectParams - See the params of the
describe()
method in the Record API.
- itemsPerPage - An integer between
- where - A drizzle where clause like you would use with the
findMany()
method below.
find
Locates and returns a single record by it's id
or undefined
if no record is found.
const record = await Users.find('xxx');
findMany
Locates and returns a list of records by a drizzle where clause or an empty array if no records are found.
const listOfFredRecords = await Users.findMany(like(Users.realName, 'Fred%'));
findAll
Does the same thing as findMany
except with an iterator so not all records are loaded into memory at the same time.
const allFreds = await Users.findMany(like(Users.realName, 'Fred%'));
for await (const fred of allFreds) {
console.log(fred.get('id'));
}
findOne
Locates and returns a single record by a drizzle where clause or undefined
if no record is found.
const fredRecord = await Users.findOne(eq(Users.username, 'Fred'));
findOrDie
Locates and returns a single record by it's id
or throws a 404
error if no record is found.
const record = await Users.findOrDie('xxx');
select
Write your own custom select function. Returns a drizzle result set, not a list of records.
const results = await Users.select.where(like(Users.realName, 'Fred%'));
Updating Records
Updating existing records.
update
Update records already in the database without first selecting them by writing your own custom query.
const results = await Users.update.set({admin: false}).where(like(Users.realName, 'Fred%'))
Note that this where clause is raw. To use it safeley you should wrap the
like(Users.realName, 'Fred%')
portion in thecalcWhere()
method below.
See also the update()
method in the Record API for updating a record you've already fetched.
Deleting Records
delete
Delete records by writing your own custom query.
const results = await Users.delete.where(like(Users.realName, 'Fred%'));
Note that this where clause is raw. To use it safeley you should wrap the
like(Users.realName, 'Fred%')
portion in thecalcWhere()
method below.
See also the delete()
method in the Record API for deleting a record you've already fetched.
deleteMany
A safer version of delete
above as it uses calcWhere()
.
await Users.deleteMany(like(Users.realName, 'Fred%'));
Utility Methods
calcWhere
Adds propDefaults
(if any) into a where clause to limit the scope of affected records. As long as you're using the built in queries you don't need to use this method. But you might want to use it if you're using create
, select
, update
, or delete
directly.
const results = await Users.delete.where(Users.calcWhere(like(Users.realName, 'Fred%')));
count
Returns an integer representing how many records match a given where clause.
const usersNamedFred = await Users.count(like(Users.realName, 'Fred%'));
Properties
propDefaults
An array of objects containing a list of properties used in building relationships between this record and another. For example the User
record uses it like this to establish related APIKey
records:
apikeys.propDefaults.push({
prop: 'userId',
field: apikeys.table.userId,
value: this.get('id')
});
It is then used with calcWhere()
to limit the scope of a where clause to related records. So for this example it would limite the list of APIKey
s to the ones related to the current User
.
Record API
The Record is akin to a relational database table row. You'll get a record via the Kind API, perhaps like this:
import {useKind} from '#ving/record/VingRecord.mjs';
const users = await useKind('User');
const record = users.findOrDie('xxx');
Once you have a record you can use the following methods to manipulate it.
Reading Data
describe
Formats everything known about a record into an object that is easily serializable and sanitized for privileges. This is used by the Rest API to format a record for public consumption.
const description = await record.describe(params)
Result
{
props : { id: 'xxx', ... }, // database properties
meta : { displayName : 'Freddy', ... }, // calculated properties
links: { self : {
href : "/api/v1/user/xxx",
methods: ['GET','PUT','DELETE']
},
...
}, // urls for various web services
options : { useAsDisplayName : [
{ label : 'Username', value : 'username' },
...
]}, // options for enumerated / boolean fields
related: { }, // records with a parent/sibling relationship to this record
extra : {}, // an object that include literally anything special defined by the object
}
Parameters
- currentUser - Either a
User
record or aSession
object can be used to determine what data should be included in the description based upon user privileges. - include - An object to modify the output.
- options - A boolean that if
true
will add list of enumerated options for props on this record. - related - An array that can contain the names of parent relationships to this object that should be included in the description.
- extra - An array that include the names of special extras that will be included the description.
- meta - A boolean that if
true
will include calculated properties. - links - A boolean that if
true
will include a list of API links. - private - A boolean that if
true
will ignore the privileges of thecurrentUser
passed in and include all private information.
- options - A boolean that if
get
Returns the value of a single prop on this record using the name of the prop.
const value = record.get('id');
getAll
Returns an object containing key value pairs of all the props stored in the database for this record.
const props = record.getAll();
refresh
Refetches the data from the database for this record.
await record.refresh();
Writing Data
createAndVerify
Calls testCreationProps()
, setPostedProps()
and insert()
in a single method call.
await record.createAndVerify({foo: 'bar', one: 1}, currentUserOrSession);
delete
Delete the current record from the database.
await record.delete();
insert
Insert the current record into the database. Can only be called if it hasn't already been inserted.
await record.insert();
set
Set the value of a prop on this record. Returns the value set or throws an error if there is a validation problem, which then throws an error. Updates the in memory prop, but doesn't write it to the database; call update()
or insert()
for that.
user.set('admin', true);
setAll
Set the values of multiple props on this record at the same time. Returns an object containing all the props unless there is a validation problem, which then throws an error. Updates the in memory props, but doesn't write it to the database; call update()
or insert()
for that.
record.set({foo: 'bar', one: 1});
setPostedProps
Sets props on the current record from an untrusted source, and thus checks whether a specific user has the privileges to set the prop. Returns true
if everything sets properly, or throws an error if there are privilege problems. Updates the in memory props, but doesn't write it to the database; call update()
or insert()
for that.
await record.setPostedProps({foo: 'bar', one: 1}, currentUserOrSession);
update
Updates the current record in the database.
updateAndVerify
Calls setPostedProps()
and update()
in a single method call.
await record.updateAndVerify({foo: 'bar', one: 1}, currentUserOrSession);
Pseudo Properties
In addition to all the methods above, every schema prop except those of type virtual
will also get pseudo props added to the record. The means that in addition to using get('username')
and set('username', 'adufresne')
methods you can use the pseudo props as getters and setters like this:
const username = user.username;
user.username = 'adufresne';
Privileges
canEdit
Returns true
if the current user has edit rights on this record or throws a 403
error if not.
await record.canEdit(currentUserOrSession);
isOwner
Returns true
if the current user or session is defined as the owner of this record, or returns false
if not.
if (await record.isOwner(currentUserOrSession) {
console.log('they own it!')
}
else {
console.log('they do NOT own it!')
}
Utility Methods
addWarning
Add a warning to the list of warnings
so that the user can be notified in the UI.
record.addWarning({
code : 418,
message : "I'm a little teapot."
});
isInserted
Returns true
if this record has been inserted into the database, or false
if not.
parent
Returns a parent relationship record by name.
const user = apikey.parent('user');
propOptions
Returns a list of validation options for fields in this record.
const options = await user.propOptions(params, false);
Result
{
useAsDisplayName : [
{ label : 'Username', value : 'username' },
...
],
avatar : [ 'png', 'jpg' ],
...
}
Parameters
- params - an object of all the same paramters as the
describe()
method. - includeAll - A boolean, that if true, will return all the options regardless of privileges.
testCreationProps
Tests the properties trying to be set on a new object, and if all the required values are present and valid then it returns true
, otherwise it throws a 441
error.
await record.testCreationProps({foo:'bar'});
Properties
warnings
An array of warnings for this record that have been added by addWarnings()
since this record was instanciated.
[
{
code : 418,
message : "I'm a little teapot."
},
...
]