@vida-global/core 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +9 -0
  2. package/index.js +17 -0
  3. package/lib/active_record/README.md +205 -0
  4. package/lib/active_record/baseRecord.js +112 -0
  5. package/lib/active_record/db/connection.js +128 -0
  6. package/lib/active_record/db/connectionConfiguration.js +114 -0
  7. package/lib/active_record/db/importSchema.js +4 -0
  8. package/lib/active_record/db/migration.js +132 -0
  9. package/lib/active_record/db/migrationTemplate.js +8 -0
  10. package/lib/active_record/db/migrationVersion.js +68 -0
  11. package/lib/active_record/db/migrator.js +169 -0
  12. package/lib/active_record/db/queryInterface.js +47 -0
  13. package/lib/active_record/db/schema.js +113 -0
  14. package/lib/active_record/index.js +6 -0
  15. package/lib/active_record/utils.js +43 -0
  16. package/lib/http/README.md +32 -0
  17. package/lib/http/client.js +129 -0
  18. package/lib/http/error.js +34 -0
  19. package/lib/logger/README.md +2 -0
  20. package/lib/logger/index.js +16 -0
  21. package/lib/release/develop.js +27 -0
  22. package/lib/release/git.js +86 -0
  23. package/lib/release/increment.js +56 -0
  24. package/lib/release/index.js +10 -0
  25. package/lib/release/release.js +30 -0
  26. package/lib/release/utils.js +44 -0
  27. package/lib/server/README.md +37 -0
  28. package/lib/server/index.js +9 -0
  29. package/lib/server/server.js +359 -0
  30. package/lib/server/serverController.js +344 -0
  31. package/lib/server/systemController.js +23 -0
  32. package/package.json +37 -0
  33. package/scripts/active_record/migrate.js +30 -0
  34. package/scripts/release.js +62 -0
  35. package/test/active_record/baseRecord.test.js +179 -0
  36. package/test/active_record/db/connection.test.js +221 -0
  37. package/test/active_record/db/connectionConfiguration.test.js +184 -0
  38. package/test/active_record/db/migrator.test.js +266 -0
  39. package/test/active_record/db/queryInterface.test.js +66 -0
  40. package/test/http/client.test.js +271 -0
  41. package/test/http/error.test.js +71 -0
  42. package/test/release/develop.test.js +57 -0
  43. package/test/release/git.test.js +189 -0
  44. package/test/release/increment.test.js +145 -0
  45. package/test/release/release.test.js +72 -0
  46. package/test/release/utils.test.js +148 -0
  47. package/test/server/helpers/controllers/barController.js +9 -0
  48. package/test/server/helpers/controllers/fooController.js +48 -0
  49. package/test/server/helpers/controllers/sub/bazController.js +10 -0
  50. package/test/server/helpers/server.js +14 -0
  51. package/test/server/server.test.js +188 -0
  52. package/test/server/serverController.test.js +251 -0
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Vida/Core
2
+ This package is intended to contain core elements used across all Vida Apps. Keep in mind that it's included in the `vida-apps-tools` package, so customers developing Vida Apps will have access to it.
3
+
4
+ # Guides
5
+
6
+ - [ActiveRecord](lib/active_record)
7
+ - [HttpClient](lib/http)
8
+ - [Logger](lib/logger)
9
+ - [VidaServer](lib/server)
package/index.js ADDED
@@ -0,0 +1,17 @@
1
+ const { HttpClient, HttpError } = require('./lib/http/client');
2
+ const { AuthorizationError,
3
+ VidaServer,
4
+ VidaServerController } = require('./lib/server');
5
+ const { logger } = require('./lib/logger');
6
+ const ActiveRecord = require('./lib/active_record');
7
+
8
+
9
+ module.exports = {
10
+ ActiveRecord,
11
+ AuthorizationError,
12
+ HttpClient,
13
+ HttpError,
14
+ logger,
15
+ VidaServer,
16
+ VidaServerController,
17
+ };
@@ -0,0 +1,205 @@
1
+ # Configuration
2
+ To configure your database, create a `config.js` file in the `config/db/` directory of your project. That file should follow this format:
3
+ ```
4
+ module.exports = {
5
+ default: {
6
+ development: {
7
+ database: process.env.DATABASE,
8
+ host: process.env.DB_HOST,
9
+ password: process.env.DB_PASSWORD,
10
+ username: process.env.DB_USERNAME
11
+ },
12
+ production: {
13
+ ...
14
+ },
15
+ ...
16
+ }
17
+ }
18
+ ```
19
+
20
+ Add additional entries for multiple databases
21
+ ```
22
+ module.exports = {
23
+ default: {
24
+ ...
25
+ },
26
+ metrics: {
27
+ development: {
28
+ database: process.env.DATABASE,
29
+ host: process.env.DB_HOST,
30
+ password: process.env.DB_PASSWORD,
31
+ username: process.env.DB_USERNAME
32
+ },
33
+ production: {
34
+ ...
35
+ },
36
+ ...
37
+ },
38
+ ...
39
+ }
40
+ ```
41
+
42
+
43
+ ### Connection Pooling
44
+ Minimum and maximum connection pool sizes can be configured in config.js, but default to 0 and 5, respectively.
45
+ ```
46
+ development: {
47
+ database: process.env.DATABASE,
48
+ host: process.env.DB_HOST,
49
+ password: process.env.DB_PASSWORD,
50
+ username: process.env.DB_USERNAME,
51
+ pool: {
52
+ min: 1,
53
+ max: 10
54
+ }
55
+ }
56
+ ```
57
+
58
+
59
+ ### Replication
60
+ Read replicas can be configured by adding a `readers` key to your configuration
61
+ ```
62
+ development: {
63
+ database: process.env.DATABASE,
64
+ host: process.env.DB_HOST,
65
+ password: process.env.DB_PASSWORD,
66
+ username: process.env.DB_USERNAME,
67
+ readers: [
68
+ {
69
+ database: ...
70
+ host: ...
71
+ password: ...
72
+ username: ...
73
+ },
74
+ {
75
+ ...
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+
82
+ # Migrations
83
+ To prepare your project to run database migrations, add the following lines to the `scripts` section of your `package.json`:
84
+ ```
85
+ "db:create_migration": "node node_modules/@vida-global/core/scripts/active_record/migrate.js create_migration",
86
+ "db:migrate": "node node_modules/@vida-global/core/scripts/active_record/migrate.js migrate",
87
+ "db:rollback": "node node_modules/@vida-global/core/scripts/active_record/migrate.js rollback"
88
+ ```
89
+
90
+ To generate a new migration file, run: `npm run db:create_migration createUsers`. This will automatically generate a migration file. Your migration can create tables, create indexes, and add, remove, or update columns.
91
+ ```
92
+ module.exports = {
93
+ up: async function() {
94
+ await this.createTable('users', {
95
+ email: this.DataTypes.STRING,
96
+ team_id: this.DataTypes.INTEGER,
97
+ is_admin: {
98
+ type: this.DataTypes.BOOLEAN,
99
+ defaultValue: false,
100
+ allowNull: false,
101
+ }
102
+ });
103
+
104
+ await this.addIndex('users', ['team_id'], {where: {team_id: {[this.Operators.ne]: null}}});
105
+ await this.addIndex('users', ['email'], {unique: true});
106
+ },
107
+
108
+ down: async function () {
109
+ await this.dropTable('users');
110
+ }
111
+ }
112
+ ```
113
+ Available migrations are:
114
+ ```
115
+ createTable(tableName, details)
116
+ dropTable(tableName)
117
+ addColumn(tableName, columnName, columnDetails)
118
+ removeColumn(tableName, columnName)
119
+ addIndex(tableName, fields, { concurrently, unique, name, where })
120
+ removeIndex(tableName, indexNameOrAttributes, concurrently=false)
121
+ renameColumn(tableName, oldName, newName)
122
+ changeColumn(tableName, columnName, dataTypeOrOptions)
123
+ ```
124
+ To run your migrations, run `npm run db:migrate` or `npm run db:rollback` to rollback a single migration.
125
+
126
+ **NOTE**: By default, `createTable` automatically adds an autoincrement, primary key `id` column, a `created_at` column, and an `updated_at` column.
127
+
128
+
129
+ # Models
130
+ The following code will create a basic model representing a user in our database:
131
+ ```
132
+ const { ActiveRecord } = require('@vida-global/core')
133
+
134
+ class User extends ActiveRecord.BaseRecord {
135
+ }
136
+
137
+ User.initialize();
138
+
139
+ const user = new User({email: 'mark@vida.inc', team_id: 1, is_admin: false});
140
+ await user.save();
141
+
142
+ user.is_admin = true;
143
+ await user.save();
144
+
145
+ await user.destroy();
146
+ ```
147
+
148
+
149
+ ### Querying Models
150
+ ```
151
+ const users = await User.findAll();
152
+ const users = await User.findAll{where: {team_id: 1}});
153
+ const users = await User.findAll{where: User.Operator
154
+
155
+ const users = await User.findAll({where: {
156
+ team_id: {
157
+ [Recording.Operators.or]: [1,2]
158
+ }
159
+ }});
160
+
161
+ ```
162
+
163
+
164
+ ### Aggregators and Functions
165
+ ```
166
+ const rows = await User.findAll({
167
+ attributes: ['team_id', [User.fn('MAX', User.col('id')), 'max_id']],
168
+ group: 'team_id'
169
+ });
170
+
171
+ const { count, users } = await User.findAndCountAll({
172
+ where: {
173
+ email: {
174
+ [User.Operators.like]: '%@vida.inc',
175
+ },
176
+ },
177
+ offset: 0,
178
+ limit: 1,
179
+ });
180
+
181
+ const totalCount = await User.count({where: {team_id: 1}});
182
+ ```
183
+ More examples can be found here, https://sequelize.org/docs/v6/core-concepts/model-querying-basics/.
184
+
185
+ **Note** The `sequelize.fn`, `sequelize.col`, and `Op` properties have been added as helpers on the Model (e.g. `User.fn('MAX', User.col('id'), 'max_id')` and `User.Operators.or`)
186
+
187
+
188
+ ### Associations
189
+ **TODO**
190
+
191
+
192
+ ### Validators
193
+ **TODO**
194
+
195
+
196
+ ### Hooks
197
+ **TODO**
198
+
199
+
200
+ ### Paranoid
201
+ **TODO**
202
+
203
+
204
+ # Closing Connections
205
+ When cleaning up your application, close all open connections with `User.closeAllConnections()`. This will close all connections for all models, not just the `User` model.
@@ -0,0 +1,112 @@
1
+ const { Connection, DEFAULT_DATABASE_ID } = require('./db/connection');
2
+ const { getActiveRecordSchema } = require('./db/schema');
3
+ const { Model, Op } = require('sequelize');
4
+ const { tableize } = require('inflection');
5
+ const utils = require('./utils');
6
+ const nodeUtil = require('util');
7
+
8
+
9
+
10
+ class BaseRecord extends Model {
11
+ static _connection;
12
+ static _initialized = false;
13
+
14
+
15
+ static Operators = Op;
16
+ static get fn() { return this.connection._sequelize.fn }
17
+ static get col() { return this.connection._sequelize.col }
18
+
19
+
20
+ constructor() {
21
+ if (new.target == BaseRecord) throw new Error("BaseRecord must be subclasses");
22
+ super(...arguments);
23
+ }
24
+
25
+
26
+ /***********************************************************************************************
27
+ * QUERIES
28
+ ***********************************************************************************************/
29
+ static async find(id) {
30
+ return await this.findByPk(id);
31
+ }
32
+
33
+
34
+ static async where(where, options={}) {
35
+ const parameters = {...options, where };
36
+ return await this.findAll(parameters);
37
+ }
38
+
39
+
40
+ /***********************************************************************************************
41
+ * INITIALIZATION
42
+ ***********************************************************************************************/
43
+ static initialize() {
44
+ if (this == BaseRecord) throw new Error("BaseRecord must be subclasses");
45
+
46
+ if (this._initialized) return;
47
+
48
+ const schema = getActiveRecordSchema(this._tableName, this.databaseId, this.connection.dialect);
49
+ const options = {
50
+ createdAt: 'created_at',
51
+ deletedAt: 'deleted_at',
52
+ modelName: this.name,
53
+ sequelize: this.connection._sequelize,
54
+ tableName: this._tableName,
55
+ updatedAt: 'updated_at',
56
+ }
57
+
58
+ if (!schema.created_at || !schema.updated_at) {
59
+ options.timestamps = false;
60
+ }
61
+
62
+ this.init(schema, options);
63
+
64
+ this._initialized = true;
65
+ }
66
+
67
+
68
+ /***********************************************************************************************
69
+ * DB CONNECTION
70
+ ***********************************************************************************************/
71
+ static get connection() {
72
+ if (!this._connection) {
73
+ this._connection = new Connection(this.constructor.databaseId);
74
+ }
75
+ return this._connection;
76
+ }
77
+
78
+
79
+ static get databaseId() {
80
+ return DEFAULT_DATABASE_ID;
81
+ }
82
+
83
+
84
+ static get _tableName() {
85
+ return tableize(this.name);
86
+ }
87
+
88
+
89
+ static closeConnection() {
90
+ this.connection.close();
91
+ }
92
+
93
+
94
+ static closeAllConnections() {
95
+ Connection.closeAll();
96
+ }
97
+
98
+
99
+ /***********************************************************************************************
100
+ * MISC
101
+ ***********************************************************************************************/
102
+ [nodeUtil.inspect.custom](opts) {
103
+ const args = Array.from(arguments);
104
+ args.shift();
105
+ return nodeUtil.inspect(this.dataValues, ...args);
106
+ }
107
+ }
108
+
109
+
110
+ module.exports = {
111
+ BaseRecord
112
+ };
@@ -0,0 +1,128 @@
1
+ const { QueryInterface } = require('./queryInterface');
2
+ const { ConnectionConfiguration } = require('./connectionConfiguration');
3
+ const { Sequelize } = require('sequelize');
4
+
5
+
6
+ let sequelizeConnectionsCache = {};
7
+ const DEFAULT_DATABASE_ID = 'default';
8
+
9
+
10
+ class Connection {
11
+ #configuration;
12
+ #databaseId;
13
+ #options;
14
+
15
+ constructor(databaseId, options={}) {
16
+ this.#databaseId = databaseId || DEFAULT_DATABASE_ID;
17
+ this.#configuration = new ConnectionConfiguration(this.#databaseId);
18
+ this.#options = options;
19
+ }
20
+
21
+
22
+ close() {
23
+ this._sequelize.close();
24
+ }
25
+
26
+
27
+ static closeAll() {
28
+ Object.values(sequelizeConnectionsCache).forEach(sequelize => {
29
+ sequelize.close();
30
+ });
31
+ }
32
+
33
+
34
+ static clearConnectionsCache() {
35
+ this.closeAll();
36
+ sequelizeConnectionsCache = {};
37
+ }
38
+
39
+
40
+ get queryInterface() {
41
+ return new QueryInterface(this);
42
+ }
43
+
44
+
45
+ get _sequelize() {
46
+ if (sequelizeConnectionsCache[this.#databaseId]) return sequelizeConnectionsCache[this.#databaseId];
47
+
48
+ const connection = new Sequelize(null, null, null, this.sequelizeOptions);
49
+ sequelizeConnectionsCache[this.#databaseId] = connection;
50
+
51
+ return connection;
52
+ }
53
+
54
+
55
+ get sequelizeOptions() {
56
+ const options = {
57
+ ...this.dialectOptions,
58
+ define: { underscored: true },
59
+ dialect: this.dialect,
60
+ };
61
+
62
+ if (!this.loggingEnabled) options.logging = false;
63
+
64
+ return options;
65
+ }
66
+
67
+
68
+ get dialectOptions() {
69
+ if (this.dialect == 'sqlite') {
70
+ return this.sqliteSequelizeOptions;
71
+ } else {
72
+ return this.postgresSequelizeOptions;
73
+ }
74
+ }
75
+
76
+
77
+ get postgresSequelizeOptions() {
78
+ const options = {
79
+ pool: this.#configuration.pool,
80
+ };
81
+
82
+ const primaryConfig = {
83
+ database: this.#configuration.database,
84
+ host: this.#configuration.host,
85
+ password: this.#configuration.password,
86
+ port: this.#configuration.port,
87
+ username: this.#configuration.username
88
+ };
89
+
90
+ if (this.#configuration.readers.length) {
91
+ options.replication = {
92
+ write: primaryConfig,
93
+ read: this.#configuration.readers
94
+ }
95
+ } else {
96
+ Object.assign(options, primaryConfig);
97
+ }
98
+
99
+ if (this.#configuration.ssl) options.dialectOptions = {ssl: {require: true}};
100
+ return options;
101
+ }
102
+
103
+
104
+ get sqliteSequelizeOptions() {
105
+ return {
106
+ storage: this.#configuration.storage
107
+ };
108
+ }
109
+
110
+
111
+ get loggingEnabled() {
112
+ if (this.#options.hasOwnProperty('loggingEnabled')) {
113
+ return this.#options.loggingEnabled;
114
+ }
115
+ return this.#configuration.loggingEnabled;
116
+ }
117
+
118
+
119
+ get dialect() {
120
+ return this.#configuration.dialect;
121
+ }
122
+ }
123
+
124
+
125
+ module.exports = {
126
+ Connection,
127
+ DEFAULT_DATABASE_ID
128
+ }
@@ -0,0 +1,114 @@
1
+ const utils = require('../utils');
2
+
3
+
4
+ class ConnectionConfiguration {
5
+ #_config;
6
+ #databaseId;
7
+
8
+ constructor(databaseId) {
9
+ this.#databaseId = databaseId;
10
+ this.#validateConfiguration();
11
+ }
12
+
13
+
14
+ get databaseId() { return this.#databaseId };
15
+
16
+ get dialect() { return this.#config.dialect || 'postgres' };
17
+ get database() { return this.#config.database };
18
+ get host() { return this.#config.host };
19
+ get password() { return this.#config.password };
20
+ get port() { return this.#config.port || 5432 };
21
+ get username() { return this.#config.username };
22
+ get ssl() { return Boolean(this.#config.ssl) };
23
+ get storage() { return `${utils.configDirectory}/database.${this.env}.sqlite` };
24
+
25
+ get readers() { return this.#config.readers || [] };
26
+
27
+ get pool() { return {min: this.#poolMin, max: this.#poolMax}}
28
+ get #poolMin() { return this.#config.pool?.min || 0 }
29
+ get #poolMax() { return this.#config.pool?.max || 5 }
30
+
31
+ get loggingEnabled() { return this.env == 'development' };
32
+
33
+
34
+ #validateConfiguration() {
35
+ const config = this.#config;
36
+ this.#keysToValidate.forEach(key => {
37
+ if (!config[key]) throw new Error(`Missing ${key} for database configuration ${this.#configId}`);
38
+ });
39
+ }
40
+
41
+
42
+ get #keysToValidate() {
43
+ if (this.dialect == 'postgres') {
44
+ return ['database', 'host', 'password', 'username'];
45
+ }
46
+ return [];
47
+ }
48
+
49
+
50
+ get #configId() {
51
+ return `${this.#databaseId}.${this.env}`;
52
+ }
53
+
54
+
55
+ get #config() {
56
+ if (this.#_config) return this.#_config[this.env];
57
+
58
+ if (!this._allConfigs) throw new Error(`Missing database configuration`);
59
+
60
+ const config = this._allConfigs[this.#databaseId];
61
+ if (!config || !config[this.env]) throw new Error(`Missing database configuration for ${this.#configId}`);
62
+
63
+ this.#_config = config;
64
+ return this.#_config[this.env];
65
+ }
66
+
67
+
68
+ /***********************************************************************************************
69
+ * "ALL" CONFIGS
70
+ ***********************************************************************************************/
71
+ static get _allConfigs() {
72
+ return structuredClone(this._fetchAllConfigs());
73
+ }
74
+
75
+
76
+ static _fetchAllConfigs() {
77
+ try {
78
+ return require(this.configFilePath);
79
+ } catch(err) {
80
+ throw new Error(`Missing or invalid ${this.configFilePath}`);
81
+ }
82
+ }
83
+
84
+
85
+ static get configFilePath() {
86
+ return `${utils.configDirectory}/config.js`;
87
+ }
88
+
89
+
90
+ static get env() {
91
+ return process.env.NODE_ENV || 'development';
92
+ }
93
+
94
+
95
+ get _allConfigs() { return this.constructor._allConfigs; }
96
+ get configFilePath() { return this.constructor.configFilePath; }
97
+ get env() { return this.constructor.env; }
98
+
99
+
100
+ static configuredDatabaseIds() {
101
+ const databaseIds = [];
102
+
103
+ for (const [databaseId, config] of Object.entries(this._allConfigs)) {
104
+ if (config[this.env]) databaseIds.push(databaseId);
105
+ }
106
+
107
+ return databaseIds
108
+ }
109
+ }
110
+
111
+
112
+ module.exports = {
113
+ ConnectionConfiguration
114
+ };
@@ -0,0 +1,4 @@
1
+ // This is in a separate file so that it can be easily mocked during testing
2
+ module.exports = function(filePath, tableName) {
3
+ return structuredClone(require(filePath)[tableName]);
4
+ }