stackagejs

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.

What does this even do?

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.

Each back-end package will:

  1. provide standardized JS classes describing each model from your choosen ORM, such as field names, types, nullability, constraints, and relationships to other models.
  2. provide standardized controller actions for basic data interaction, setting up a convention-based API from your ORM models.

Each front-end package will:

  1. Understand the provided JS model classes to automatically interact with the API(s) as-needed. Records are accessed through proxy, so there’s never a reason to write your own fetch call.
  2. Manage front-end data caching. Don’t ask for data you’ve already gotten, unless you want to.
  3. Manage offline data storage via IndexedDB. Easily cache data for offline use, or keep user input ready for when your application is back online.
  4. Provide update hooks for value changes, allowing integration with reactive frameworks or any custom reactivity.
  5. Provide optional hooks for receiving live data pushes.

Planned front-ends:

Planned back-ends:

Other stackages:

API definitions

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.

Model definitions

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 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, and model-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:

The following static property. Can be omitted or empty if there are not any foreign keys:

End 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

Properties Descriptor Object

This is the object telling us all about the properties from the ORM.

For each property:

propertyName (camel case)

Front-End definition

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.

Record Class

Record properties:

Record Local methods:

Record API methods:

Design notes:

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.

Table Class

Table properties:

Table Local methods:

Table API methods:

Filtration set (Optional)

Server-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:

FilterQuery object properties:

query object format:

Database Class

Data Types

These 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:

Optional properties: