@balena/pinejs 15.0.0-true-boolean-911aca4062d3132ad3c34712014739b6849fa13a → 15.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.dockerignore +4 -0
- package/.github/workflows/flowzone.yml +21 -0
- package/.husky/pre-commit +4 -0
- package/.pinejs-cache.json +1 -0
- package/.resinci.yml +1 -0
- package/.versionbot/CHANGELOG.yml +9678 -2001
- package/CHANGELOG.md +2975 -2
- package/Dockerfile +14 -0
- package/Gruntfile.ts +3 -6
- package/README.md +10 -1
- package/VERSION +1 -0
- package/build/browser.ts +1 -1
- package/build/config.ts +0 -1
- package/docker-compose.npm-test.yml +11 -0
- package/docs/AdvancedUsage.md +77 -63
- package/docs/GettingStarted.md +90 -41
- package/docs/Migrations.md +102 -1
- package/docs/ProjectConfig.md +12 -21
- package/docs/Testing.md +7 -0
- package/out/bin/abstract-sql-compiler.js +17 -17
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +23 -20
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +22 -22
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/bin/utils.d.ts +2 -2
- package/out/bin/utils.js +3 -3
- package/out/bin/utils.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +9 -8
- package/out/config-loader/config-loader.js +135 -78
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +41 -16
- package/out/config-loader/env.js +46 -2
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +2 -19
- package/out/data-server/sbvr-server.js +44 -38
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.d.ts +32 -14
- package/out/database-layer/db.js +120 -41
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +10 -11
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +2 -18
- package/out/http-transactions/transactions.js +29 -21
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.d.ts +7 -0
- package/out/migrator/async.js +168 -0
- package/out/migrator/async.js.map +1 -0
- package/out/migrator/migrations.sbvr +43 -0
- package/out/migrator/sync.d.ts +9 -0
- package/out/migrator/sync.js +106 -0
- package/out/migrator/sync.js.map +1 -0
- package/out/migrator/utils.d.ts +78 -0
- package/out/migrator/utils.js +283 -0
- package/out/migrator/utils.js.map +1 -0
- package/out/odata-metadata/odata-metadata-generator.js +10 -13
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
- package/out/passport-pinejs/passport-pinejs.js +8 -7
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +20 -6
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +3 -2
- package/out/sbvr-api/abstract-sql.js +9 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +1 -1
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/common-types.d.ts +6 -5
- package/out/sbvr-api/control-flow.d.ts +8 -1
- package/out/sbvr-api/control-flow.js +36 -9
- package/out/sbvr-api/control-flow.js.map +1 -1
- package/out/sbvr-api/errors.d.ts +47 -40
- package/out/sbvr-api/errors.js +78 -77
- package/out/sbvr-api/errors.js.map +1 -1
- package/out/sbvr-api/express-extension.d.ts +4 -0
- package/out/sbvr-api/hooks.d.ts +16 -15
- package/out/sbvr-api/hooks.js +74 -48
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.d.ts +2 -2
- package/out/sbvr-api/odata-response.js +28 -30
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.d.ts +17 -16
- package/out/sbvr-api/permissions.js +369 -304
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +33 -15
- package/out/sbvr-api/sbvr-utils.js +397 -235
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +6 -0
- package/out/sbvr-api/translations.js +150 -0
- package/out/sbvr-api/translations.js.map +1 -0
- package/out/sbvr-api/uri-parser.d.ts +23 -17
- package/out/sbvr-api/uri-parser.js +33 -27
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/sbvr-api/user.sbvr +2 -0
- package/out/server-glue/module.d.ts +6 -6
- package/out/server-glue/module.js +4 -2
- package/out/server-glue/module.js.map +1 -1
- package/out/server-glue/server.js +5 -5
- package/out/server-glue/server.js.map +1 -1
- package/package.json +89 -73
- package/pinejs.png +0 -0
- package/repo.yml +9 -9
- package/src/bin/abstract-sql-compiler.ts +5 -7
- package/src/bin/odata-compiler.ts +11 -13
- package/src/bin/sbvr-compiler.ts +11 -17
- package/src/bin/utils.ts +3 -5
- package/src/config-loader/config-loader.ts +167 -53
- package/src/config-loader/env.ts +106 -6
- package/src/data-server/sbvr-server.js +44 -38
- package/src/database-layer/db.ts +205 -64
- package/src/express-emulator/express.js +10 -11
- package/src/http-transactions/transactions.js +29 -21
- package/src/migrator/async.ts +323 -0
- package/src/migrator/migrations.sbvr +43 -0
- package/src/migrator/sync.ts +152 -0
- package/src/migrator/utils.ts +458 -0
- package/src/odata-metadata/odata-metadata-generator.ts +12 -15
- package/src/passport-pinejs/passport-pinejs.ts +9 -7
- package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
- package/src/sbvr-api/abstract-sql.ts +17 -14
- package/src/sbvr-api/common-types.ts +2 -1
- package/src/sbvr-api/control-flow.ts +45 -11
- package/src/sbvr-api/errors.ts +82 -77
- package/src/sbvr-api/express-extension.ts +6 -1
- package/src/sbvr-api/hooks.ts +123 -50
- package/src/sbvr-api/odata-response.ts +23 -28
- package/src/sbvr-api/permissions.ts +548 -415
- package/src/sbvr-api/sbvr-utils.ts +581 -259
- package/src/sbvr-api/translations.ts +248 -0
- package/src/sbvr-api/uri-parser.ts +63 -49
- package/src/sbvr-api/user.sbvr +2 -0
- package/src/server-glue/module.ts +16 -10
- package/src/server-glue/server.ts +5 -5
- package/tsconfig.dev.json +1 -0
- package/tsconfig.json +1 -2
- package/typings/lf-to-abstract-sql.d.ts +6 -9
- package/typings/memoizee.d.ts +1 -1
- package/.github/CODEOWNERS +0 -1
- package/circle.yml +0 -37
- package/docs/todo.txt +0 -22
- package/out/migrator/migrator.d.ts +0 -20
- package/out/migrator/migrator.js +0 -188
- package/out/migrator/migrator.js.map +0 -1
- package/src/migrator/migrator.ts +0 -286
package/Dockerfile
ADDED
package/Gruntfile.ts
CHANGED
@@ -17,16 +17,13 @@ _.forEach(serverConfigs, (config) => {
|
|
17
17
|
config.optimization = {
|
18
18
|
minimizer: [
|
19
19
|
new TerserPlugin({
|
20
|
-
cache: true,
|
21
20
|
parallel: true,
|
22
|
-
sourceMap: true,
|
23
21
|
terserOptions: {
|
24
22
|
output: {
|
25
23
|
beautify: true,
|
26
24
|
ascii_only: true,
|
27
25
|
},
|
28
26
|
compress: {
|
29
|
-
warnings: true,
|
30
27
|
sequences: false,
|
31
28
|
unused: false, // We need this off for OMeta
|
32
29
|
},
|
@@ -59,9 +56,9 @@ export = (grunt: typeof Grunt) => {
|
|
59
56
|
},
|
60
57
|
|
61
58
|
concat: _.mapValues(serverConfigs, (config, task) => {
|
62
|
-
const defines = (
|
63
|
-
WebpackPluginInstance & { definitions?: {} }
|
64
|
-
|
59
|
+
const defines = (
|
60
|
+
config.plugins as Array<WebpackPluginInstance & { definitions?: {} }>
|
61
|
+
).find((plugin) => plugin.definitions != null)!.definitions;
|
65
62
|
return {
|
66
63
|
options: {
|
67
64
|
banner: `
|
package/README.md
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
|
1
|
+
|
2
|
+
<div align="center">
|
3
|
+
<img width="400" height="auto" src="https://raw.githubusercontent.com/balena-io/pinejs/master/pinejs.png">
|
4
|
+
<br>
|
5
|
+
<br>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
[![npm version](https://badge.fury.io/js/@balena%2Fpinejs.svg)](https://badge.fury.io/js/@balena%2Fpinejs)
|
9
|
+
|
10
|
+
|
2
11
|
Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This makes it very easy to rapidly create, update and maintain a backend while keeping the logic in an easily understood form, as well as providing the ability to update and maintain this logic going forward.
|
3
12
|
|
4
13
|
Rules are described in *SBVR* format, which stands for "Semantics of Business Vocabulary and Business Rules". SBVR provides a way to capture specifications in natural language and represent them in formal logic, so they can be machine processed.
|
package/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
15.0.0
|
package/build/browser.ts
CHANGED
@@ -18,7 +18,7 @@ config.plugins = config.plugins.concat(
|
|
18
18
|
new webpack.DefinePlugin({
|
19
19
|
'process.browser': true,
|
20
20
|
'process.env.CONFIG_LOADER_DISABLED': true,
|
21
|
-
'process.env.
|
21
|
+
'process.env.PINEJS_DEBUG': true,
|
22
22
|
'process.env.SBVR_SERVER_ENABLED': true,
|
23
23
|
}),
|
24
24
|
);
|
package/build/config.ts
CHANGED
package/docs/AdvancedUsage.md
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
-
# Advanced
|
1
|
+
# Advanced Pine.js usage
|
2
2
|
|
3
|
-
This guide assumes you have already read through the [getting started guide](./GettingStarted.md) and the [project configuration guide](./ProjectConfig.md).
|
4
|
-
|
3
|
+
This guide assumes you have already read through the [getting started guide](./GettingStarted.md) and the [project configuration guide](./ProjectConfig.md).
|
4
|
+
In this tutorial we will iterate on the previous model to add a custom validation step when creating a new device.
|
5
|
+
To achieve this we can use [Hooks](./Hooks.md), they allow us to execute custom server code whenever an API call is made.
|
5
6
|
|
6
|
-
## Custom
|
7
|
+
## Custom Server Code
|
7
8
|
To add custom functionality to our module we will have to provide a setup function, this will be called during server startup, and will take care of initializing everything as we need it.
|
8
9
|
|
9
|
-
After following the [getting started guide](./GettingStarted.md), we should have a `.sbvr` file with our model, and a `config.json` file with our configuration,
|
10
|
-
|
10
|
+
After following the [getting started guide](./GettingStarted.md), we should have a `.sbvr` file with our model, and a `config.json` file with our configuration,
|
11
|
+
the only change we have to do for now is to add the name of the file where we will write our custom server code, to the configuration.
|
12
|
+
Go ahead and make an empty `src/example.js` file, Pine.js can load custom code written in both Javascript or CoffeeScript, however we will stick to JavaScript for this guide.
|
11
13
|
|
12
14
|
This is how your files should look at the end of this step.
|
13
15
|
|
@@ -43,7 +45,7 @@ Fact Type: device has type
|
|
43
45
|
"modelName": "Example",
|
44
46
|
"modelFile": "example.sbvr",
|
45
47
|
"apiRoot": "example",
|
46
|
-
|
48
|
+
"customServerCode": "example.js"
|
47
49
|
}],
|
48
50
|
"users": [{
|
49
51
|
"username": "guest",
|
@@ -55,78 +57,90 @@ Fact Type: device has type
|
|
55
57
|
|
56
58
|
Now that we have taken care of the necessary plumbing we can start to write some actual code.
|
57
59
|
|
58
|
-
Lets assume we want to enforce that each device that is created, has a unique pair of name and
|
60
|
+
Lets assume we want to enforce that each device that is created, has a unique pair of name and type. This way we can have multiple devices with the same name as long as they correspond to different underlying dev types.
|
59
61
|
We could enforce this directly from the SBVR model, but we will do so with a custom hook just to show how easy and effective adding a hook can be.
|
60
62
|
|
61
|
-
In our `example.
|
63
|
+
In our `example.js` file we will export a setup function with the following signature
|
62
64
|
|
63
|
-
```
|
64
|
-
exports.setup = (app, sbvrUtils, db, callback)
|
65
|
+
```javascript
|
66
|
+
exports.setup = (app, sbvrUtils, db, callback) => {}
|
65
67
|
```
|
66
68
|
|
67
|
-
This function will be called during pines initialization; inside the `setup` function we can run any custom code, in our case this will be a hook (refer to [Hooks](./Hooks.md) for more information).
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
This translates to the following function
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
69
|
+
This function will be called during pines initialization; inside the `setup` function we can run any custom code, in our case this will be a hook (refer to [Hooks](./Hooks.md) for more information).
|
70
|
+
The last argument passed is a callback to signal the setup is complete; since the callback is wrapped into a promise, we also have the option to ignore it and directly return a promise from the `setup` function.
|
71
|
+
The setup will fail if the returned promise resolves to an error and succeed otherwise.
|
72
|
+
|
73
|
+
There are many options for when a hook must run, in this case we will add a `PRERUN` hook.
|
74
|
+
This translates to the following function:
|
75
|
+
```javascript
|
76
|
+
sbvrUtils.addPureHook('POST', 'example', 'device', {
|
77
|
+
PRERUN: ({ request, api }) => {
|
78
|
+
validate(request, api);
|
79
|
+
}
|
80
|
+
});
|
78
81
|
```
|
79
82
|
|
80
83
|
Now we are just left with needing to define our own validation step: in general this can be either synchronous or asynchronous.
|
81
84
|
In case our hook needs to perform an async action we simply return a promise from it, pine will take care of waiting for the result of this promise before processing a request further.
|
82
85
|
|
83
|
-
Recall we want to enforce that, when creating a device, the pair of name and
|
84
|
-
To check for this condition we can directly query the device resource to see if any device comes up with the same pair of name and
|
85
|
-
|
86
|
-
The api object that is passed in the `
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
86
|
+
Recall we want to enforce that, when creating a device, the pair of name and type is unique to the device being created.
|
87
|
+
To check for this condition we can directly query the device resource to see if any device comes up with the same pair of name and type.
|
88
|
+
|
89
|
+
The `api` object that is passed in the `PRERUN` callback is a special object generated by Pine.js.
|
90
|
+
It can be used to query the model: here it will be bound to the model the hook is referring to and will automatically inherit the user privileges.
|
91
|
+
|
92
|
+
We can use this to check if the model contains a device with the same name/type combination.
|
93
|
+
|
94
|
+
```javascript
|
95
|
+
const validate = (request, api) => {
|
96
|
+
api.get({
|
97
|
+
resource: request.resourceName,
|
98
|
+
options: {
|
99
|
+
$filter: {
|
100
|
+
name: request.values.name,
|
101
|
+
type: request.values.type
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}).then((result) => {
|
105
|
+
if (result && result.length && result.length > 0) {
|
106
|
+
throw new Error('Each device must have a different name/type pair');
|
107
|
+
}
|
108
|
+
});
|
109
|
+
};
|
101
110
|
```
|
102
111
|
|
103
112
|
`resourceName` will contain the name of the resource being accessed by the request, since we specified the hook to only run when requests are made against the `device` resource this will actually be fixed.
|
104
|
-
The query we run will select all devices where name and
|
113
|
+
The query we run will select all devices where name and type match the ones we found in the request values, if any result is found this means that the pair is already taken.
|
105
114
|
To report this to the user we can simply throw an Error with the desired message and pine will relay it back to the end user with an appropriate status code.
|
106
115
|
|
107
116
|
This is the full source code for the tutorial.
|
108
117
|
|
109
|
-
```
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
118
|
+
```javascript
|
119
|
+
const validate = (request, api) => {
|
120
|
+
api.get({
|
121
|
+
resource: request.resourceName,
|
122
|
+
options: {
|
123
|
+
$filter: {
|
124
|
+
name: request.values.name,
|
125
|
+
type: request.values.type
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}).then((result) => {
|
129
|
+
if (result && result.length && result.length > 0) {
|
130
|
+
throw new Error('Each device must have a different name/type pair');
|
131
|
+
}
|
132
|
+
});
|
133
|
+
};
|
134
|
+
|
135
|
+
exports.setup = (_app, sbvrUtils) => {
|
136
|
+
sbvrUtils.addPureHook('POST', 'example', 'device', {
|
137
|
+
PRERUN: ({ request, api }) => {
|
138
|
+
validate(request, api);
|
139
|
+
}
|
140
|
+
});
|
141
|
+
};
|
128
142
|
```
|
129
143
|
|
130
|
-
### Where to go from here
|
131
|
-
* Learn about migrations that you can execute prior to Pine.js executing a given
|
132
|
-
* Learn more about what you can do in the setup function
|
144
|
+
### Where to go from here
|
145
|
+
* Learn about migrations that you can execute prior to Pine.js executing a given SBVR model: [Migrations.md](./Migrations.md)
|
146
|
+
* Learn more about what you can do in the setup function: [CustomServerCode.md](./CustomServerCode.md)
|
package/docs/GettingStarted.md
CHANGED
@@ -1,37 +1,40 @@
|
|
1
1
|
# Getting Started with Pine.js
|
2
2
|
|
3
|
-
This guide assumes that you have already read the main [README](
|
3
|
+
This guide assumes that you have already read the main [README](../README.md) file of this repo and you have understood the main concepts of Pine.js.
|
4
4
|
|
5
5
|
## Initialize an example application
|
6
6
|
|
7
7
|
Let's create a new Pine.js application. We will see that by defining our model rules in SBVR format, Pine.js will create the database schema and will provide out of the box an OData API, ready to use to interact with our database and resources.
|
8
8
|
|
9
|
-
To begin with, you'll need to install PostgreSQL on your system, and configure a database and a user with read/write/metadata permissions on the database.
|
9
|
+
To begin with, you'll need to install PostgreSQL on your system, and configure a database and a user with read/write/metadata permissions on the database.
|
10
|
+
In this guide, we will use `example` as the database name and `exampler` as the user name. Open your favorite terminal and type the following commands:
|
10
11
|
|
11
|
-
```
|
12
|
-
|
13
|
-
|
12
|
+
```sh
|
13
|
+
createuser -W exampler
|
14
|
+
createdb example -O exampler
|
14
15
|
```
|
15
16
|
|
16
|
-
The above commands will create a user with name `exampler` and will prompt for a password, and then will set `exampler` as the database owner.
|
17
|
+
The above commands will create a user with name `exampler` and will prompt for a password, and then will set `exampler` as the database owner.
|
18
|
+
You can also use your favorite tool to achieve the same result, such as pgAdmin.
|
17
19
|
|
18
20
|
Next you'll need to install Pine.js as a dependency of your application. Go to a new directory that will use for your application. Let's say `pine-get-started` and type:
|
19
21
|
|
20
|
-
```
|
21
|
-
|
22
|
+
```sh
|
23
|
+
npm init
|
22
24
|
```
|
23
25
|
|
24
|
-
Feel free to enter any information you like for your application when prompted, like application name, version, description, etc.
|
26
|
+
Feel free to enter any information you like for your application when prompted, like application name, version, description, etc.
|
27
|
+
The above command will initialize your application by creating the `package.json` file.
|
25
28
|
|
26
|
-
```
|
27
|
-
|
29
|
+
```sh
|
30
|
+
npm install @balena/pinejs
|
28
31
|
```
|
29
32
|
|
30
|
-
The above commands will install pinejs as a dependency for your application, i.e. it will create the node_modules directory that amongst others will contain Pine.js, and will update the corresponding record in your `package.json` file.
|
33
|
+
The above commands will install pinejs as a dependency for your application, i.e. it will create the `node_modules` directory that amongst others will contain Pine.js, and will update the corresponding record in your `package.json` file.
|
31
34
|
|
32
35
|
Let's see what your directory looks like now:
|
33
36
|
|
34
|
-
```
|
37
|
+
```sh
|
35
38
|
$ tree -L 3
|
36
39
|
.
|
37
40
|
├── node_modules
|
@@ -42,9 +45,10 @@ $ tree -L 3
|
|
42
45
|
|
43
46
|
Now, create a directory for our source files, `src` and enter that directory.
|
44
47
|
|
45
|
-
First, we have to create a configuration file, `config.json` that will provide to Pine.js the necessary configuration regarding the resource model and the user permissions.
|
48
|
+
First, we have to create a configuration file, `config.json` that will provide to Pine.js the necessary configuration regarding the resource model and the user permissions.
|
49
|
+
Open your favorite editor and type the following into the `config.json` file:
|
46
50
|
|
47
|
-
```
|
51
|
+
```json
|
48
52
|
{
|
49
53
|
"models": [{
|
50
54
|
"modelName": "Example",
|
@@ -59,7 +63,8 @@ First, we have to create a configuration file, `config.json` that will provide t
|
|
59
63
|
}
|
60
64
|
```
|
61
65
|
|
62
|
-
The above file states that Pine.js has to use the file `example.sbvr` to find the model definitions, and `/example` as the root path to access the model's API.
|
66
|
+
The above file states that Pine.js has to use the file `example.sbvr` to find the model definitions, and `/example` as the root path to access the model's API.
|
67
|
+
You can read more about project configuration in [ProjectConfig](./ProjectConfig.md).
|
63
68
|
|
64
69
|
Now, let's create the models. Again in your favorite editor, type the following in the `example.sbvr` file and save it under `src` folder.
|
65
70
|
|
@@ -87,19 +92,58 @@ Fact Type: device has type
|
|
87
92
|
Necessity: each device has exactly one type.
|
88
93
|
```
|
89
94
|
|
90
|
-
In this model we are defining an entity called `device`, this entity has some attributes such as `name`, `note` and `type`, along with some constraints,
|
95
|
+
In this model we are defining an entity called `device`, this entity has some attributes such as `name`, `note` and `type`, along with some constraints,
|
96
|
+
ensuring that a device must have exactly one device type, and at most one name and one note. The `Vocabulary` declaration is a convenient way for partitioning parts of larger SBVR files.
|
97
|
+
|
98
|
+
### Initialise a TypeScript project
|
91
99
|
|
92
100
|
Now, let's create a small main file for our application that will call the Pine.js server. Let's install some basic dependencies:
|
101
|
+
Create a small main file for our application that will call the Pine.js server. Let's install some basic dependencies:
|
93
102
|
|
103
|
+
```sh
|
104
|
+
npm install express body-parser
|
105
|
+
npm install -D typescript ts-node @types/express
|
94
106
|
```
|
95
|
-
|
96
|
-
|
97
|
-
|
107
|
+
|
108
|
+
And inside your `src` folder, create a file `app.ts` with the following content:
|
109
|
+
|
110
|
+
```typescript
|
111
|
+
import express, { Request, Response } from 'express';
|
112
|
+
import * as pine from '@balena/pinejs';
|
113
|
+
|
114
|
+
const app = express();
|
115
|
+
app.use(express.urlencoded({ extended: true }));
|
116
|
+
app.use(express.json());
|
117
|
+
|
118
|
+
app.use('/ping', (_req: Request, res: Response) => {
|
119
|
+
res.sendStatus(200);
|
120
|
+
});
|
121
|
+
|
122
|
+
pine.init(app).then(() => {
|
123
|
+
app.listen(1337, () => {
|
124
|
+
console.log('server started');
|
125
|
+
});
|
126
|
+
});
|
98
127
|
```
|
99
128
|
|
100
|
-
|
129
|
+
Inside your `package.json` file enter the following line inside the section `scripts`:
|
101
130
|
|
102
131
|
```
|
132
|
+
"start": "ts-node src/app.ts src"
|
133
|
+
```
|
134
|
+
|
135
|
+
### Initialise a CoffeeScript project
|
136
|
+
|
137
|
+
Alternatively, here's an example of the same small application written in CoffeeScript.
|
138
|
+
Install some basic dependencies:
|
139
|
+
|
140
|
+
```sh
|
141
|
+
npm install coffeescript express body-parser
|
142
|
+
```
|
143
|
+
|
144
|
+
And inside your `src` folder, create a file `app.coffee` with the following content:
|
145
|
+
|
146
|
+
```coffeescript
|
103
147
|
pinejs = require '@balena/pinejs'
|
104
148
|
express = require 'express'
|
105
149
|
app = express()
|
@@ -118,7 +162,6 @@ pinejs.init(app)
|
|
118
162
|
console.info('Server started')
|
119
163
|
```
|
120
164
|
|
121
|
-
|
122
165
|
Finally, inside your `package.json` file enter the following line inside the section `scripts`:
|
123
166
|
|
124
167
|
```
|
@@ -127,7 +170,7 @@ Finally, inside your `package.json` file enter the following line inside the sec
|
|
127
170
|
|
128
171
|
Let's see what our application directory looks like now:
|
129
172
|
|
130
|
-
```
|
173
|
+
```sh
|
131
174
|
$ tree -L 3
|
132
175
|
.
|
133
176
|
├── node_modules
|
@@ -171,24 +214,28 @@ $ tree -L 3
|
|
171
214
|
|
172
215
|
Assuming postgreSQL is running, execute the following command, replacing `[your_password]` with the password you set for the user `exampler`.
|
173
216
|
|
174
|
-
```
|
175
|
-
|
217
|
+
```sh
|
218
|
+
DATABASE_URL=postgres://exampler:[your_password]@localhost:5432/example npm start
|
176
219
|
```
|
177
220
|
|
178
|
-
Pine.js will connect to the `example` database and it will create the database schema and the associated API endpoints.
|
221
|
+
Pine.js will connect to the `example` database and it will create the database schema and the associated API endpoints.
|
222
|
+
Once the server is up, use your favourite tool, such as pgAdmin, to connect to the database and take a look inside.
|
223
|
+
Among the other things, you will find that Pine.js has created a table called `device`, which will contain the devices we earlier specified in the model.
|
224
|
+
By inspecting the structure of this table, you can see that the constraints specified in sbvr model get directly translated to constraints in the underlying database.
|
179
225
|
|
180
226
|
Pine.js also generates users and permissions; the database will contain a `guest` user with access to all resources, these entities are created internally by Pine.js from the config file we provided.
|
181
227
|
|
182
228
|
## Accessing Resources
|
183
229
|
|
184
|
-
Now that the server is up and running we are able to create, delete or update entities from the specified model.
|
230
|
+
Now that the server is up and running we are able to create, delete or update entities from the specified model.
|
231
|
+
Recall that Pine.js provides access to the database through the associated [OData API](http://www.odata.org), this means that we can make requests to the database following the OData specification to manage our model.
|
185
232
|
|
186
233
|
We will use cURL to make these requests, so open up another terminal window and place it side by side to the one running the server.
|
187
234
|
|
188
235
|
First of all we need to create a device. To do so type the following in the new window:
|
189
236
|
|
190
|
-
```
|
191
|
-
|
237
|
+
```sh
|
238
|
+
curl -X POST -d name=testdevice -d note=testnote -d type=raspberry http://localhost:1337/example/device
|
192
239
|
```
|
193
240
|
|
194
241
|
If the creation succeeds the server will respond with an object representing the new entity, in this case it will look something like this:
|
@@ -201,14 +248,14 @@ Aside from `__metadata` which is used internally, the properties of this object
|
|
201
248
|
|
202
249
|
If we ask the server for a list of devices we will see the one we just created:
|
203
250
|
|
204
|
-
```
|
205
|
-
|
251
|
+
```sh
|
252
|
+
curl -X GET http://localhost:1337/example/device
|
206
253
|
```
|
207
254
|
|
208
255
|
The server will respond with an array containing all the devices, if we want to access a specific one, it is sufficient to add the id at the end of the URL we pass.
|
209
256
|
|
210
|
-
```
|
211
|
-
|
257
|
+
```sh
|
258
|
+
curl -X GET 'http://localhost:1337/example/device(1)'
|
212
259
|
```
|
213
260
|
|
214
261
|
The above cURL request will return the single entity with `id=1`.
|
@@ -216,24 +263,26 @@ The above cURL request will return the single entity with `id=1`.
|
|
216
263
|
To modify the device we just created: the OData specification tells us that to do so we can make a `PUT` request to the endpoint that represents the entity.
|
217
264
|
Lets try this:
|
218
265
|
|
219
|
-
```
|
220
|
-
|
266
|
+
```sh
|
267
|
+
curl -X PUT -d name=testdevice -d note=updatednote 'http://localhost:1337/example/device(1)'
|
221
268
|
|
222
269
|
***
|
223
270
|
Internal Server Error
|
224
271
|
```
|
225
272
|
|
226
|
-
What went wrong here? Pine.js is simply preventing us from violating the constraints we had previously defined.
|
273
|
+
What went wrong here? Pine.js is simply preventing us from violating the constraints we had previously defined.
|
274
|
+
One of these was that each device has exactly one type, but in the request we forgot about this; luckily Pine.js can catch these kind of mistakes and will reject the update.
|
227
275
|
|
228
276
|
To correctly modify the device we can try:
|
229
277
|
|
230
|
-
```
|
231
|
-
|
278
|
+
```sh
|
279
|
+
curl -X PUT -d name=testdevice -d note=updatednote -d type=raspberry 'http://localhost:1337/example/device(1)'
|
232
280
|
```
|
233
281
|
|
234
|
-
You can now try to delete this entity to restore the database to it’s initial state.
|
282
|
+
You can now try to delete this entity to restore the database to it’s initial state.
|
283
|
+
Recall from the OData specification that this can be done by performing a `DELETE` request at the endpoint represented by the entity we intend to delete.
|
235
284
|
|
236
285
|
### Where to go from here:
|
237
286
|
* Follow the [advanced usage guide](./AdvancedUsage.md) that builds on top of this example to add some custom validation via hooks
|
238
|
-
* Learn about migrations that you can execute prior to Pine.js executing a given sbvr model: [Migrations.md](
|
239
|
-
* Learn about [Hooks](
|
287
|
+
* Learn about migrations that you can execute prior to Pine.js executing a given sbvr model: [Migrations.md](./Migrations.md)
|
288
|
+
* Learn about [Hooks](./Hooks.md) that you can implement in order to execute custom code when API calls are requested.
|
package/docs/Migrations.md
CHANGED
@@ -46,4 +46,105 @@ Migrations which are Node modules should export a function which will be called
|
|
46
46
|
|
47
47
|
You can execute SQL using `tx.executeSql(query)`, however note that the model for which the migration is running will *not* be available under `sbvrUtils.api`, as it has not executed yet.
|
48
48
|
|
49
|
-
You should return a promise so pinejs can wait for completion/errors.
|
49
|
+
You should return a promise so pinejs can wait for completion/errors.
|
50
|
+
|
51
|
+
|
52
|
+
## Async Migrations
|
53
|
+
|
54
|
+
Migrations that may lock tables for a long time, eg. migrating data between columns on large tables, can be run as async migrations.
|
55
|
+
|
56
|
+
Async migrations are executed in small batches within individual transactions and run concurrently while Pine is serving requests. Async migrations are multi-instance safe. They are synchronized between instances via the `migration status` database table and the minimum interval between two migration executions is guaranteed across instances.
|
57
|
+
|
58
|
+
When Pine starts and determines there are multiple async migrations to be run, it starts all migrations concurrently. These migrations however do not run in parallel: only one migration batch is executed at any given time, then another, and so on. Thus, migrations should not depend on the result a previous one.
|
59
|
+
Once an async migration starts, it keeps running forever even after Pine instance restarts. The async migration will stop being executed when finalized with a dedicated flag in the async migration definition. This is to guarantee that new data inserted or updated at runtime can also be migrated. The status of running migrations can be checked in `migration status` table. The configured execution parameters and the execution metrics are stored and updated after each migration batch completes.
|
60
|
+
|
61
|
+
Each async migration needs to specify a pair of an async and a sync migration part, so that the sync migration statement closes the async migration and guarantees database consistency.
|
62
|
+
|
63
|
+
Async migrations are stored in the migrations folder, alongside synchronous migrations. Their file names are significant and must contain the string `.async.` so they are treated as async migrations. They can only be Typescript / Javascript (with `.ts`/`.js` extensions) files.
|
64
|
+
|
65
|
+
The async migration query must have a `LIMIT` statement to limit the maximum number of affected rows per batch.
|
66
|
+
|
67
|
+
|
68
|
+
### Async migration procedure
|
69
|
+
* Deployment 1
|
70
|
+
- Add new column (with independent sync migration) to contain new data and add code accessing the new column.
|
71
|
+
- Update the service's implementation to set both the old & new column on each write.
|
72
|
+
- The service's implementation should only read the old column since the async migration still migrates data from old column to new column.
|
73
|
+
- Async migrator runs forever.
|
74
|
+
* Deployment 2
|
75
|
+
- Finalize async migration => only sync migration part gets executed.
|
76
|
+
- Sync migration migrates all left over data from old column to new column.
|
77
|
+
- Update the service's implementation to only read the new column, but still write the old one as well.
|
78
|
+
* Deployment 3
|
79
|
+
- Update the service's implementation to stop settings the old column and remove it from the sbvr.
|
80
|
+
- Make the old field NULLable if it isn't.
|
81
|
+
* Deployment 4
|
82
|
+
- Delete the old column with a sync migration.
|
83
|
+
-
|
84
|
+
|
85
|
+
### TS migration file format with SQL query string
|
86
|
+
|
87
|
+
The placeholder `%%ASYNC_BATCH_SIZE%%` will be replaced with the value specified by asyncBatchSize parameter
|
88
|
+
|
89
|
+
``` javascript
|
90
|
+
export = {
|
91
|
+
asyncSql: `\
|
92
|
+
UPDATE "device"
|
93
|
+
SET "note" = "device"."name"
|
94
|
+
WHERE id IN (
|
95
|
+
SELECT id FROM "device"
|
96
|
+
WHERE "device"."name" <> "device"."note" OR "device"."note" IS NULL
|
97
|
+
LIMIT %%ASYNC_BATCH_SIZE%%
|
98
|
+
);
|
99
|
+
`,
|
100
|
+
syncSql: `\
|
101
|
+
UPDATE "device"
|
102
|
+
SET "note" = "device"."name"
|
103
|
+
WHERE "device"."name" <> "device"."note" OR "device"."note" IS NULL;
|
104
|
+
`,
|
105
|
+
delayMS: 100,
|
106
|
+
backoffDelayMS: 4000,
|
107
|
+
errorThreshold: 15,
|
108
|
+
asyncBatchSize: 1,
|
109
|
+
finalize: true,
|
110
|
+
};
|
111
|
+
```
|
112
|
+
|
113
|
+
### TS migration file format with migrator function definition
|
114
|
+
|
115
|
+
`${options.batchSize}` will be the value specified by asyncBatchSize parameter.
|
116
|
+
|
117
|
+
``` javascript
|
118
|
+
export = {
|
119
|
+
asyncFn: async (tx: any, options) => {
|
120
|
+
const staticSql = `\
|
121
|
+
UPDATE "device"
|
122
|
+
SET "note" = "device"."name"
|
123
|
+
WHERE id IN (
|
124
|
+
SELECT id FROM "device"
|
125
|
+
WHERE "device"."name" <> "device"."note" OR "device"."note" IS NULL
|
126
|
+
LIMIT ${options.batchSize}
|
127
|
+
);
|
128
|
+
`;
|
129
|
+
|
130
|
+
return await tx.executeSql(staticSql);
|
131
|
+
},
|
132
|
+
syncFn: async (tx: any) => {
|
133
|
+
const staticSql = `\
|
134
|
+
UPDATE "device"
|
135
|
+
SET "note" = "device"."name"
|
136
|
+
WHERE "device"."name" <> "device"."note" OR "device"."note" IS NULL;
|
137
|
+
`;
|
138
|
+
|
139
|
+
await tx.executeSql(staticSql);
|
140
|
+
},
|
141
|
+
asyncBatchSize: 1,
|
142
|
+
delayMS: 100,
|
143
|
+
backoffDelayMS: 4000,
|
144
|
+
errorThreshold: 15,
|
145
|
+
finalize: true,
|
146
|
+
};
|
147
|
+
|
148
|
+
```
|
149
|
+
### SQL query file (plain text)
|
150
|
+
Plain SQL files are not supported as they cannot bundle async and sync migration statements in one file. Moreover they cannot carry migration metadata.
|