Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

Records — what any record is called, where it opens, who may read it

Records.Title(row) · Records.Link(row) · Records.Noun(row) · Records.MayRead(person, row) · Records.Readable(person, rows) · Records.Type(row) · Records.IsA(type, baseType) · Records.Fields(type) · Records.Choices(type, field) · Records.Value(row, field) · Records.Time(row, field) · Records.RollsUpTo(row) · Records.FieldName(selector) · Records.Text(selector, value) · [Opens(Type, param = value)] · [NotInRules] · [RollsUpTo]

`Records.*` answers facts about a row of ANY type from what the app declared — what it is called (`[Title]`), where it opens (the page that `[Opens]` it), what kind of thing it is, whether a person may read it, which fields a person may ask about and what each holds, and which records its history rolls up to.

stable5 examples compiled by CIstdlibrecordssecurityrouting

Summary#

A kit that works on "some record" — a notification, a comment, a question — cannot know the record's type, so it cannot know what to call it, where it opens or who may read it. Those are the record's own facts, declared once where the record and its page are declared, and Records answers them for any row:

callanswersfrom
Records.Title(x)what it is called — "#1042 · Refund not received"its [[entity-title[Title]]], else its first required text
Records.Link(x)where it opens — "/w/mine/t/1042"; "" when no page opens itthe page that [Opens] its type
Records.Noun(x)what kind of thing it is — "Urgent case"its type's name, in words
Records.MayRead(person, x)may this person read itthe row's security, as that person
Records.Readable(person, rows)the rows this person may read, in the order giventhe same, one read per type

Each takes a row of any type, or an Entity. A null — an Entity the caller may not read — answers "" or false.

A kit that works on a record by its FIELDS — a rule editor, a filter builder — asks these:

callanswers
Records.Type(x)its type's full name
Records.IsA(type, baseType)whether one type is the other, or derives from it
Records.Fields(type)the fields of that type the caller may read, each a RecordField — Name, Label, Kind (Text, Number, Flag, Choice, Time), an enum's Choices, a reference's Target. No statement
Records.Choices(type, field)what a person picks a value of that field from — an enum's members, or the rows a reference points at, as the caller reads them
Records.Value(x, field)one field's value as text — an enum member's name, a reference's id, true/false, a number, "" for none
Records.Time(x, field)one time field's value
Records.RollsUpTo(x)the records its history is also told on — its [RollsUpTo] references
Records.FieldName(selector)the field a selector names — t => t.Title → "Title" — so a kit takes a field the compiler checked
Records.Text(selector, value)a value of that field's own type, as the text Records.Value answers the field in — JobStage.Ready → "Ready"

Signature#

string Records.Title(<row>)
string Records.Link(<row>)
string Records.Noun(<row>)
bool   Records.MayRead(Principal person, <row>)
List<T> Records.Readable(Principal person, List<T> rows)

string             Records.Type(<row>)
bool               Records.IsA(string type, string baseType)
List<RecordField>  Records.Fields(string type)
List<ActionChoice> Records.Choices(string type, string field)
string             Records.Value(<row>, string field)
DateTime?          Records.Time(<row>, string field)
List<Entity>       Records.RollsUpTo(<row>)
string             Records.FieldName(Func<T, V> selector)   // t => t.Title → "Title"
string             Records.Text(Func<T, V> selector, V value)   // (t => t.Stage, JobStage.Ready) → "Ready"

[NotInRules] <type> <Field>;          // never offered by Records.Fields
[RollsUpTo] <Record> <Reference>;     // what happens to this record is told on that one too

[Route("/<route with {params}>")] [Opens(<Type>, <param> = <value or member path>, …)]
component <PageName>(<params>) { … }

Description#

Where does a record open?#

[Opens(Ticket)] on a page says it is where a Ticket opens. Records.Link(t) fills the page's route from the record: each route parameter is filled by a named argument of [Opens] (a literal, or a path over the record's members — id = Article.Id), else by the record's member of the same name ({number} ← Number), else by the parameter's default. Values are URL-escaped.

A derived type with no page of its own opens where its base does. An app's page beats a kit's for the same type, and a page that takes over a kit's page keeps its [Opens]. Two pages opening one type in the same place are refused. A record no page opens has no link ("") — the kit then shows it without one.

Who may read it — as whom?#

Records.MayRead(person, x) evaluates the row's security { } as person, never as the caller — what a notifier asks before telling somebody about a record. Records.Readable(person, rows) asks it of many rows at once — a digest, a list of mentions — in one secured read per type among them, whatever their number. SECURITY-INVARIANT batch-may-read-answers-as-the-person.

Which fields may a person ask about?#

Records.Fields(type) answers the fields the CALLER may read — a field their rules mask is not in it, so a rule they write cannot ask about it, and could not tell them its value by firing or not. Mark a field [NotInRules] to keep it out for everybody (an internal code, a hash). A reference's rows cost a read, so they are not in the field: Records.Choices(type, field) lists them when a person is actually picking one.

How does a kit take a field the compiler checks?#

A kit that asks the app to NAME a field — a rule's condition, a column to sort by — takes it as a selector, Func<T, V>, the way EF's Property(t => t.Title) does, and asks Records.FieldName(selector) which field it names. The app writes t => t.Title; a field the type does not have is a compile error where it is written ("entity 'Job' has no property 'Titel'. Did you mean 'Title'?"), so a misspelling never becomes a rule that silently never holds. A method declaring Test<V>(Func<T, V> field) needs no type argument at the call: V is whatever the lambda produces. t => t.Customer.Name answers "Customer.Name"; a selector that computes something (t => t.A + t.B) names no field, and the call says so.

