GraphQL Server for Visma Business

Tokens for several companies

One token for several companies, and how to pick the company with useCompany.

The frm claim in a token says which companies the caller may work in:

frm Meaning
F0001 One firm database, as before. The token decides; useCompany(no:) is ignored.
F0001,F0002 Those firms only. The caller picks one with useCompany(no:).
ANY Every firm of the installation. The caller picks with useCompany(no:).

Every name is checked as a plain identifier (VismaFirm), since it becomes a database name in executed SQL. A list with one bad entry is refused as a whole.

query {
  a: useCompany(no: 1)    { order(first: 3) { nodes { orderNo } } }
  b: useCompany(no: 9999) { order(first: 3) { nodes { orderNo } } }
}

mutation {
  one: useCompany(no: 1)    { order_update(orderNo: 7, order: { information1: "x" }) { orderNo } }
  two: useCompany(no: 9999) { order_update(orderNo: 3, order: { information1: "y" }) { orderNo } }
}

Rules

  • With a list or ANY, everything that reads or writes a company’s data has to be inside a useCompany(no:) on the query or mutation root. A root field outside one is refused. So are a useCompany without no and a nested useCompany. api and introspection need no company.
  • no must be a company Visma Business has, read from its own register. The catalogue’s Site table gives the system database for site standard, and that database’s Frm table lists the companies. Business:CatalogDatabase names the catalogue: a database name on the same server (default Catalog) or a full connection string. The list is kept for five minutes. If the register cannot be read, the server logs it and only checks that the database exists.
  • no maps to the firm database the way Visma names them: an optional prefix (Business:DatabasePrefix), F, and the number with at least four digits. 1 is F0001 and 12345 is F12345. This is Visma’s own {0}F{1:#0000}; the register records no database names. With a list in the token, the database must be one of the list.
  • Under useCompany on the mutation root the mutations run one after another, in document order, exactly as root mutations do. @export keeps working.

Isolation

Each useCompany is its own tree, and everything under it works in that tree’s company. That covers the EF context, SQL connections, the Visma Business request (CompanyNo), the data loaders and the caches.

  • useCompany puts the firm in the tree’s scoped state. FirmScope.Middleware runs around every field and makes it current (an AsyncLocal) while the field resolves. That is where contexts, connections and Visma requests are made. GetVismaBusinessDatabase() reads it first.
  • A data loader’s batch runs outside the field that asked. Every loader that touches a company’s data therefore takes Firmed<TKey> keys, which carry the firm. FirmBatches.PerFirm runs each firm’s keys in that firm. One company’s identical key, such as a product number, is never answered from another company’s batch or cache.
  • VBFirstOrDefaultFetcher and ContributedFinder hold a context for the firm they were made in, so they are transient rather than scoped.
  • Outside a useCompany, a multi-firm token names no database. Building a connection string then throws instead of falling back to the connection string’s catalogue, which would quietly serve a company the caller never chose.

FirmTests covers this without a database: an interceptor records which database every statement would have gone to.