You know how you utilize an ORM to keep your back-end in sync with your database, and easily access or manipulate that data? We’re just taking that one step further. Any ORM, any SPA, mix and match if you need to, and Stackage will handle the mundane part for you.
Allow you to access your data models and properties directly in your Single-Page Application, including relationships and complex custom types; then automatically get that data as-needed with no further effort.
The minimum endpoints for default interactions. Created for each model on the back end, consumed by the front end.
Model.pathname should be the model name in kebab-case by default.
The model format created by the back end for any given front end to consume.
Include a comment at the head of the file warning that this is a generated file.
//This file was generated by stackage, do not modify directly as your changes will be lost.
Import for the Model class.
Imports for any related models.
Imports for any enums.
Imports for any custom types.
Start Class declaration extending Model.
A constructor that takes a record and a config object, calling the super(record, config) internally.
The following static properties:
prefix
Prefix option for multi-system setups, where this value is what will be used to determine which API connection to use for this particular model. Omitted by default, which will use the “default” registered API connection.*name
The name of the model, which includes the prefix if any provided. recommend PascalCase, but not enforced.**pathName
is the controller sub-string in the URL, that is the “my-controller” section of “https://mysite.com/my-controller/get”. Should be kebab-case, but casing is not enforced. Deviate at your own peril.**dto
Bool indicating if this is a DTO (see DTO section)*prefix is used as a system identifier, rather than having each model definition carry its own URLs around. This allows for flexible configurations, like environment-specific API connections. See Connection object TODO link this
**casing is not enforced and generally JS doesn’t care, it’s just using strings; you could make these almost anything you want. However, the recommended and default pattern is that
(Prefix)ModelName
be used for the model name, andmodel-name
for the pathName. The prefix on model name is to help prevent clashing models in multi-system setups: AppAUserModel vs AppBUserModel, where AppA and AppB are prefixes that keep the UserModels from fighting amongst themselves for dominance. The API would not need to be aware of this setup whatsoever, and so prefix is not used in the pathName.
The following static read-only function:
properties
function that replaces itself with the properties descriptor object. Using a function here is necessary to avoid issues with circular referencing. Returns the properties descriptor object. TODO linkThe following static property. Can be omitted or empty if there are not any foreign keys:
foreignKeys
KvPs in the format fkPropertyName
: PropertyName
//TODO determine if this is still necessary, I think it’s covered by property settingsEnd Class declaration.
Set a reference to the class on the window object by symbol, something to do with ensuring we have a single object reference for comparison. Honestly it was a long time ago and now it’s fuzzy, but there’s an issue this solves.
window[Symbol.for(MyModel.name)] = MyModel;
Finally, export the model:
export default MyModel
This is the object telling us all about the properties from the ORM.
For each property:
propertyName
(camel case)
type
: an appropriate Javascript type for the property in question. Each back-end stackage will decide these default mappings. This type will determine how values are interpreted by the front-end and repackaged for the back-end as necessary. An example would be a Date type - transmitted as an ISO 8601 date value, the front-end will automatically translate this to a JS Date object for immediate use. When _out is called on this record (such as when saving) stackage will translate that value back to an ISO 8601 string value for transmission.
Custom types can also be provided, for special data types not handled well by JS types. See Custom Data Types //TODO linkconfig
: specific configuration values that will be passed into many internal functions
nullable
(bool): whether this value is nullable. Attempting to save a non-nullable property (other than id or createdAt) without a value will generate an error.foreignKey
: the name of the corresponding foreign key property, if this property is a relationship to another model.min
, max
, minLength
, maxLength
, etc. Any sort of property where it will be useful to know with a particular data type. Database
is a simple object to keep fetched data cached. Utilizing proxies, attempting to access a Table
for the first time will create that Table on the Database
object.
Table
is also a simple object, accessed from the Database
object like so: Database[MyModel.name]
. It utilizes proxies to create empty Records
as they are asked for or discovered.
Record
is an instance of a given Model class, as they are defined from your ORM’s stackage. Access a record like so: Database[MyModel.name][PK]
. The first time a record is accessed directly, it will ask its corresponding API for the available data. There are utilities to pre-fetch at will, and re-fetch at will or with an expiration setting.
Access related records from another record and stackage will understand what you’re looking for based on the Model definitions
Empty Records are created anytime a PK is discovered for the Table. This could be as an FK from another Table or directly asked for from API.
Accessing Empty records triggers a Read call to the API. Successful retrieval updates _loaded property to true and _fetched property to current time.
If Table has a set expiration duration, the proxy remains and expiration is checked on subsequent accesses. If it does not have a set expiration, the proxy is unwrapped.
Newly created Records are extended with non-enumerable methods and properties.
_loaded
read-only reference to bool value indicating if record is considered loaded._loader
read-only promise resolving when this record loads_busy
read-only reference to bool value representing that there is an active API call involving this Record._errors
read-only reference to error object for this record. Errors are generated by failed returns from API methods and validation callbacks._modified
read-only reference to if this record has been modified since instatiating or loading._values
read-only reference to the raw values of each property. Sometimes useful for complex operations where you care about the raw values and not the display values._remove()
removes this Record from Table, and does not call API. Shortcut for _remove(id)
method on table._validate(prop)
if prop is passed, validates that particular prop. If no prop passed, calls each _validation function for each prop on this record_addError(errorObject)
adds an error to the _errors property_removeError(errorObject)
removes an error from the _errors property_clearErrors()
removes all errors from the _errors property_callback(record, prop)
null by default, setable callback function that will be called any time a value is modified on the record. Passes the record and modified prop. //TODO should be event by default…_out()
processes all values and returns a JSON object ready for transport to the API. This is used internally by the _save()
method and generally shouldn’t be called manually._typeof(prop)
shortcut to return the prop type defined on the Model_copy()
returns a new instance of the current Model with the copied values //TODO double check this isn’t passing anything by reference…_populate(data)
uses the passed data object to set the values for all properties in the record. This is used internally for processing the data from the API and generally shouldn’t be called manually._read()
manual trigger for Read API call, updating the Record upon success. Returns a promise._save()
calls Update API if PK is present. Calls Create API if PK is not present. Successful Create updates Record with returned PK. Returns a promise._delete()
calls Delete API. Upon Successful Delete, _remove is called on the Record. Returns a promise. Shortcut for _delete(id)
method on table._refresh()
manually calls the Get API for this record. Shortcut for the refresh(id)
method on table.There were two ways considered to go about auto-fetching the record values - when the record is first accessed, or when a particular value on the record is acceessed.
The latter would allow for peeking at helper properties like _loaded without bothering to call the API for values. However, in previous itterations of this pattern that use case has rarely come up in practical application. If there was a reason to look and see if the record was loaded, it was likely because we were calling on the API already.
Fetching when the record is first accessed covers most scenarios, and setting one self-unwrapping getting at the record level is less lift than setting them on every property.
_array
read-only reference to const array of known records for this Table._keys
read-only reference to const array of keys of known records for this Table._promises
read-only reference to const array of active prommises for this Table._length
read-only length of known records for this Table._remove([Record] || Record || [PK] || PK)
removes the passed Record(s) from this Table by reference or PK, and does not call API. Returns nothing_add(record)
Adds the passed record to this Table. Does not call API. Returns the reference to the record_discover()
Calls the ‘all-ids’ API for this Table. Returns a promise._save(Record || id)
Calls the ‘save’ API for the passed record. Returns a promise._saveAll()
Calls the ‘save-many’ API for all modified records on this Table (including those added). Returns a promise._refresh(Record || id)
Calls the get
API for the passed record. Returns a promise._refreshAll()
Calls the list
API for all known record ids. Returns a promise._all()
Calls the all
API for this table, fetching all available record data. Overusing this defeats the purpose of lazy-loading, but can be very useful in situations where you have small tables that benefit from pre-loading. Returns a promise._equals
, _contains
, _startsWith
, _endsWith
, _order
, and _filter
methods as described in the Filtration Set sectionServer-side data filtering that returns an array of PKs for the given queries on this Table. Devs should be conscious of whether their data set is better filtered on the back-end (no need to send all records to client to filter) or the front-end (more data initially, but fewer network calls and less stress on the back-end).
As these can be finicky to outright grueling to implement in an abstracted, efficient manner, at this time Filtration methods will be considered optional.
TODO define a standard not-implemented response for the API
These properties exist on the Table:
_equals(prop, spec)
sets up an equals query for Filter API consumption. Returns a FilterQuery object._contains(prop, spec)
sets up a contains query for Filter API consumption. Returns a FilterQuery object._startsWith(prop, spec)
sets up a startsWith query for Filter API consumption. Returns a FilterQuery object._endsWith(prop, spec)
sets up an endsWith query for Filter API consumption. Returns a FilterQuery object._filter(filterQuery)
skips the chaining and takes a FilterQuery object directly. Calls the FilerQuery’s _go method, returns the resulting promiseFilterQuery object properties:
_go()
Performs the Filter API call, or gets cashed result if present. Returns an object TODO define this object, needs promise and reference to future PK array of results._queries
array of query objects constructed for this filter_subset
array of PKs in the current Table for records to restrict this filter to_doRefresh
- bool indicating if a data refresh is requested, default false. If false, _go may use existing filter result if it exists_refresh(bool)
- forces this query to call the API whether or not there’s a cached return for it. Returns this FilterQuery object._equals()
, _contains()
, _startsWith()
, and _endWith()
methods as shortcut references to those of the same names on the Table, and return this FilterQuery objectquery object format:
prop
a string, dot-notated path to the property being filtered onspec
the value to test againstThese are types that can be assigned to properties to handle special data cases not reasonably handled by default JS types. They will translate data from the API to a useable JS format interally, and back again for when the data needs sent to the API.
They are a class with a constructor that takes a value suitable for their _value property as the first argument, and an optional config for the second.
Required properties:
_raw
is how the back-end record interacts with this data type. It has a setter that takes the raw data as provided by the API and stores it internally in a format that makes sense for JS use. Has a getter that returns the internal value in the format the API expects._value
is how the front-end record interacts with this data type. It has a setter that takes input data and transforms it into the internal value format. The getter returns the value in a format that is sensible for use. It may be that no transformation is necessary here.baseType
set to DataType
from stackage-js, this is used to identify that this is intended to be used by stackage as a custom data type following the convention set here. //TODO might be good enough to assume, and just leave this offOptional properties:
_validate
if this data type has a universal validation method, it should be kept here. Attempts to validate a record that contains a property with this data type will run this validator. Return an array of errors.