Osy#betaRun it on Osyrin: osyrin.com

Reference / Local

Writing a kit — its parts, and every seam an app connects

The page to read before writing a kit. What `osy kits new` makes, and each seam in a few lines with the one page that covers it: the app's answer to your policies, a field only your function changes, rows that belong to other people, calling another kit, events, settings, the app's own rules found by interface, an app's own subtype, personal data and its bridge, pages and their screenshots, and testing a rule behind a deny.

stable1 example compiled by CIlocalkitspackagesauthoring

Summary#

A kit is one capability — its data, the rules over it, its pages — that any app can use with one use line. It names nothing of the app: where it needs the app's answer (who manages it, who may see whose rows) it asks with a policy.

Description#

What does osy kits new make?#

osy kits new Lockers writes kits/lockers/: package.osy, model/ and tests/ — an app of its own that uses only the kit. osy test kits/lockers/tests runs it; use <Owner>.Lockers@0; uses it. See package.osy.

How does a kit ask the app who may do something?#

policy ManagesLockers; — every app must answer. virtual policy ManagesLockers => false; — nobody, until the app says. A parameterised one's default must READ its parameter (virtual policy SeesLockersOf(Principal holder) => holder == user || ManagesLockers;); one that ignores it is refused in a where, because it never reads the row. See [[security-naming-a-policy#supplied-default]].

How do I make a field only my function changes?#

deny update Left except via Spend;, with [Server, Authorize(P)] on Spend. The deny steps aside only for Spend's own writes — the caller still needs an allow update Left — and the kit's own workflow, running as the system, needs neither. See deny … except via.

How does someone change a row that belongs to another person?#

Through the function the deny names — an allow … where Owner == user opens nothing past it; who may call it is its [Authorize]. Work that must see rows the caller cannot belongs in the kit's workflow, which runs as the system. See What may go past the caller's own rights — and what never does.

My kit calls another kit's guarded function — who may?#

Its [Authorize] is checked at every call, nested ones too. When a step must happen whoever asks, make the request a row and let its workflow's enter take the step — as the system, in the same commit. See enter and exit (a state's arrival and departure hooks).

How do I tell the app something happened?#

Declare [Event("…")] public sealed class X { … }, Raise(new X { … }) in your function; the app hears it with [On] void H(X e), after the commit. See [Event] — what happens to a record, declared once and [On] — hear an event, after the commit that raised it.

What can the app configure?#

A class marked [AppSetting("Name")], filled by app.Name = new … { … };, read with App.Setting<T>() on the server; a member typed as a class with virtual methods lets the app override behaviour. See settings a package asks its app for (app settings).

How does the app plug in a rule of its own?#

Declare public interface IShiftRule { string? Refusal(Shift s, Principal p); } and ask every App.Implementations<IShiftRule>(): the app's classes are found, never wired. When another kit can answer the same question (a qualifications kit for a shift's requirements), ship a bridge — completes <Kit>@0, <OtherKit>@0; with the one class — which arrives by itself in an app using both (Bridges — a package that arrives when an app uses the two it completes). A bridge may also hear one kit's event and call the other's guarded function ([On] void Grant(PurchasePaid e) { GrantCredits(…); }): a handler runs with the app's authority.

What happens when an app derives from my entity?#

entity Employee : User is its own type: your grants do not carry over, your DENIES do, and the app cannot widen what your rules allow. So a field your functions write for any caller (a count of places sold) belongs on a row of its own, not on the derivable entity. See [[entity-inheritance#security]].

How do people's data reach a privacy request?#

Say whose a row is ([DataSubject], [PersonalData(…)]) and classify every personal field, free text included ([Classification(Personal.Content)]), so the audit keeps no copy. When erasing must go through your function, ship a bridge Osysharp.Privacy.<Kit> (completes Osysharp.Privacy@0, <Kit>@0;) with an IPersonalDataHolder (Bridges — a package that arrives when an app uses the two it completes). See Declared personal data — say whose a row is, and the platform answers for it.

Where do its pages go?#

At the kit's own [Route], in [Layout(WorkspaceFrame)], around [Composable] screens, theme tokens only, so a look re-skins them; an app takes one over by declaring a component of the same name. A component keeps the state it mounted with when a parameter changes, so open one record's detail in a Dialog rather than a side panel fed a new id. One Ui.* test per page; osy test --pixels shots go in the README (from tests/osy-shots/ to the kit's docs/). See component.

How do I test a rule that sits behind a deny?#

Seed the first row in a fixture (it writes past security), then try the second as a person, through the function. A fixture's READS obey the rules: keep the rows you made in variables. See [[testing-fixtures#behind-a-deny]].

[Principal] entity Member { [Unique, MaxLength(80)] string Name; security { allow read when IsAuthenticated; } }

entity Tally {
  [Required] Member Holder;
  int Left = 3;
  security {
    allow read, create, update when IsAuthenticated where Holder == user;
    deny update Left except via Spend;
  }
}

[Server, Authorize(IsAuthenticated)]
void Spend(Tally t) { t.Left = t.Left - 1; }

void SetLeft(Tally t) { t.Left = 9; }

[TestFixture]
void OneTally() { new Tally { Holder = new Member { Name = "Ida" } }; }

principal Ida => Member.Single(m => m.Name == "Ida");

[Test(OneTally)]
[runas(Ida)]
void Only_the_function_changes_it() {
  var t = Tally.Single(x => x.Holder.Name == "Ida");
  Assert.Denied(() => SetLeft(t));
  Spend(t);
  Assert.Equal(2, Tally.Single(x => x.Id == t.Id).Left);
}

Is it ready to publish?#

osy kits check kits/lockers composes it the way an app would and flags what an app would trip on (Checking a kit composes — osy kits check); osy publish runs its tests and tags the release ([[project-package#publishing]]).

See also#

Related

Finding a kit, and learning one from its tests

Lists the kits (packages) this project can build on — each one's contracts, the kits that implement them, and what this…

Checking a kit composes — osy kits check

Checks that a kit composes with the kits around it: it uses no other product kit it does not complete…

Connecting a kit — each seam with the code that connects it

Prints a kit's composition surface — every event, policy, setting, slot, contract, base, grant, extension member…

package.osy

The manifest that makes a git repository a publishable Osy# package. It names the package, says which of the…

deny … except via

Names the ONLY functions whose own code may change something a deny withholds: `deny update UnitsLeft except via…

policy

Names an authorization rule once so every place that needs it can say the name. A policy is a boolean about the caller…

What may go past the caller's own rights — and what never does

A request always runs as the person who made it; there is no system mode, sudo or definer's rights a function can…

Fixtures: built once, cloned for every test, and built on each other

A [TestFixture] is built once per run, and every test that names it runs on its own copy of what it left — rows, files…

enter and exit (a state's arrival and departure hooks)

Run code when a run arrives in a state, and when it leaves. enter may redirect the run somewhere else; exit may not…

Bridges — a package that arrives when an app uses the two it completes

A bridge is a package whose manifest says `completes A@0, B@0;`. An app that uses every package it completes gets it…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…