Sealious is a declarative, resource-oriented framework for creating application backends. Although it has been built with webapps in mind, it can be (and successfully has been) used for desktop applications, as well.
When creating a Sealious application, the developer has to focus on the "what" of the application, not the "how". Consider the following Sealious resource-type declaration:
var Person = new Sealious.Collection({
name: "people",
fields: [
{name: "full-name", type: "text", required: true},
{name: "age", type: "int"}
],
access_strategy: {
default: "owner",
view: "public",
create: "logged_in"
}
})
That's all Sealious needs to create an API that lets users perform CRUD operations on the people
collection. It will let anyone view the elements of the collection, but only authenticated users will be able to create new entries, and only the owner of a given entry would be able to make edits to it. The details and the meaning behind this and other declarations will be explained in the following chapters.
Based on the given declarations, Sealious builds the Subject Graph, which contains methods that can be called by clients.
Note that it does look like a tree in the above example, but certain custom Subjects can redirect to another nodes, possibly creating a cycle.
Sealious's structure is highly modular. In fact, one cannot create a functional Sealious application without using at least one module! (Fortunately, Sealious comes with sane default modules to ease development).
Sealious uses npm
as a way to distribute it's modules. To distinguish them from regular node modules, Sealious modules from npm
are called "plugins".
Sealious plugins contain one or more "chips". Chips are small entities that contain a singular purpose. Every chip is one of these types:
- Channel
- Collection
- FieldType
- AccessStrategyType
- Datastore
- Subject
A Channel's responsibility is to provide access to some (or all) of the methods from the Subject Tree. It identifies the client and creates a Context object for each of the client's requests.
Examples:
- REST,
- WebSocket.
Describes the template for all elements in a Resource Type Collection. Contains Fields - each with a FieldType assigned.
Examples:
- Person,
- User,
- Animal,
- Document.
Describes behavior for a type of a field in a Resource Type: what are the correct values handled by this field? Can be parametrized and inherit from other field types.
Examples:
- text,
- integer,
- color,
- reference.
A function of Context. Takes a Context object and decides whether that context meets the arbitrary criteria or not. One AccessStrategyType can be parametrized and instantiated into various AccessStrategy instances.
Exmples:
- public,
- admin,
- only_logged_in.
Responsible for storing and querying data.
Examples:
- MongoDB,
- MySQL (possibly in the future).
Creates a node in the Subject Graph. Can expose arbitrary methods.
Examples:
- Resource Collection,
- Single Resource (identified by its
id
), - Sessions.
Whenever a client calls a method through the API generated by Sealious, the Channel through which the request is made is tasked with generating a Context for this request. Context is an object that represent who and when made a given request. It is immutable. It is being passed down to each method which is called as a consequence of that given request. Thanks to that Insecure Direct Object Reference vulnerabilities are basically impossible by default in a Sealious app.
Here's a lifespan for a "create a resource of type Person" method call.
Assume the following client input:
{
"path": "/resources/person",
"method": "create",
"arguments": {
"name": "Alice",
"age": 99
}
}
The exact syntax of a call can be different for each Channel, but essentially it always has to contain the information presented above. The differences between call syntax for Sealious Channels does not fit inside the scope of this reference.
The darker nodes represent functions that are context-sensitive.
Creating a Sealious application consists mainly of composing various declarations and letting Sealious take care of interactions between each one of them. Below are listed and describe all the declaration syntaxes you need to know in order to be a productive Selious developer.
Note: we'll be using the jsig notation for describing the various syntaxes.
type AcceptCallback: () => void;
type RejectCallback: (errorMessage: String) => void;
type Params: Object<String, Any>;
-
Syntax
type FieldType: { name?: String, is_proper_value: ( context: Context, params: Params, new_value: Any, old_value?: Any ) => Promise, encode?: ( context: Context, params: Params, value_in_code: Any ) => Promise<encoded_value: Any> & Any, decode?: ( context: Context, params: Params, value_in_db: Any ) => Promise<decoded_value: Any> & Any, extends?: FieldType, has_index?: (params: Any) => false | index_type: String | Promise<false | index_type: String> format?: ( context: Context, decoded_value: Any, format: String ) => formatted_value: Any | Promise<formatted_value: Any> } | FieldTypeName
-
Explanation
You can notice that there are two possible syntaxes (separated above with a
|
character). When creating a new FieldType, one has to describe it's behavior with the former notation. When only referencing an already described FieldType, one can use it's unique name.name
: optional. The name of the FieldType. Please note that this is the name of the type of a field, not a name of a field. Has to be unique amongst all other registred FieldTypes. When provided, the FieldType created by this declaration can be referenced withFieldTypeName
.is_proper_value
: a function that takes a value (new_value
) and decides whether that value is accepted. Can takeold_value
into consideration.params
are parameters for the particular field that uses this field type. Shouldreturn Promise.resolve()
to accept, andreturn Promise.reject("Reason")
to reject.encode
: optional. Takes the value for a field from client's input and transforms it into something else. The result of calling that function for the client's input is what will be stored in the database.decode
: optional. A function reverse toencode
. If declared, the value in the database will be run through that function before being returned to the client.extends
: optional. Must be a properFieldType
declaration. When specified, the field-type being declared will inherit behavior from the type it is extending. All specified methods will obscure the parent's methods. The unspecified will be inherited.has_index
: optional. Whether or not to instruct the Datastore to create an index on that field. In order to include the contents of the fields of this type in full-text-search, return "text" here.format
: optional. The user viewing a resource or a list of resources might specify a string for each of the fields in a collection that tells Sealious how the user wants the values to be displayed. That string is then passed to theformat
method of a given field type, if it exists. The method will be given the current context and the decoded value of the field. The method is supposed to format thedecoded_value
according to the specifiedformat
.
-
Usage
To create a new FieldType instance, call the
Sealious.FieldType
constructor.var my_field_type = new Sealious.FieldType({ name: "text", //... });
-
Example
var Sealious = require("sealious"); var Promise = require("bluebird"); var Color = require("color"); var field_type_color = new Sealious.FieldType({ name: "color", is_proper_value: function(context, params, new_value){ try { if (typeof (new_value) === "string"){ Color(new_value.toLowerCase()); } else { Color(new_value); } } catch (e){ return Promise.reject("Value `" + new_value + "` could not be parsed as a color."); } return Promise.resolve(); }, encode: function(context, params, value_in_code){ var color = Color(value_in_code); return color.hexString(); } });
Fields are the most important part of a Collection. They describe it's behavior and structure.
-
Syntax
-
Explanation
type
: required. A FieldType declaration. It's more comfortable to use the "short" FieldType notation here (that is: just entering the name of the registered FieldType).name
: the name of the field. Has to be unique within the Collection.required
: optional. Defaults tofalse
. If set totrue
, Sealious won't allow modifications of the resource that would result in removing a value from that field.params
: optional. A set of parameters that configure the behavior of the FieldType for that particular field.
-
Usage
Use it when describing a Collection.
-
Example
{"name": "full-name", "type": "text", "required": true}
AccessStrategyType describes a type of an access strategy that can be parametrized, instantiated, and, for example, assigned to a Collection.
-
Syntax
type AccessStrategyType: { name?: String, checker_function: ( context: Context, params: Params, item?: Any ) => Promise | Boolean, item_sensitive?: Boolean | (params: Any) => (Boolean | Promise) } | AccessStrategyName
-
Explanation
name
: optional. If specified, the AccessStrategyType will be registered and will be accessible by it's name. Useful when the type is used many times.checker_function
: required. Takes acontext
,params
, and decides whether or not to allow access. If access is denied, the function should returnPromise.reject("reason")
. The function can also returntrue
andfalse
to accept and deny access, respectively. If the access strategy is defined asitem_sensitive
, it will receive a third parameter - an object representing the item to which access is being decided.item_sensitive
: optional. Default tofalse
. If set totrue
(or a resolving function, or a function that returnstrue
), thechecker_function
will be provided with the item to which access is decided by the access strategy of this type. Loading the item data might by costly - that's why by default thechecker_function
will not receive it as argument.
-
Usage
To create a new AccessStrategyType, call the AccessStrategyType constructor:
var new_access_str_type = new Sealious.AccessStrategyType({ name: "only_on_tuesdays", //... });
-
Examples
-
declaring a new Access Strategy Type
{ name: "only_on_tuesdays", checker_function: function(context, params, item){ var d = new Date(context.timestamp); var wd = d.getDay(); if(wd == 2){ return Promise.resolve(); }else{ return Promise.reject("Come back on a Tuesday!"); } } }
-
pointing at an already existing Strategy Type
"only_on_tuesdays"
-
-
Syntax
type AccessStrategy: AccessStrategyType | [type: AccessStrategyType, params:Any]
-
Explanation
If the shorter notation is used (
AccessStrategyType
), you cannot specify any parameters to the AccessStrategyType assigned to that particular Access strategy. If you need to customize the behavior of the AccessStrategyType for that particular AccessStrategy, you have to use the longer syntax ([type: AccessStrategyType, params:Any]
). -
Usage
Currently this declaration is only being used when describing access strategies to resource actions in Collection declaration.
-
Examples
"only_on_tuesdays"
"logged_in"
["and", ["only_on_tuesdays", "logged_in"]]
-
Syntax
type ResourceActionName: "default" | "create" | "retrieve" | "update" | "delete"
-
Usage
It does not have it's own constructor, as it doesn't do anything by itself. It can be used when describing access strategies in Collection declaration.
-
Syntax
type Collection: { name: String, fields: Array<Field>, access_strategy: AccessStrategy | Object<ResourceActionName, AccessStrategy> }
-
Explanation
name
: required. The name of the resource-type.fields
: required. An array of Fields declarations.access_strategy
: required. Describes what access strategies will be used for granting access to calling various resource actions. When a single AccessStrategy is specified, it will be used for all of the actions. If theObject<ResourceActionName, AccessStrategy>
notation is being used, then each action can have a different access_strategy assigned. If an action does not have an AccessStrategy assigned, it will use thedefault
one.
-
Usage
To create a new Collection, call the
Collection
constructor:var Person = new Sealious.Collection({ name: "person", fields: //... });
-
Examples
{ name: "person", fields: [ {name: "full-name", type: "text", required: true}, {name: "age", type: "int"} ], access_strategy: { default: "owner", view: "public", create: "logged_in" } }
"Base chips" are chips that come bundled with Sealious and don't require separate installation.
and
access strategy type combines multiple access strategies into one. It resolves only if all of the access strategies provided in its params
resolve, and it rejects otherwise.
It is sensitive to the context
and/or item
- depending on the strategies in params
.
type and_params: Array<AccessStrategy>
["and", ["logged_in", "only_on_tuesdays"]]
Resolves when the user_id in the provided context is set (not null
), rejects otherwise.
-
Sensitivity
It is only sensitive to the
context
argument. -
Params synopsis
This Access Strategy Type does not take any parameters.
-
Example instance declaration
"logged_in"
Always rejects.
-
Sensitivity
sensitive to nothing. Always rejects.
-
Params synopsis
This Access Strategy Type does not take any parameters.
-
Example instance declaration
"noone"
Takes an Access Strategy as an argument. Resolves if the strategy in the params
rejects. Rejects if the strategy in the params resolves.
-
Sensitivity
It is sensitive to the
context
and/oritem
- depending on the strategy inparams
. -
Params synopsis
type not_params: AccessStrategy
-
Example instance declaration
["not", "logged_in"]
["not", ["and", ["logged_in", "owner"]]]
Similarly to the and access strategy type, this strategy type takes a list of AccessStrategies as the only parameter. It resolves iff one of the strategies on the list resolves.
-
Sensitivity
It is sensitive to the
context
and/oritem
- depending on the strategies inparams
. -
Params synopsis
type or_params: Array<AccessStrategy>
-
Example instance declaration
["or", ["owner", "admin"]]
Resolves only if the user_id
in the provided context matches the user_id
in the created_context
attribute of the given item
.
-
Sensitivity
It is sensitive to the
context
and to theitem
arguments. -
Params synopsis
This Access Strategy Type does not take any parameters.
-
Example instance declaration
"owner"
Always resolves.
-
Sensitivity
Sensitive to nothing. Resolves for any given arguments.
-
Params synopsis
This Access Strategy Type does not take any parameters.
-
Example instance declaration
"public"
Resolves only if the provided Context is an instance of SuperContext.
-
Sensitivity
It is sensitive to the
context
argument only. -
Params synopsis
This Access Strategy Type does not take any parameters.
-
Example instance declaration
"super"
Resolves only if the user_id
in the context
argument matches the id
attribute of the item
argument.
Useful for creating access strategies for the User
Collection.
-
Sensitivity
It is sensitive to the
context
and to theitem
arguments. -
Params synopsis
This Access Strategy Type does not take any parameters.
-
Example instance declaration
"themselves"
Stores a true/false value.
-
acceptable values:
This field type tries really hard to understand vast amount of ways one can want to represent a boolean value, including:
- a
boolean
value:true
,false
; - a string:
"1"
,"0"
,"true"
,"false"
,"True"
,"False"
; - a number:
1
,0
.
- a
-
sensitivity
This field type is only sensitive to the provided
value
. -
storage format
Whatever the input value, the value stored in the database is going to be a Boolean.
Stores a color value
-
acceptable values:
This field will accept any format that's acceptable in CSS, including:
- rgb:
rgb(255, 255, 255)
; - hsl:
hsl(0, 0%, 100%)
; - hex:
#fffff
.
- rgb:
-
sensitivity
This field type is only sensitive to the provided
value
. -
storage format
The colors are stored in the database as strings containing hex color representation.
Stores a context. Used internally to store the context of the last login of a particular user.
-
acceptable values:
This field will only accept objects that are an instance of
Context
-
sensitivity
This field type is only sensitive to the provided
context
. -
storage format:
Values for this field type will be stored as an object.
Used for storing dates, without information on time of day. See also: datetime.
-
acceptable values:
Accepts all dates in ISO standard 8601, that is:
YYYY-MM-DD
.Examples:
"2016-07-04"
"1999-12-31"
-
sensitivity
This field type is only sensitive to the provided
value
. -
storage format:
The values are stored verbatim as strings
Stores timestamps in the form of milliseconds passed since the Epoch. This time format was chosen to mitigate timezone issues.
-
acceptable values:
Any (positive or negative) Number value is accepted.
Examples:
1467645583744
represents2016-07-04T15:19:43.744Z
0
represents1970-01-01T00:00:00.000Z
-1467645583744
represents1923-06-30T08:40:16.256Z
-
sensitivity
This field type is only sensitive to the provided
value
. -
storage format
Values for this field type are kept verbatim as numbers in the datastore.
Stores a proper email address.
-
acceptable values:
Accepts email addresses that match the following regular expression:
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
Example:
-
sensitivity
This field type is only sensitive to the provided
value
. -
storage format
Values for this field type are kept verbatim as strings.
Stores a binary file.
Note: the files are stored in the uploaded_files
directory, located next to your app's index.js
file.
-
acceptable values
This field accepts:
- objects that are an instance of
Sealious.File
and - strings that are HTTP URLs to an accessible file
as it's
value
parameter.Here's an example on how to create a Sealious.File instance:
var context = new Sealious.Context(); var buffer = fs.readFile("photo.jpg"); var file = new Sealious.File(context, "photo.jpg", buffer);
- objects that are an instance of
-
sensitivity
This field-type's behavior depends only on the provided
value
s. -
storage format
The uploaded files are stored in the
uploaded_files
directory, located next to your app'sindex.js
file. The files have randomly generated names, and have no extension.In the datastore they are represented as entries taking the form of:
{ "id": "the_random_id", "filename": "the_original_filename" }
Stores any real number.
-
acceptable values:
Accepts any number. Interprets
.
as the decimal separator.Examples:
2
,3.141592654
,-35432432132124123
-
sensitivity
This field type's behavior depends only on the provided
value
s. -
storage format
The values are stored in the datastore as verbatim real numbers (floats).
Extends the text field-type. Takes any text, and if it meets the criteria specified in the field's params, hashes it with the RFC 2898-recommended PBKDF2 algorithm.
-
params synopsis
type hashed_text_params: { required_digits?: Number, required_capitals?: Number, hide_hash?: Boolean
required_digits
: optional. If specified, thevalue
will not be accepted if it doesn't contain at least that many digits.required_capitals
: optional. If specified, thevalue
will not be accepted if it doesn't contain at least that many capitals.hide_hash
: optional. If set totrue
, the hash value will be hidden (the value for the field will benull
). Useful for password fields.
Also, all of the params from the text field-type apply.
-
acceptable values
Any string that matches the requirements specified in the
params
. -
storage format
The values are stored as a string containing the PBKDF2 hash of the input. There's no practical way to get back the original input.
Accepts only integer numbers (positive or negative).
-
sensitivity
Sensitive only to the provided
value
. -
params synopsis
This field type does not take any params.
-
acceptable values
Any integer number, or a string representation of an integer number.
Examples:
1
,11
,123
-2
,0
;
Will not accept non-integer numbers.
-
storage format
The values are stored verbatim as Numbers in the datastore.
-
filter format
int_filter: Number | Range;
Can reference any other resource.
-
params synopsis
type single_reference_params: { collection: CollectionDeclaration }
collection
: required. Only references to resources of that particular type will be accepted.
-
sensitivity
The behavior of this resource type depends on
context
,params
andvalue
. -
acceptable values
Accepts only IDs of existing resources of the type specified in the
params
.Example:
qytmp7waxm
-
storage format
The input values are stored as strings containing the referenced resource's ID.
Used for storing text.
-
params synopsis
type text_params: { max_length?: Number, min_length?: Number, include_in_search?: Boolean, }
Explanation:
max_length
: optional. If specified, all text with char count higher than that will be rejected with an appropriate error message.min_length
: optional. If specified, all text with char count lower than that will be rejected with an appropriate error message.full_text_search
: optional. Defaults tofalse
. If set totrue
, the datastore will be instructed to create a full-text index on contents of the field.
-
sensitivity
The behavior of this field type depends on provided
value
andparams
. -
acceptable values
Will accept all strings that meets the criteria contained in
params
. -
storage format
The text will be stored as an object with two properties:
- original: the original string, as input by the user;
- safe: a html-safe version of the user input;
Extends the text field-type.
-
acceptable values
Accepts only strings that are not already used as a username, and which are neither of:
"me"
,"myself"
,"current"
. -
Sensitivity
Things that can influence the behavior of fields of this type:
value
- the state of the datastore (existing usernames)
params
-
params synopsis
See params for the text field type.
-
storage format
See: text field type.
Extends the text field-type.
-
acceptable values
Accepts all text values that would be accepted by field-type-text with the same restrictions in params.
-
Sensitivity
Things that can influence the behavior of fields of this type:
value
params
-
params synopsis
See params for the text field type.
-
storage format
Similar to text field type, but the 'safe' value is additionally deprived of any xss code.
The default field in the User resource type are:
- username (type: username), required;
- email (type: email)
- password (type: hashed-text), required. Min-length: 6.
- last_login_context (type: context)
By default, anyone can register (create a user), but only the user itself can edit or delete the account.
Ranges are used to specify a range of numbers. Synnopsis:
type Range{
'<' | 'lt'?: Number,
'>' | 'gt'?: Number,
'<=' | '=<' | 'lte'?: Number,
'>=' | '=>' | 'gte'?: Number,
}
Examples:
{'>': 100}
- a number greater than 100{'>': 100, '<=': 200}
- a number greater than 100, but smaller than or equal to 200