@balena/pinejs 15.0.0-true-boolean-7896b116c446d891d7a0d5e4085c02a13bc9c725 → 15.0.1-build-migrations-clarify-marking-sbvr-optional-d6d0ded8eccc6eadb2492f4697918cf0afd00215-1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/workflows/flowzone.yml +21 -0
  3. package/.husky/pre-commit +4 -0
  4. package/.pinejs-cache.json +1 -0
  5. package/.resinci.yml +1 -0
  6. package/.versionbot/CHANGELOG.yml +9678 -2002
  7. package/CHANGELOG.md +2976 -2
  8. package/Dockerfile +14 -0
  9. package/Gruntfile.ts +3 -6
  10. package/README.md +10 -1
  11. package/VERSION +1 -0
  12. package/build/browser.ts +1 -1
  13. package/build/config.ts +0 -1
  14. package/docker-compose.npm-test.yml +11 -0
  15. package/docs/AdvancedUsage.md +77 -63
  16. package/docs/GettingStarted.md +90 -41
  17. package/docs/Migrations.md +102 -1
  18. package/docs/ProjectConfig.md +12 -21
  19. package/docs/Testing.md +7 -0
  20. package/out/bin/abstract-sql-compiler.js +17 -17
  21. package/out/bin/abstract-sql-compiler.js.map +1 -1
  22. package/out/bin/odata-compiler.js +23 -20
  23. package/out/bin/odata-compiler.js.map +1 -1
  24. package/out/bin/sbvr-compiler.js +22 -22
  25. package/out/bin/sbvr-compiler.js.map +1 -1
  26. package/out/bin/utils.d.ts +2 -2
  27. package/out/bin/utils.js +3 -3
  28. package/out/bin/utils.js.map +1 -1
  29. package/out/config-loader/config-loader.d.ts +9 -8
  30. package/out/config-loader/config-loader.js +135 -78
  31. package/out/config-loader/config-loader.js.map +1 -1
  32. package/out/config-loader/env.d.ts +41 -16
  33. package/out/config-loader/env.js +46 -2
  34. package/out/config-loader/env.js.map +1 -1
  35. package/out/data-server/sbvr-server.d.ts +2 -19
  36. package/out/data-server/sbvr-server.js +44 -38
  37. package/out/data-server/sbvr-server.js.map +1 -1
  38. package/out/database-layer/db.d.ts +32 -14
  39. package/out/database-layer/db.js +120 -41
  40. package/out/database-layer/db.js.map +1 -1
  41. package/out/express-emulator/express.js +10 -11
  42. package/out/express-emulator/express.js.map +1 -1
  43. package/out/http-transactions/transactions.d.ts +2 -18
  44. package/out/http-transactions/transactions.js +29 -21
  45. package/out/http-transactions/transactions.js.map +1 -1
  46. package/out/migrator/async.d.ts +7 -0
  47. package/out/migrator/async.js +168 -0
  48. package/out/migrator/async.js.map +1 -0
  49. package/out/migrator/migrations.sbvr +43 -0
  50. package/out/migrator/sync.d.ts +9 -0
  51. package/out/migrator/sync.js +106 -0
  52. package/out/migrator/sync.js.map +1 -0
  53. package/out/migrator/utils.d.ts +78 -0
  54. package/out/migrator/utils.js +283 -0
  55. package/out/migrator/utils.js.map +1 -0
  56. package/out/odata-metadata/odata-metadata-generator.js +10 -13
  57. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  58. package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
  59. package/out/passport-pinejs/passport-pinejs.js +8 -7
  60. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  61. package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
  62. package/out/pinejs-session-store/pinejs-session-store.js +20 -6
  63. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  64. package/out/sbvr-api/abstract-sql.d.ts +3 -2
  65. package/out/sbvr-api/abstract-sql.js +9 -9
  66. package/out/sbvr-api/abstract-sql.js.map +1 -1
  67. package/out/sbvr-api/cached-compile.js +1 -1
  68. package/out/sbvr-api/cached-compile.js.map +1 -1
  69. package/out/sbvr-api/common-types.d.ts +6 -5
  70. package/out/sbvr-api/control-flow.d.ts +8 -1
  71. package/out/sbvr-api/control-flow.js +36 -9
  72. package/out/sbvr-api/control-flow.js.map +1 -1
  73. package/out/sbvr-api/errors.d.ts +47 -40
  74. package/out/sbvr-api/errors.js +78 -77
  75. package/out/sbvr-api/errors.js.map +1 -1
  76. package/out/sbvr-api/express-extension.d.ts +4 -0
  77. package/out/sbvr-api/hooks.d.ts +16 -15
  78. package/out/sbvr-api/hooks.js +74 -48
  79. package/out/sbvr-api/hooks.js.map +1 -1
  80. package/out/sbvr-api/odata-response.d.ts +2 -2
  81. package/out/sbvr-api/odata-response.js +28 -30
  82. package/out/sbvr-api/odata-response.js.map +1 -1
  83. package/out/sbvr-api/permissions.d.ts +17 -16
  84. package/out/sbvr-api/permissions.js +369 -304
  85. package/out/sbvr-api/permissions.js.map +1 -1
  86. package/out/sbvr-api/sbvr-utils.d.ts +33 -15
  87. package/out/sbvr-api/sbvr-utils.js +397 -235
  88. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  89. package/out/sbvr-api/translations.d.ts +6 -0
  90. package/out/sbvr-api/translations.js +150 -0
  91. package/out/sbvr-api/translations.js.map +1 -0
  92. package/out/sbvr-api/uri-parser.d.ts +23 -17
  93. package/out/sbvr-api/uri-parser.js +33 -27
  94. package/out/sbvr-api/uri-parser.js.map +1 -1
  95. package/out/sbvr-api/user.sbvr +2 -0
  96. package/out/server-glue/module.d.ts +6 -6
  97. package/out/server-glue/module.js +4 -2
  98. package/out/server-glue/module.js.map +1 -1
  99. package/out/server-glue/server.js +5 -5
  100. package/out/server-glue/server.js.map +1 -1
  101. package/package.json +89 -73
  102. package/pinejs.png +0 -0
  103. package/repo.yml +9 -9
  104. package/src/bin/abstract-sql-compiler.ts +5 -7
  105. package/src/bin/odata-compiler.ts +11 -13
  106. package/src/bin/sbvr-compiler.ts +11 -17
  107. package/src/bin/utils.ts +3 -5
  108. package/src/config-loader/config-loader.ts +167 -53
  109. package/src/config-loader/env.ts +106 -6
  110. package/src/data-server/sbvr-server.js +44 -38
  111. package/src/database-layer/db.ts +205 -64
  112. package/src/express-emulator/express.js +10 -11
  113. package/src/http-transactions/transactions.js +29 -21
  114. package/src/migrator/async.ts +323 -0
  115. package/src/migrator/migrations.sbvr +43 -0
  116. package/src/migrator/sync.ts +152 -0
  117. package/src/migrator/utils.ts +458 -0
  118. package/src/odata-metadata/odata-metadata-generator.ts +12 -15
  119. package/src/passport-pinejs/passport-pinejs.ts +9 -7
  120. package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
  121. package/src/sbvr-api/abstract-sql.ts +17 -14
  122. package/src/sbvr-api/common-types.ts +2 -1
  123. package/src/sbvr-api/control-flow.ts +45 -11
  124. package/src/sbvr-api/errors.ts +82 -77
  125. package/src/sbvr-api/express-extension.ts +6 -1
  126. package/src/sbvr-api/hooks.ts +123 -50
  127. package/src/sbvr-api/odata-response.ts +23 -28
  128. package/src/sbvr-api/permissions.ts +548 -415
  129. package/src/sbvr-api/sbvr-utils.ts +581 -259
  130. package/src/sbvr-api/translations.ts +248 -0
  131. package/src/sbvr-api/uri-parser.ts +63 -49
  132. package/src/sbvr-api/user.sbvr +2 -0
  133. package/src/server-glue/module.ts +16 -10
  134. package/src/server-glue/server.ts +5 -5
  135. package/tsconfig.dev.json +1 -0
  136. package/tsconfig.json +1 -2
  137. package/typings/lf-to-abstract-sql.d.ts +6 -9
  138. package/typings/memoizee.d.ts +1 -1
  139. package/.github/CODEOWNERS +0 -1
  140. package/circle.yml +0 -37
  141. package/docs/todo.txt +0 -22
  142. package/out/migrator/migrator.d.ts +0 -20
  143. package/out/migrator/migrator.js +0 -188
  144. package/out/migrator/migrator.js.map +0 -1
  145. package/src/migrator/migrator.ts +0 -286
