The
cardant package provides a server-based application to track inventory. It stores
detailed information about types of
items, and can
store the counts of those items present in defined
locations.
This section of the documentation describes the internal cardant model.
An
item is an object that is tracked by the inventory system. When the
cardant
package refers to items, it should more accurately be understood to be referring to
classes
of items. That is, the inventory system tracks sets of items of a given class
within
locations. An item has
associated
metadata, an identifier that uniquely
identifies the item class, zero or more
types
that can constrain the metadata associated with the item, and a
name.
Metadata
is data associated with an item that describes that item. Items can have any number of metadata values, and
inventory managers can introduce strong requirements on the presence and types of metadata on items through the
application of
types.
A
type is a label that can be applied to an item that will constrain the
metadata
associated with that item. A
type on an item refers to a
type declaration
created by the inventory manager. A
type declaration
is essentially a
record type
in the sense that it defines a set of named metadata values along with their individual
scalar types.
Types are intended to ensure the integrity and quality of metadata associated with items, and allow for more
precise searching. For example, inventory managers might define a voltage_regulator type
that is applied to electronics components within the inventory that are voltage regulators. The
voltage_regulator
type might be declared to require that metadata includes numeric values
input_voltage
and
output_voltage
that describe the input and output voltages of the regulator. Any attempt to update an item that has the
voltage_regulator
type applied without providing values for the
input_voltage
and output_voltage metadata will be rejected with a clear error message.
As mentioned, types are intended to facilitate more precise searching. It is possible to, for example, search
for all items in the inventory that have an output_voltage metadata value, but this
does not imply that all the returned items will be voltage regulators. With a well-designed and well-managed
inventory, one can simply search for all items that have the type
voltage_regulator
(and then perhaps narrow down the search by asking for only those items that have an
output_voltage
value equal to 5.0).
A scalar type is a named type derived from one of the following base
types:
Typically, a named scalar type will augment an existing scalar base type with further constraints such as a
bound on the range of allowed values. For example, an inventory manager supervising
a stock of computer keyboards might define a
keyboard
type
with a
keys field of a scalar type
keyboard.keys. The
keyboard.keys
scalar type might be derived from the
Integral
scalar type with a bound of
[68, 104], expressing that keyboard may have as few
as
68 or as many as
104
keys.
Items can have zero or more attachments. An attachment is simply a file associated with
the item according to a given relation. For example, an electronics component might
have a PDF datasheet associated with it using a
datasheet
relation. Relations are entirely user-defined, and it is the responsibility of the inventory manager to use
sensible and consistent relations.
Stock instances have globally-unique UUID identifier values.
A set instance is a stock instance that describes a set of items that, by themselves,
do not have any kind of unique identities.
For example, an inventory manager that is managing an inventory that contains rolls of passive electronics
components such as
resistors
would use set instances to describe sets of resistors in the inventory. The resistors themselves do not have
serial numbers or any kind of individual identity and therefore there is no reason to (or, in fact, any way to)
track the individual resistors; only the
count of resistors has any meaning.
A set instance always has a count ≥ 0. Attempting to remove items from a set instance such that the count would
become negative is an error.
A
serial instance is a stock instance that describes a single item that may have one or
more
serial numbers associated with it.
For example, an inventory manager that is managing an inventory that contains musical instruments would use
serial instances to identify individual instruments in the inventory. Instruments of each type are distinguished
from other instruments of the same type by way of serial numbers.
As mentioned, stock instances do have a guaranteed-unique identifier, so this can be used as a system-enforced
unique identifier for an item if required.
A
serial number
in the
cardant package is a pair consisting of a
type
and an arbitrary string
value.
The
type categorizes the serial number. For example, a purchased product might
include the manufacturer-assigned serial number, and the inventory manager might choose to categorize
serial numbers of this type by setting the type of the serial number to
manufacturer_serial. The
cardant package does
not ascribe any particular semantics to serial number types, and types may be freely chosen by the
inventory manager and do not need to be declared in any form ahead of time.
Serial numbers are assumed to be unique for a given
item class but are not assumed be globally unique,
and uniqueness is not enforced by the system.
After all, two manufacturers of completely different products
might coincidentally manage to use the same serial numbers for their products. Within a single manufacturer's
product range, however, serial numbers will most assuredly be unique.
Stock is added to and removed from locations by
repositing. Intuitively, a
reposit
operation atomically adds zero or more instances of an item to a location, and removes zero or more items from
another location in a single step. A reposit operation is either a
set
reposit or a
serial reposit.
A set reposit is an operation used to move sets of items that do not, themselves,
have useful individual identities.
A serial reposit is an operation used to move items that have individual identities.
Reposit operations are described by the following algebraic datatype:
A reposit (CAStockRepositSerialIntroduce i t l s) introduces a new serial stock
instance i of item
t, placing it in location
l, and using the initial serial number
s.
A reposit (CAStockRepositRemove i) removes the stock instance
i.
A reposit (CAStockRepositSerialMove i l) moves an existing serial stock
instance i from the location it is currently in to the new
location l.
A reposit (CAStockRepositSerialNumberAdd i s) adds a new serial number
s to an existing serial stock instance i.
A reposit (CAStockRepositSerialNumberRemove i s) removes a serial number
s from an existing serial stock instance
i.
A reposit (CAStockRepositSetIntroduce i t l c) introduces a new set stock
instance i of item
t, placing it in location
l, and using the initial count
c.
A reposit (CAStockRepositSetAdd i c) adds
c
items to an existing set stock instance i.
A reposit (CAStockRepositSetMove i0 i1 l c) moves
c
items from an existing set stock instance
i0
to the set stock instance i1.
A reposit (CAStockRepositSetRemove i c) removes
c
items from an existing set stock instance i.
A
location is an object that tracks
item counts. Locations, as a concept, are kept
deliberately abstract to allow for a wide range of use-cases. A location might represent a physical storage bin
in a warehouse, or it might represent a deployed computer system that has been built using items taken from the
inventory. By treating locations as generic containers in this manner, the system is intended to allow for
supporting different use cases such as managing retail inventory, or managing the deployment of computer parts
in a laboratory. Locations are
hierarchical.
Locations carry
metadata in the same manner as items.
Locations can have applied
types in the same manner
as items.
Locations can have
attachments in the same manner as
items.
Locations form a hierarchy (specifically, a tree) in that any location may have any
number of child locations, although a location may only have at most one parent.
Locations can be reparented at any time; they are not locked into having whichever
parent they had when they were created.
The hierarchical nature of locations can be used to model different kinds of inventory arrangements. For
example, a manager of computer laboratories might define
Laboratory A and
Laboratory B
locations. Within those locations, the manager might define locations
Computer A1,
Computer A2,
Computer A3, and so on. Within the
Computer A1 location, the
manager might place a single motherboard
item, one or
more CPU items, and so on, from the inventory. This allows the manager to know what computer parts they have,
and the computers in which those parts are being used in the laboratories under their supervision. Additionally,
should one assembled computer be moved from one laboratory to another, the manager can simply reparent the
computer location to the new laboratory.
Conversely, a manager of a retail business might define a Storage Room A location, and
then within that location, define Shelf A01,
Shelf A02, Shelf A03, etc. The manager can then place items
onto those storage shelf locations. This allows the manager to track what stock they have, and exactly where
they're keeping each kind of item.
Types
are introduced into a running
cardant server using
type
packages. A
type package is simply a named, versioned container of types that
can be uploaded and installed into the server's database atomically.
Type packages are identified by their
name and
version. It is
not permitted to have two packages with the same name but different versions installed at the same time. A type
package name is a
Lanark
dotted name (such as
com.io7m.example) and the types declared within the package
have their names derived by concatenating their unqualified names onto the end of the package name. So, for
example, a package
com.io7m.example that contains a scalar type named
t
will cause the scalar type
com.io7m.example:t
to be created on the server when the package is installed.
A type package may
import other type packages. An
import
consists of a
package name and a
version range. When a user tries to install a type package
p, each import
i declared by
p
is examined, and the server inspects its own set of installed packages and verifies that a package is installed
with both a name that matches
i
and a version that falls within the range declared by
i. If no such package
exists, the type package installation is rejected and the system is left untouched.
For the sake of simplicity, and because the number of installed type packages is expected to be small and not
change particularly often, imports are merely an install-time check. It is possible (but heavily discouraged)
for type packages to refer to types in packages that they have not imported.
Type packages are serialized using the given
XML schema.
The cardant package uses role-based access control for all operations.
Each user has a set of
roles
associated with it. When the user attempts to perform an operation on the server, the account's roles are
checked to see if it has permission to perform the action.
A role R may be granted to a user
A
by user B if
B
has role R. Accordingly, a role R may be
revoked
from a user
A
by user B if
B
has role R.
A user holding the inventory.admin role effectively always has all available
roles. If new roles are added in future versions of the cardant package, users
holding the inventory.admin role will be automatically granted the new roles. It
is recommended to limit this role to a single user, and to avoid using that user account for day-to-day
operations.
The following roles are available:
The server maintains an append-only audit log consisting of a series of
audit events. An audit event has an integer
id, an owner (represented by an account UUID),
a timestamp, a type, and a
message
consisting of a set of key/value pairs.
Each operation that changes the underlying database typically results in an event being logged to the audit log.
The
inventory API is the interface exposed to user clients. It exposes
a JSON-based API over HTTP, using the included
schema.
The inventory API is the primary means by which clients perform operations on the server.