Ent
The Ent represents a single node in the graph. Generated from the schema, it can be configured as needed based on the application.
Each generated Ent
implements the following interface:
interface Ent<TViewer extends Viewer = Viewer> {
id: ID;
viewer: TViewer;
getPrivacyPolicy(): PrivacyPolicy<this, TViewer>;
nodeType: string;
}
Fields
id
id of the Ent. usually the primary key in the database.
viewer
The Viewer
who loaded the Ent. The Privacy Policy associated with this Ent must permit the viewer to see this Ent otherwise it's not returned by the system.
getPrivacyPolicy
Privacy Policy used to determine if the Viewer can see this object.
This policy is applied everywhere the Ent is loaded. This is good because we don't need permissions checks all over the place. We can trust that anytime we encounter one of these objects, we know the right checks have been done and we can call things safely with no worries.
nodeType
Generated by the system, unique per object type, used to differentiate between different nodes. Stored in id1_type
or id2_type
columns in the database when an edge is written to or from this node.
Also used when this is id is stored in a polymorphic column.
Example
Given the following schema:
import { EntSchema, StringType } from "@snowtop/ent";
import { EmailType } from "@snowtop/ent-email";
import { PasswordType } from "@snowtop/ent-password";
const UserSchema = new EntSchema({
fields: {
FirstName: StringType(),
LastName: StringType(),
EmailAddress: EmailType(),
Password: PasswordType(),
},
});
we have the following classes generated
//... missing imports
export class UserBase {
readonly nodeType = NodeType.User;
readonly id: ID;
readonly createdAt: Date;
readonly updatedAt: Date;
readonly firstName: string;
readonly lastName: string;
readonly emailAddress: string;
protected readonly password: string;
constructor(public viewer: Viewer, data: Data) {
this.id = data.id;
this.createdAt = data.created_at;
this.updatedAt = data.updated_at;
this.firstName = data.first_name;
this.lastName = data.last_name;
this.emailAddress = data.email_address;
this.password = data.password;
}
// default privacyPolicy is Viewer can see themselves
getPrivacyPolicy(): PrivacyPolicy<this> {
return AllowIfViewerPrivacyPolicy;
}
static async load<T extends UserBase>(
this: new (viewer: Viewer, data: Data) => T,
viewer: Viewer,
id: ID,
): Promise<T | null> {
return loadEnt(viewer, id, UserBase.loaderOptions.apply(this));
}
static async loadX<T extends UserBase>(
this: new (viewer: Viewer, data: Data) => T,
viewer: Viewer,
id: ID,
): Promise<T> {
return loadEntX(viewer, id, UserBase.loaderOptions.apply(this));
}
/// more...
}
import { UserBase } from "src/ent/internal";
export class User extends UserBase {}
The UserBase
class is where all generated code related to User is put and is regenerated everytime the schema is changed. The User
class is generated once and then the developer can add new functionality there over time as needed.
Privacy Policy
The default PrivacyPolicy
that comes with the framework is that the Viewer
can see themselves. This usually isn't sufficient for any real application so we provide a way to override that. This can be done by just overriding the getPrivacyPolicy
method in the User
class.
For example, to make it so that anyone can see any User
, you can change src/ent/user.ts
as follows:
import { UserBase } from "src/ent/internal";
import { AlwaysAllowPrivacyPolicy, PrivacyPolicy } from "@snowtop/ent"
export class User extends UserBase {
getPrivacyPolicy() {
return AlwaysAllowPrivacyPolicy;
}
}
This new Policy takes precedence over the default one and now the User is visible to anyone.
Loading
To load a User
, a Viewer
and its id are needed
const user = await User.load(viewer, id);
That performs the privacy check and either returns the ent associated with the id or returns null.
If you know the ent is loadable or want to throw an exception if it's not, you can use the loadX
variant of the same API:
const user = await User.loadX(viewer, id);
Custom functionality
To add custom functionality, just add it in the User
class.
For example, to return how long the user's account has existed:
import { UserBase } from "src/ent/internal";
import { AlwaysAllowPrivacyPolicy, ID, LoggedOutViewer, PrivacyPolicy } from "@snowtop/ent"
import { Interval } from "luxon";
export class User extends UserBase {
getPrivacyPolicy() {
return AlwaysAllowPrivacyPolicy;
}
howLong() {
return Interval.fromDateTimes(this.createdAt, new Date()).count('seconds');
}
}
and when the User
is loaded, can access the new method since an instance of User
is what's returned.
const user = await User.loadX(new LoggedOutViewer(), id);
console.log(user.howLong());
Nested objects
Because the privacy policy is applied everywhere an object is loaded consistently, the framework can provide helpful accessors by default.
For example, given the following schema:
const EventSchema = new EntSchema({
fields: {
/// ... more fields
creatorID: UUIDType({
foreignKey: { schema: "User", column: "id" },
}),
// or
creatorID: UUIDType({
fieldEdge: { schema: "User", inverseEdge: "createdEvents" },
}),
},
});
export default EventSchema;
the following accessors are added:
export class EventBase {
/// bunch of stuff
loadCreator(): Promise<User | null> {
return loadEnt(this.viewer, this.creatorID, User.loaderOptions());
}
loadCreatorX(): Promise<User> {
return loadEntX(this.viewer, this.creatorID, User.loaderOptions());
}
}
and when an event is loaded, we can easily load the creator afterwards and be confident that the returned creator is visible since we pass the viewer down and check the permissions before returning.
const event = await Event.loadX(viewer, id);
const creator = await event.loadCreatorX();
console.log(creator.howLong());