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

> `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.

<!-- id: stdlib-records · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/records/ -->

## Summary        {#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:

| call | answers | from |
|---|---|---|
| `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 it | the 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 it | the row's security, as **that person** |
| `Records.Readable(person, rows)` | the rows this person may read, in the order given | the same, **one read per type** |

Each takes a row of any type, or an [`Entity`](https://osysharp.com/reference/types/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:

| call | answers |
|---|---|
| `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      {#signature}
```osy syntax
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    {#description}

### Where does a record open?   {#opens}
`[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?   {#may-read}
`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?   {#fields}
`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?   {#field-name}
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?   {#rolls-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](https://osysharp.com/reference/ui/activity-kit/)) shows a parent's whole story.

### Where can I call them?   {#where}
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       {#examples}

```osy title="a record's title and link, and what one person may read" test app=stdlib-records
[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   {#example-fields}
```osy title="the fields a person may ask about, one value, and what a quote rolls up to" test app=stdlib-records-fields
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));
}
```

```osy title="InternalCode is left out; the quote's history rolls up to its opportunity" run app=stdlib-records-fields
[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   {#example-field-name}
```osy title="a rule's condition names the field typed — Test(j => j.Copies, …) — and the kit stores its name" test app=stdlib-records-field-name
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")]; }
}
```

```osy title="the names the selectors gave, and the values as text" run app=stdlib-records-field-name
[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       {#see-also}
- [[Title] on an entity](https://osysharp.com/reference/entity/title/) — what each record is called
- [Entity](https://osysharp.com/reference/types/entity/) — a reference to a row of any type, which these answer for
- [[OnDelete]](https://osysharp.com/reference/entity/on-delete/) — what deleting a referenced row does
- [Security.MayRead — may THIS person read this row, under their own rules](https://osysharp.com/reference/security/may-read/) — `Security.MayRead`, the single-row question for a known type
- [routes and pages](https://osysharp.com/reference/ui/routing/) — pages and their routes
- [The Automations kit — rules that do an app's routine work when something happens](https://osysharp.com/reference/ui/automations-kit/) — conditions offered from a record's fields
- [The RecordTimeline kit — what happened to a record, on its own timeline](https://osysharp.com/reference/ui/activity-kit/) — a timeline that rolls up