A VALUE for that field is typed the same way: Test<V>(Func<T, V> field, V value) takes a value of the field's own type, and Records.Text(field, value) writes it as the text Records.Value answers the field in — an enum member's name, a row's id, true/false, a number in invariant form, an ISO time (a TimeSpan as hours) — so the two compare. A value of another type is refused where it is written, at the value: cannot pass 'string' to parameter 'value' of type 'JobStage'. JobStage declares: ….

Whose history does it roll up to?#

[RollsUpTo] on a reference says that what happens to this record also happened to the one it points at — a quote's events belong on its opportunity's timeline too. Records.RollsUpTo(x) answers those records, which is how a timeline (The RecordTimeline kit — what happened to a record, on its own timeline) shows a parent's whole story.

Where can I call them?#

On the server — in a function, a workflow body, a [Test]. Each reads the model's declarations, and MayRead another person's rules. A page shows what a server function answered.

Examples#

[Principal] entity Agent {
  [Required, MaxLength(100)] string Name;
  security { allow read when IsAuthenticated; }
}

[Title($"#{Number} · {Subject}")]
entity Ticket {
  [Required] int Number;
  [Required, MaxLength(200)] string Subject;
  bool Restricted;
  security { allow read, create when IsAuthenticated where !Restricted; }
}

/// Where a ticket opens: `{view}` from the attribute, `{number}` from the ticket's own Number.
[Route("/w/{view}/t/{number}")] [Opens(Ticket, view = "mine")] [Authorize] [Render(CSR)]
component TicketPage(string view, int number) { render { Text("ticket"); } }

public string LineFor(Ticket t) { return Records.Title(t) + " → " + Records.Link(t); }

/// The tickets this agent may read, among many — one read for all of them.
public int ReadableBy(Agent agent, List<Ticket> tickets) { return Records.Readable(agent, tickets).Count; }

A record's fields, as a rule engine sees them#

entity Opportunity {
  [Required, MaxLength(100)] string Name;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

enum QuoteStage { Draft, Sent, Accepted }

entity Quote {
  [Required, MaxLength(100)] string Title;
  QuoteStage Stage;
  decimal Amount;
  [NotInRules, MaxLength(40)] string InternalCode = "";
  [RollsUpTo] Opportunity Opportunity;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

public string FieldsOf(Quote q) {
  return string.Join(", ", Records.Fields(Records.Type(q)).Select(f => f.Name + " " + f.Kind.Name));
}
[Test]
void A_quote_by_its_fields() {
  var o = new Opportunity { Name = "Fleet renewal" };
  var q = new Quote { Title = "Twelve vans", Stage = QuoteStage.Sent, Amount = 18000, InternalCode = "X-9", Opportunity = o };
  Assert.Equal("Title Text, Stage Choice, Amount Number, Opportunity Choice", FieldsOf(q));
  Assert.Equal("Sent", Records.Value(q, "Stage"));
  Assert.Equal(o.Id, Records.RollsUpTo(q).Single().Id);
}

A kit that takes a field by selector#

entity Job {
  [Required, MaxLength(100)] string Title;
  int Copies;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

public class Condition { public string Field = ""; public string Value = ""; }

/// What a kit offers: the app's own rules about `T`, each naming a field and a value of its type the compiler checks.
public class Rules<T> {
  public Condition Test<V>(Func<T, V> field, V value) {
    return new Condition { Field = Records.FieldName(field), Value = Records.Text(field, value) };
  }
}

/// …and what the app writes — `j => j.Copis`, or `Test(j => j.Copies, "500")`, would not compile.
public class JobRules : Rules<Job> {
  public List<Condition> Shipped() { return [Test(j => j.Copies, 500), Test(j => j.Title, "poster")]; }
}
[Test]
void A_selector_names_its_field() {
  Assert.Equal(["Copies", "Title"], new JobRules().Shipped().Select(c => c.Field).ToList());
  Assert.Equal(["500", "poster"], new JobRules().Shipped().Select(c => c.Value).ToList());
}

See also#

Related

[Title] on an entity

`[Title]` on an entity says what each of its records is called — the line a notification, a search result or a link…

Entity

`Entity` is a reference to a row of ANY type the app keeps — what a notification, a comment or a question "about some…

[OnDelete]

`[OnDelete]` on an `Entity` member says what deleting the row it references does to the row holding it: Remove deletes…

Security.MayRead — may THIS person read this row, under their own rules

`Security.MayRead(person, row)` answers whether a given person may read a given row under THEIR OWN security — the…

routes and pages

How a component becomes a page: it declares a route with `[Route("/catalog/{slug}")]`, and navigating to a matching…

[Event] — what happens to a record, declared once

An event is a class marked `[Event]`: something that happens to a record, declared once by the code that owns the…

The Automations kit — rules that do an app's routine work when something happens

`Osysharp.Automations` runs rules — WHEN something happens to a record (any event about it, or every hour), IF the…

The RecordTimeline kit — what happened to a record, on its own timeline

`Osysharp.RecordTimeline` puts what happened to a record on its timeline: every event the app and its kits raise about…