package/Dockerfile ADDED
@@ -0,0 +1,14 @@
1
+ FROM node:16-alpine as runner
2
+
3
+ WORKDIR /usr/src/pine
4
+
5
+ COPY . ./
6
+ RUN npm install
7
+
8
+
9
+ FROM runner as sut
10
+ CMD npm run mocha
11
+
12
+ FROM runner
13
+
14
+
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 = (config.plugins as Array<
63
- WebpackPluginInstance & { definitions?: {} }
64
- >).find((plugin) => plugin.definitions != null)!.definitions;
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
- # Pine.js
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.1
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.DEBUG': true,
21
+ 'process.env.PINEJS_DEBUG': true,
22
22
  'process.env.SBVR_SERVER_ENABLED': true,
23
23
  }),
24
24
  );
package/build/config.ts CHANGED
@@ -18,7 +18,6 @@ export = {
18
18
  externals: {
19
19
  bcrypt: true,
20
20
  bcryptjs: true,
21
- bluebird: true,
22
21
  'body-parser': true,
23
22
  child_process: true,
24
23
  compression: true,
@@ -0,0 +1,11 @@
1
+ version: "2.1"
2
+ services:
3
+ postgres:
4
+ image: postgres:15-alpine
5
+ restart: always
6
+ environment:
7
+ POSTGRES_USER: docker
8
+ POSTGRES_PASSWORD: docker
9
+ POSTGRES_DB: postgres
10
+ ports:
11
+ - "5431:5432"
@@ -1,13 +1,15 @@
1
- # Advanced Pinejs use
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). In this tutorial we will iterate on the previous model to add a custom validation step when creating a new device.
4
- To achieve this we can use (Hooks)[./Hooks.md], they allow us to execute custom server code whenever an API call is made.
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 Sever Code
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, 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.
10
- Go ahead and make an empty `example.coffee` file, pine can load custom code written in both Javascript or CoffeeScript, however we will stick to CoffeeScript for this guide.
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
- "customServerCode": "example.coffee"
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 devtype. This way we can have multiple devices with the same name as long as they correspond to different underlying dev types.
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.coffee` file we will export a setup function with the following signature
63
+ In our `example.js` file we will export a setup function with the following signature
62
64
 
63
- ```coffee-script
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). The last argument passed is a callback to signal the setup is complete; since the callback is wrapped into a promise, we also have to option to ignore it and directly return a promise from the `setup` function. The setup will fail if the returned promise resolves to an error and succeed otherwise.
68
-
69
- There are many options for when a hook must run, in our case, we want to run the validation step right after pine parses the request as this is the earliest moment we will have access to the information we will need.
70
-
71
- To do so, we can add a `POSTPARSE` hook: this hook should only run on `POST` requests which attempt to create devices.
72
- This translates to the following function
73
-
74
- ```coffee-script
75
- sbvrUtils.addHook 'POST', 'example', 'device',
76
- POSTPARSE: ({ req, request, api }) ->
77
- validate(request, api)
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 devtype, is unique to the device being created.
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 devtype.
85
-
86
- The api object that is passed in the `POSTPARSE` callback is a special object generated by pinejs. 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.
87
-
88
- We can use this to check if the model contains a device with the same name/devtype combination.
89
-
90
- ```coffee-script
91
- validate = (request, api) ->
92
- api.get
93
- resource: request.resourceName
94
- options:
95
- $filter:
96
- name: request.values.name
97
- devtype: request.values.devtype
98
- .then (result) ->
99
- if result?.length > 0
100
- throw new Error('Each device must have a different name/devtype pair')
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 devtype match the ones we found in the request values, if any result is found this means that the pair is already taken.
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
- ```coffee-script
110
- exports.setup = (app, sbvrUtils, db, callback) ->
111
-
112
- sbvrUtils.addHook 'POST', 'example', 'device',
113
- POSTPARSE: ({ req, request, api }) ->
114
- validate(request, api)
115
-
116
- validate = (request, api) ->
117
- api.get
118
- resource: request.resourceName
119
- options:
120
- $filter:
121
- name: request.values.name
122
- devtype: request.values.devtype
123
- .then (result) ->
124
- if result?.length > 0
125
- throw new Error('Each device must have a different name/devtype pair')
126
-
127
- callback()
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 sbvr model: [Migrations.md](./Migrations.md)
132
- * Learn more about what you can do in the setup function from the documentation [CustomServerCode.md](./CustomServerCode.md)
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)
@@ -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](https://github.com/balena-io/pinejs/blob/master/README.md) file of this repo and you have understood the main concepts of Pine.js.
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. 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:
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
- $ createuser -W exampler
13
- $ createdb example -O exampler
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. You can also use your favorite tool to achieve the same result, such as pgAdmin.
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
- $ npm init
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. The above command will initialize your application by creating the `package.json` file.
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
- $ npm install --save @balena/pinejs
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. Open your favorite editor and type the following into the `config.json` file:
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. You can read more about project configuration in [ProjectConfig](https://github.com/balena-io/pinejs/blob/master/docs/ProjectConfig.md).
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, 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.
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
- $ npm install --save coffeescript
96
- $ npm install --save express
97
- $ npm install --save body-parser
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
- And inside your `src` folder, create a file `app.coffee` with the following content:
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
- $ DATABASE_URL=postgres://exampler:[your_password]@localhost:5432/example npm start
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. Once the server is up, use your favourite tool, such as pgAdmin, to connect to the database and take a look inside. 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. 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.
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. 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.
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
- $ curl -X POST -d name=testdevice -d note=testnote -d type=raspberry http://localhost:1337/example/device
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
- $ curl -X GET http://localhost:1337/example/device
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
- $ curl -X GET http://localhost:1337/example/device(1)
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
- $ curl -X PUT -d name=testdevice -d note=updatednote http://localhost:1337/example/device(1)
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. One of these was that each device has exactly one type, but in the request we are forgot about this; luckily Pine.js can catch this kind of mistakes and will reject the update.
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
- $ curl -X PUT -d name=testdevice -d note=updatednote -d type=raspberry http://localhost:1337/example/device(1)
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. 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.
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](https://github.com/balena-io/pinejs/blob/master/docs/Migrations.md)
239
- * Learn about [Hooks](https://github.com/balena-io/pinejs/blob/master/docs/Hooks.md) that you can implement in order to execute custom code when API calls are requested.
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.
@@ -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
+ - Mark the old field as optional in the sbvr if it isn't, or set a default value for it.
79
+ * Deployment 3
80
+ - Update the service's implementation to stop settings the old column and remove it from the sbvr.
81
+ - Make the old field NULLable if it isn't.
82
+ * Deployment 4
83
+ - Delete the old column with a sync migration.
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.