├── docs ├── .nojekyll ├── CNAME ├── logo.png ├── _sidebar.md ├── index.html ├── install.md ├── webpack.md ├── README.md └── context.md ├── test ├── base │ ├── exceptions-test.js │ ├── base-model-test.js │ ├── methods-test.js │ ├── data_types-test.js │ ├── scope-test.js │ ├── events-test.js │ └── convert-test.js ├── fixtures │ ├── models │ │ ├── camel_cased_model.js │ │ ├── other_name.js │ │ ├── _user │ │ │ ├── attribute.js │ │ │ └── model-functions.js │ │ └── user.js │ ├── migrations │ │ ├── 20140225054125_destroy_bar.js │ │ ├── 20140225054124_rename_foo_to_bar.js │ │ ├── 20140225142344_fix_posts.js │ │ ├── 20140225142454_remove_foo_from_posts.js │ │ ├── 20140425233709_create_view.js │ │ ├── 20140225054123_create_foo.js │ │ ├── 20140223120915_add_last_name_to_users.js │ │ ├── 20140425233701_seed.js │ │ ├── 20140223120815_create_users.js │ │ ├── 20181003115058_create_table_with_comments.js │ │ ├── 20181003144711_create_table_with_references.js │ │ ├── 20181003111622_create_compound_primary_key_table.js │ │ ├── 20181003151428_create_table_with_indices.js │ │ ├── 20181106180625_create_table_with_eums.js │ │ ├── 20140225142344_create_posts.js │ │ ├── 20190214220159_create_migration_helpers.js │ │ └── 20140225144123_create_attribute_tests.js │ ├── plugins │ │ └── test-plugin.js │ ├── classes │ │ └── Post.es6 │ └── stores │ │ ├── webpack-sqlite3.js │ │ ├── webpack-sqlite3-migrations.js │ │ └── webpack-postgres.js ├── mocha.opts ├── sql │ ├── postgres │ │ ├── fixtures │ │ │ └── migrations │ │ │ │ ├── 20170603143310_create_json_test.js │ │ │ │ ├── 20181106183654_create_enum_test.js │ │ │ │ └── 20141211203427_create_array_test.js │ │ ├── raw-test.js │ │ ├── __helper.js │ │ ├── uuid_key-test.js │ │ └── enum_type-test.js │ ├── __shared │ │ ├── empty-test.js │ │ ├── exec-test.js │ │ ├── plugins │ │ │ ├── serialize-test.js │ │ │ ├── paranoid-test.js │ │ │ └── total_count-test.js │ │ ├── data_types-test.js │ │ ├── autoload-test.js │ │ ├── dependent_nullify-test.js │ │ └── autojoin-test.js │ ├── mysql │ │ └── __helper.js │ ├── table_name-test.js │ ├── sqlite3 │ │ └── __helper.js │ ├── oracle │ │ └── clear.sql │ ├── aggregate_function-test.js │ └── interceptors-test.js ├── graphql │ ├── __schema_auto.js │ └── context-test.js ├── rest │ └── client │ │ ├── destroy-test.js │ │ ├── resource_name-test.js │ │ ├── includes-test.js │ │ ├── create-test.js │ │ ├── exec-test.js │ │ └── update-test.js ├── record-test.js ├── model-test.js ├── ldap │ └── client │ │ ├── __shared │ │ └── include-test.js │ │ ├── destroy-test.js │ │ └── exec-test.js └── definition-test.js ├── .prettierrc ├── lib ├── stores │ ├── mysql │ │ ├── index.js │ │ ├── autoload.js │ │ ├── connection.js │ │ └── attributes.js │ ├── sql │ │ ├── interceptors.js │ │ ├── migrations │ │ │ ├── seed.js │ │ │ ├── polymorph.js │ │ │ ├── run.js │ │ │ └── raw.js │ │ ├── exceptions.js │ │ ├── data_types │ │ │ ├── index.js │ │ │ ├── string.js │ │ │ ├── bigint.js │ │ │ ├── float.js │ │ │ ├── integer.js │ │ │ ├── boolean.js │ │ │ ├── datetime.js │ │ │ ├── binary.js │ │ │ ├── date.js │ │ │ └── time.js │ │ ├── limit.js │ │ ├── convert.js │ │ ├── raw.js │ │ ├── query.js │ │ ├── plugins │ │ │ ├── paranoid.js │ │ │ ├── total_count.js │ │ │ ├── serialize.js │ │ │ └── stampable.js │ │ ├── table_name.js │ │ ├── attributes.js │ │ ├── index.js │ │ ├── conditions.js │ │ ├── select.js │ │ ├── exec.js │ │ ├── transaction.js │ │ └── validations.js │ ├── sqlite3 │ │ ├── index.js │ │ ├── foreign_keys.js │ │ ├── connection.js │ │ └── autoload.js │ ├── postgres │ │ ├── index.js │ │ ├── data_types │ │ │ ├── date_array.js │ │ │ ├── time_array.js │ │ │ ├── float_array.js │ │ │ ├── string_array.js │ │ │ ├── boolean_array.js │ │ │ ├── datetime_array.js │ │ │ ├── point.js │ │ │ ├── circle.js │ │ │ ├── uuid.js │ │ │ ├── interval.js │ │ │ ├── box.js │ │ │ ├── enum.js │ │ │ ├── index.js │ │ │ ├── polygon.js │ │ │ ├── _geom.js │ │ │ ├── line.js │ │ │ ├── path.js │ │ │ ├── _array.js │ │ │ ├── integer_array.js │ │ │ ├── json.js │ │ │ └── tsvector.js │ │ ├── autoload.js │ │ └── connection.js │ ├── oracle │ │ ├── index.js │ │ ├── conditions.js │ │ ├── autoload.js │ │ ├── connection.js │ │ └── data_types │ │ │ └── date.js │ ├── rest │ │ ├── conditions.js │ │ ├── index.js │ │ ├── helper.js │ │ ├── limit.js │ │ ├── collection.js │ │ ├── connection.js │ │ ├── exec.js │ │ ├── resource.js │ │ ├── operators.js │ │ ├── urls.js │ │ └── destroy.js │ ├── activedirectory │ │ ├── index.js │ │ ├── schema │ │ │ ├── container.js │ │ │ ├── group.js │ │ │ └── organizational_unit.js │ │ └── data_types │ │ │ ├── date.js │ │ │ ├── group_type.js │ │ │ ├── sid.js │ │ │ ├── timestamp.js │ │ │ ├── guid.js │ │ │ └── user_account_control.js │ └── ldap │ │ ├── search_root.js │ │ ├── limit.js │ │ ├── dn.js │ │ ├── schema.js │ │ ├── conditions.js │ │ ├── data_types │ │ ├── object_class.js │ │ ├── dn.js │ │ └── dn_array.js │ │ ├── search_scope.js │ │ ├── index.js │ │ ├── relations.js │ │ ├── connection.js │ │ ├── select.js │ │ ├── controls │ │ └── tree_delete.js │ │ ├── object_class.js │ │ ├── errors.js │ │ ├── destroy.js │ │ ├── utils.js │ │ ├── relations │ │ ├── has_parent.js │ │ └── has_many.js │ │ └── attributes.js ├── graphql │ ├── index.js │ ├── relations.js │ ├── data_types.js │ └── type.js └── base │ ├── primary_keys.js │ ├── relations │ ├── has_one.js │ └── has.js │ ├── mixin.js │ ├── parent.js │ ├── cache.js │ ├── connection.js │ ├── method.js │ ├── methods.js │ ├── expect_result.js │ ├── dynamic_loading.js │ ├── index.js │ ├── context.js │ ├── save.js │ ├── operators.js │ └── every.js ├── .gitignore ├── .npmignore ├── store ├── base.js ├── ldap.js ├── rest.js ├── sql.js ├── mysql.js ├── oracle.js ├── sqlite3.js ├── postgres.js ├── index.js └── activedirectory.js ├── .eslintrc.js ├── LICENSE └── .travis.yml /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | openrecord.js.org -------------------------------------------------------------------------------- /test/base/exceptions-test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/models/camel_cased_model.js: -------------------------------------------------------------------------------- 1 | module.exports = function() {} 2 | -------------------------------------------------------------------------------- /test/fixtures/models/other_name.js: -------------------------------------------------------------------------------- 1 | module.exports = function RightName() {} 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilWaldmann/openrecord/HEAD/docs/logo.png -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive 3 | --full-trace 4 | --timeout 10000 5 | --exit 6 | -------------------------------------------------------------------------------- /test/fixtures/models/_user/attribute.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.attribute('login', String) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/models/_user/model-functions.js: -------------------------------------------------------------------------------- 1 | exports.model = { 2 | foobar() { 3 | return 'foo' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225054125_destroy_bar.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.removeTable('bar') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225054124_rename_foo_to_bar.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.renameTable('foo', 'bar') 3 | } 4 | -------------------------------------------------------------------------------- /lib/stores/mysql/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./attributes'), 3 | require('./autoload'), 4 | require('./connection') 5 | ] 6 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225142344_fix_posts.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.renameColumn('posts', 'messages', 'message') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225142454_remove_foo_from_posts.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.removeColumn('posts', 'foo') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140425233709_create_view.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.raw('CREATE VIEW tests AS SELECT * FROM users') 3 | } 4 | -------------------------------------------------------------------------------- /lib/stores/sql/interceptors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * STORE 3 | */ 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.addInterceptor('onJoin') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.mixin(require('./_user/attribute')) 3 | this.mixin(require('./_user/model-functions')) 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225054123_create_foo.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('foo', function() { 3 | this.text('foo') 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /lib/stores/sql/migrations/seed.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | seed: function(fn) { 3 | return this.run(function() { 4 | return this.use(fn) 5 | }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/stores/sql/migrations/polymorph.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | polymorph: function(name) { 3 | this.integer(name + '_id') 4 | this.string(name + '_type') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/stores/sqlite3/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./attributes'), 3 | require('./autoload'), 4 | require('./connection'), 5 | require('./foreign_keys') 6 | ] 7 | -------------------------------------------------------------------------------- /lib/graphql/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./data_types'), 3 | require('./helper'), 4 | require('./relations'), 5 | require('./resolve'), 6 | require('./type') 7 | ] 8 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140223120915_add_last_name_to_users.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.addColumn('users', function() { 3 | this.string('last_name') 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /lib/stores/postgres/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./attributes'), 3 | require('./autoload'), 4 | require('./connection'), 5 | require('./save') 6 | ].concat(require('./data_types')) 7 | -------------------------------------------------------------------------------- /lib/stores/oracle/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./attributes'), 3 | require('./autoload'), 4 | require('./conditions'), 5 | require('./connection'), 6 | require('./data_types/date') 7 | ] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.csv 3 | TODO.md 4 | 5 | documentation/wiki 6 | wiki 7 | logs 8 | node_modules 9 | 10 | coverage 11 | coverage.html 12 | .coveralls.yml 13 | 14 | test/**/*.sqlite3 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140425233701_seed.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.seed(function(store) { 3 | var User = store.Model('User') 4 | return User.create({ login: 'phil' }) 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140223120815_create_users.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('users', function() { 3 | this.string('login', { not_null: true }) 4 | this.string('first_name') 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /test/sql/postgres/fixtures/migrations/20170603143310_create_json_test.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('json_tests', function() { 3 | this.json('json_attr') 4 | this.jsonb('jsonb_attr') 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /lib/stores/sql/migrations/run.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | run: function(fn) { 3 | var self = this 4 | this.queue.push(function() { 5 | return fn.call(self.store) 6 | }) 7 | 8 | return this 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20181003115058_create_table_with_comments.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('with_comments', {comment: 'foobar table'}, function() { 3 | this.integer('foo', { comment: 'foobar' }) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20181003144711_create_table_with_references.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('with_references', function() { 3 | this.integer('post_id', { references: 'posts.id', unsigned: true }) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /test/sql/postgres/fixtures/migrations/20181106183654_create_enum_test.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.enum('my_enum', ['foo', 'bar']) 3 | this.createTable('enum_tests', function() { 4 | this.type('my_enum', 'enum_attribute') 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | test/ 3 | coverage/ 4 | 5 | npm-debug.log 6 | TODO.md 7 | 8 | coverage.html 9 | .coveralls.yml 10 | .npmignore 11 | .travis.yml 12 | .eslintrc.js 13 | test.js 14 | logo.png 15 | Makefile 16 | CODE_OF_CONDUCT.md 17 | CONTRIBUTING.md 18 | -------------------------------------------------------------------------------- /lib/stores/sql/migrations/raw.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | raw: function(sql, args) { 3 | var self = this 4 | 5 | this.queue.push(function() { 6 | return self.connection.raw(sql, args) 7 | }) 8 | 9 | return this 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/stores/rest/conditions.js: -------------------------------------------------------------------------------- 1 | exports.model = { 2 | find: function() { 3 | var self = this.callParent.apply( 4 | this, 5 | this.definition.store.utils.args(arguments) 6 | ) 7 | self.setInternal('action', 'show') 8 | return self 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/stores/sql/exceptions.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | const Store = require('../../store') 4 | 5 | Store.addExceptionType(function SQLError(error) { 6 | Error.apply(this) 7 | this.message = error 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20181003111622_create_compound_primary_key_table.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('compound_primary_keys', {id: false}, function() { 3 | this.integer('foo', { primary: true }) 4 | this.string('bar', { primary: true }) 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/date_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('date_array', this.toArrayCastTypes('date'), { 4 | array: true, 5 | migration: { 6 | dateArray: 'date[]' 7 | } 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/time_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('time_array', this.toArrayCastTypes('time'), { 4 | array: true, 5 | migration: { 6 | timeArray: 'time[]' 7 | } 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20181003151428_create_table_with_indices.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('with_indices', function() { 3 | this.integer('foo', { unique: true }) 4 | this.integer('bar') 5 | }) 6 | 7 | this.createIndex('with_indices', 'bar') 8 | } 9 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/float_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('float_array', this.toArrayCastTypes('float'), { 4 | array: true, 5 | migration: { 6 | floatArray: 'float[]' 7 | } 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/string_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('string_array', this.toArrayCastTypes('string'), { 4 | array: true, 5 | migration: { 6 | stringArray: 'varchar[]' 7 | } 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/boolean_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('boolean_array', this.toArrayCastTypes('boolean'), { 4 | array: true, 5 | migration: { 6 | booleanArray: 'boolean[]' 7 | } 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/datetime_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('datetime_array', this.toArrayCastTypes('datetime'), { 4 | array: true, 5 | migration: { 6 | datetimeArray: 'timestamp[]' 7 | } 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /store/base.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.base = require('../lib/base') 4 | 5 | module.exports = function(config){ 6 | config = config || {} 7 | config.type = 'base' 8 | return new Store(config) 9 | } 10 | 11 | module.exports.BaseModel = Store.BaseModel 12 | -------------------------------------------------------------------------------- /store/ldap.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.ldap = require('../lib/base').concat( 4 | require('../lib/stores/ldap') 5 | ) 6 | 7 | module.exports = function(config){ 8 | config.type = 'ldap' 9 | return new Store(config) 10 | } 11 | 12 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /store/rest.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.rest = require('../lib/base').concat( 4 | require('../lib/stores/rest') 5 | ) 6 | 7 | module.exports = function(config){ 8 | config.type = 'rest' 9 | return new Store(config) 10 | } 11 | 12 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /store/sql.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.sql = require('../lib/base').concat( 4 | require('../lib/stores/sql') 5 | ) 6 | 7 | module.exports = function(config){ 8 | config.type = 'sql' 9 | return new Store(config) 10 | } 11 | 12 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /test/fixtures/migrations/20181106180625_create_table_with_eums.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('with_enums', function() { 3 | this.enum('foo', { values: ['A', 'B', 'C'] }) 4 | this.enum('bar', { values: ['A1', 'B2', 'C3'] }) 5 | this.enum('foo2', { values: ['A', 'B', 'C'] }) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./_operators'), 3 | require('./binary'), 4 | require('./boolean'), 5 | require('./date'), 6 | require('./datetime'), 7 | require('./float'), 8 | require('./integer'), 9 | require('./bigint'), 10 | require('./string'), 11 | require('./time') 12 | ] 13 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225142344_create_posts.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('posts', function() { 3 | this.string('messages', { default: 'no message' }) 4 | this.string('foo') 5 | 6 | this.stampable() 7 | this.polymorph('foo') 8 | this.nestedSet() 9 | this.paranoid() 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /lib/stores/rest/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./collection'), 3 | require('./conditions'), 4 | require('./connection'), 5 | require('./destroy'), 6 | require('./exec'), 7 | require('./helper'), 8 | require('./limit'), 9 | require('./operators'), 10 | require('./resource'), 11 | require('./save'), 12 | require('./urls') 13 | ] 14 | -------------------------------------------------------------------------------- /store/mysql.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.mysql = require('../lib/base').concat( 4 | require('../lib/stores/sql'), 5 | require('../lib/stores/mysql') 6 | ) 7 | 8 | module.exports = function(config){ 9 | config.type = 'mysql' 10 | return new Store(config) 11 | } 12 | 13 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /store/oracle.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.oracle = require('../lib/base').concat( 4 | require('../lib/stores/sql'), 5 | require('../lib/stores/oracle') 6 | ) 7 | 8 | module.exports = function(config){ 9 | config.type = 'oracle' 10 | return new Store(config) 11 | } 12 | 13 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /store/sqlite3.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.sqlite3 = require('../lib/base').concat( 4 | require('../lib/stores/sql'), 5 | require('../lib/stores/sqlite3') 6 | ) 7 | 8 | module.exports = function(config){ 9 | config.type = 'sqlite3' 10 | return new Store(config) 11 | } 12 | 13 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /store/postgres.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.postgres = require('../lib/base').concat( 4 | require('../lib/stores/sql'), 5 | require('../lib/stores/postgres') 6 | ) 7 | 8 | module.exports = function(config){ 9 | config.type = 'postgres' 10 | return new Store(config) 11 | } 12 | 13 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /test/fixtures/plugins/test-plugin.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | myStoreFunction: function() {}, 3 | 4 | Model: function(a, b, callback) { 5 | if (typeof b === 'function') { 6 | callback = b 7 | b = '' 8 | } 9 | return this.callParent(a + b, callback) 10 | } 11 | } 12 | 13 | exports.definition = { 14 | myDefinitionFunction: function() {} 15 | } 16 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | require('./base') 4 | require('./sql') 5 | require('./postgres') 6 | require('./sqlite3') 7 | require('./mysql') 8 | require('./oracle') 9 | require('./rest') 10 | require('./ldap') 11 | require('./activedirectory') 12 | 13 | // reset type to get it via constructor config object 14 | Store.type = null 15 | 16 | module.exports = Store 17 | -------------------------------------------------------------------------------- /lib/stores/rest/helper.js: -------------------------------------------------------------------------------- 1 | exports.utils = { 2 | applyParams: function(options, primaryKeys) { 3 | var params = options.params 4 | 5 | options.url = options.url.replace(/:(\w+)/, function(match, param) { 6 | if (params[param]) { 7 | var tmp = params[param] 8 | delete params[param] 9 | return tmp 10 | } 11 | return match 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /store/activedirectory.js: -------------------------------------------------------------------------------- 1 | const Store = require('../lib/store') 2 | 3 | Store.registeredTypes.activedirectory = require('../lib/base').concat( 4 | require('../lib/stores/ldap'), 5 | require('../lib/stores/activedirectory') 6 | ) 7 | 8 | module.exports = function(config){ 9 | config.type = 'activedirectory' 10 | return new Store(config) 11 | } 12 | 13 | module.exports.BaseModel = Store.BaseModel -------------------------------------------------------------------------------- /lib/base/primary_keys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | var self = this 7 | 8 | this.primaryKeys = [] 9 | 10 | this.use(function() { 11 | for (var name in self.attributes) { 12 | if (self.attributes[name].primary) { 13 | self.primaryKeys.push(name) 14 | } 15 | } 16 | }, 0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/classes/Post.es6: -------------------------------------------------------------------------------- 1 | const Store = require('../../../store') 2 | 3 | class BasePost extends Store.BaseModel { 4 | static definition() { 5 | this.validatesPresenceOf('message') 6 | } 7 | } 8 | 9 | class Post extends BasePost { 10 | static definition() { 11 | this.belongsTo('user') 12 | this.belongsTo('thread') 13 | super.definition() 14 | } 15 | } 16 | 17 | module.exports = Post 18 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **Getting started** 2 | * [Install](install.md) 3 | * [Setup](setup.md) 4 | * **Usage** 5 | * [Model Definition](definition.md) 6 | * [Query](query.md) 7 | * [Modify](modify.md) 8 | * [Context](context.md) 9 | 10 | * **[LDAP](ldap.md)** 11 | * 12 | * **[GraphQL](graphql.md)** 13 | 14 | * **[Webpack](webpack.md)** 15 | 16 | * **[Plugins](plugins.md)** 17 | 18 | * **[Migrations](migrations.md)** 19 | -------------------------------------------------------------------------------- /test/sql/postgres/fixtures/migrations/20141211203427_create_array_test.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('array_tests', function() { 3 | this.integerArray('int_arr') 4 | this.floatArray('float_arr') 5 | this.booleanArray('bool_arr') 6 | this.dateArray('date_arr') 7 | this.datetimeArray('datetime_arr') 8 | this.timeArray('time_arr') 9 | this.stringArray('str_arr') 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/point.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'point', 5 | { 6 | read: function(value) { 7 | return value 8 | }, 9 | 10 | write: function(value) { 11 | if(typeof value === 'object') return '(' + value.x +',' + value.y + ')' 12 | return value 13 | } 14 | } 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/circle.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'circle', 5 | { 6 | read: function(value) { 7 | return value 8 | }, 9 | 10 | write: function(value) { 11 | if(typeof value === 'object') return '<(' + value.x +',' + value.y + '),' + value.radius + '>' 12 | return value 13 | } 14 | } 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/stores/sqlite3/foreign_keys.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.use(function() { 4 | if (this.config.enableForeignKeys !== false) return this.enableForeignKeys() 5 | }, 100) 6 | }, 7 | 8 | enableForeignKeys: function(){ 9 | return this.connection.raw('PRAGMA foreign_keys = ON') 10 | .catch(function(error){ 11 | console.error('Error while enabling SQLite3 foreign_keys: ', error) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/stores/oracle/conditions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | this.onRawCondition(function(chain, condition) { 7 | var attrsRegExp = new RegExp( 8 | '(' + Object.keys(chain.definition.attributes).join('|') + ')', 9 | 'g' 10 | ) 11 | 12 | // add quotes to attribute names 13 | condition.query = condition.query.replace(attrsRegExp, '"$1"') 14 | }, 10) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/sql/__shared/empty-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': Empty', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store(storeConf) 14 | 15 | // Models here 16 | }) 17 | 18 | // Tests here 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/uuid.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'uuid', 5 | function(value) { 6 | if (value === null) return null 7 | if (value === undefined) return null 8 | return value.toString() 9 | }, 10 | { 11 | migration: ['uuid'], 12 | operators: { 13 | defaults: ['eq', 'not', 'like', 'ilike'] 14 | } 15 | } 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/stores/rest/limit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | this.beforeFind(function(options) { 7 | var limit = this.getInternal('limit') 8 | var offset = this.getInternal('offset') 9 | 10 | if (limit) { 11 | options.params.limit = limit 12 | } 13 | 14 | if (offset) { 15 | options.params.limit = offset 16 | } 17 | 18 | return true 19 | }, -40) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/string.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'string', 5 | function(value) { 6 | if (value === null) return null 7 | if (value === undefined) return null 8 | return value.toString() 9 | }, 10 | { 11 | migration: ['string', 'text'], 12 | operators: { 13 | defaults: ['eq', 'not', 'like', 'ilike'] 14 | } 15 | } 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/stores/sql/limit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | this.beforeFind(function(query) { 7 | var limit = this.getInternal('limit') 8 | var offset = this.getInternal('offset') 9 | 10 | if (typeof limit === 'number') { 11 | query.limit(limit) 12 | } 13 | 14 | if (offset && offset > 0) { 15 | query.offset(offset) 16 | } 17 | 18 | return true 19 | }, -40) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./errors'), 3 | require('./data_types/date'), 4 | require('./data_types/group_type'), 5 | require('./data_types/guid'), 6 | require('./data_types/sid'), 7 | require('./data_types/timestamp'), 8 | require('./data_types/user_account_control'), 9 | require('./schema/computer'), 10 | require('./schema/container'), 11 | require('./schema/group'), 12 | require('./schema/organizational_unit'), 13 | require('./schema/user') 14 | ] 15 | -------------------------------------------------------------------------------- /test/graphql/__schema_auto.js: -------------------------------------------------------------------------------- 1 | const graphQLTools = require('graphql-tools') 2 | const graphql = require('graphql') 3 | 4 | module.exports = function(store1, store2) { 5 | const schema = graphQLTools.makeExecutableSchema({ 6 | typeDefs: [store1.toGraphQLTypeDefs(), store2.toGraphQLTypeDefs()], 7 | resolvers: store1.toGraphQLResolvers() 8 | }) 9 | 10 | return function query(query, context, variables, operation) { 11 | return graphql.graphql(schema, query, null, context, variables, operation) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/stores/sqlite3/connection.js: -------------------------------------------------------------------------------- 1 | const Knex = require('knex') 2 | 3 | /* 4 | * STORE 5 | */ 6 | exports.store = { 7 | connect: function() { 8 | this.connection = Knex({ 9 | client: 'sqlite3', 10 | connection: this.config.connection || { 11 | filename: this.config.file 12 | }, 13 | useNullAsDefault: true 14 | }) 15 | }, 16 | 17 | close: function(callback) { 18 | if (!this.connection) return callback ? callback() : null 19 | this.connection.client.destroy(callback) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/bigint.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'bigint', 7 | function(value) { 8 | if (value === null) return null 9 | if (value === '') return null 10 | return validator.toInt(value + '') 11 | }, 12 | { 13 | migration: 'bigint', 14 | operators: { 15 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 16 | } 17 | } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/float.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'float', 7 | function(value) { 8 | if (value === null) return null 9 | if (value === '') return null 10 | return validator.toFloat(value + '') 11 | }, 12 | { 13 | migration: 'float', 14 | operators: { 15 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 16 | } 17 | } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/interval.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | // TODO: V2.1. probably use https://github.com/bendrucker/postgres-interval 3 | mixinCallback: function() { 4 | this.addType( 5 | 'interval', 6 | function(value) { 7 | if (value === null) return null 8 | if (value === undefined) return null 9 | return value.toString() 10 | }, 11 | { 12 | migration: ['interval'], 13 | operators: { 14 | defaults: ['eq', 'not'] 15 | } 16 | } 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/stores/rest/collection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | this.afterFind(function(data) { 7 | var records = data.result || [] 8 | 9 | if (!Array.isArray(records)) { 10 | records = records[this.rootParam || 'data'] || [] 11 | } 12 | 13 | if (records && !Array.isArray(records)) { 14 | records = [records] // Every result is an array... 15 | } 16 | 17 | data.result = records 18 | 19 | return true 20 | }, 100) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/integer.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'integer', 7 | function(value) { 8 | if (value === null) return null 9 | if (value === '') return null 10 | return validator.toInt(value + '') 11 | }, 12 | { 13 | migration: 'integer', 14 | operators: { 15 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 16 | } 17 | } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/box.js: -------------------------------------------------------------------------------- 1 | const geom = require('./_geom') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'box', 7 | { 8 | read: function(value) { 9 | return geom.convertPoints(value) 10 | }, 11 | 12 | write: function(value) { 13 | if(Array.isArray(value)) return geom.pointsToString(value) 14 | return value 15 | } 16 | }, 17 | { 18 | array: true, 19 | migration: ['box'] 20 | } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/stores/webpack-sqlite3.js: -------------------------------------------------------------------------------- 1 | var Store 2 | try { 3 | Store = require('openrecord/store/sqlite3') // to simulate tests from the outside world 4 | require('openrecord/lib/base/dynamic_loading') 5 | } catch (e) { 6 | Store = require('../../../store/sqlite3') 7 | } 8 | 9 | module.exports = function(database, autoAttributes) { 10 | const store = new Store({ 11 | file: database, 12 | autoAttributes: autoAttributes 13 | }) 14 | 15 | store.Model('user', function() {}) 16 | store.Model('post', function() {}) 17 | 18 | return store 19 | } 20 | -------------------------------------------------------------------------------- /lib/stores/ldap/search_root.js: -------------------------------------------------------------------------------- 1 | exports.model = { 2 | searchRoot: function(root, recursive) { 3 | var self = this.chain() 4 | self.setInternal('search_root', root) 5 | 6 | self.recursive(recursive === true) 7 | 8 | return self 9 | } 10 | } 11 | 12 | exports.definition = { 13 | mixinCallback: function() { 14 | var self = this 15 | 16 | this.beforeFind(function(options) { 17 | options.root = self.store.utils.normalizeDn( 18 | this.getInternal('search_root') || self.store.config.base 19 | ) 20 | }, 80) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/stores/sql/convert.js: -------------------------------------------------------------------------------- 1 | exports.chain = { 2 | convertConditionAttribute: function(attribute, parentRelations) { 3 | parentRelations = parentRelations || 4 | this.getInternal('parent_relations') || [this.definition.tableName] 5 | return this.definition.store.utils.toAttributeName( 6 | attribute, 7 | parentRelations 8 | ) 9 | }, 10 | 11 | escapeAttribute: function(attribute) { 12 | const query = this.query() 13 | return query.client.formatter(query).wrapString(attribute) // escape attribute. from `user.id` to `"user"."id"` 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/stores/webpack-sqlite3-migrations.js: -------------------------------------------------------------------------------- 1 | var Store 2 | try { 3 | Store = require('openrecord/store/sqlite3') // to simulate tests from the outside world 4 | } catch (e) { 5 | Store = require('../../../store/sqlite3') 6 | } 7 | 8 | module.exports = function(database, autoAttributes) { 9 | const store = new Store({ 10 | file: database, 11 | autoAttributes: autoAttributes, 12 | migrations: [ 13 | require('../migrations/20140223120815_create_users') 14 | ] 15 | }) 16 | 17 | store.Model('User', function() {}) 18 | 19 | return store 20 | } 21 | -------------------------------------------------------------------------------- /lib/stores/ldap/limit.js: -------------------------------------------------------------------------------- 1 | const ldap = require('ldapjs') 2 | 3 | /* 4 | * DEFINITION 5 | */ 6 | exports.definition = { 7 | mixinCallback: function() { 8 | this.beforeFind(function(options) { 9 | var limit = this.getInternal('limit') 10 | // var offset = this.getInternal('offset') // not possible in ldap ?! 11 | 12 | if (typeof limit !== 'number') limit = 100 13 | options.controls = options.controls || [] 14 | options.controls.push( 15 | new ldap.PagedResultsControl({ value: { size: limit } }) 16 | ) 17 | }, -40) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/enum.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | enum: function(name, values) { 3 | if (Array.isArray(values)) { 4 | var enumValues = values 5 | .map(function(v) { 6 | return `'${v}'` 7 | }) 8 | .join(', ') 9 | this.raw(`CREATE TYPE ${name} AS ENUM (${enumValues})`) 10 | } else { 11 | var options = values || {} 12 | options.custom = function(table) { 13 | return table.enum(name, options.values, options) 14 | } 15 | this._addColumnTypeFn('enum').apply(this, arguments) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20190214220159_create_migration_helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('migration_helpers', function() { 3 | this.string('string_attr', 42) // includes string len 4 | this.string('string_attr_def', 69, { default: 'hrm' }) // adds default value 5 | this.string('req_string_attr!', 13, { default: 'nice' }) // adds not null 6 | this.id('foreign_id') // id is an integer 7 | this.id('req_foreign_id!') // adds not null 8 | this.index('string_attr') // adds index 9 | this.unique('req_string_attr') // adds unique 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /lib/base/relations/has_one.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | hasOne: function(name, options) { 3 | options = options || {} 4 | 5 | options.initialize = function() { 6 | this.callParent() 7 | } 8 | 9 | // hasMany relation only returns a single record! 10 | options.transform = 11 | options.transform || 12 | function(result) { 13 | if (!result) return 14 | return result[0] || null 15 | } 16 | 17 | const result = this.hasMany(name, options) 18 | 19 | options.type = 'has_one' // overwrite type! 20 | 21 | return result 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/stores/sql/raw.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.model = { 5 | /** 6 | * execute raw sql 7 | * @class Model 8 | * @method raw 9 | * @param {string} sql - The raw sql query. 10 | * @param {array} attrs - Query attributes. 11 | * @param {function} callback - The callback. 12 | * 13 | * @see Model.exec 14 | * 15 | * @return {Model} 16 | */ 17 | raw: function(sql, attrs, callback) { 18 | var promise = this.definition.store.connection.raw(sql, attrs) 19 | 20 | if (typeof callback === 'function') { 21 | promise.then(callback) 22 | } 23 | 24 | return promise 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/base/mixin.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | /** 3 | * mixin a module to extend your model definition. 4 | * The module needs to export either a function, which will be called with the definition scope 5 | * or an objects which will be mixed into the defintion object. 6 | * 7 | * @class Definition 8 | * @method mixin 9 | * @param {(object|function)} module - the module 10 | * 11 | * @return {Definition} 12 | */ 13 | mixin: function(module) { 14 | if (typeof module === 'function') { 15 | module.call(this) 16 | } else { 17 | this.include(module) 18 | } 19 | return this 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stores/ldap/dn.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | mixinCallback: function() { 3 | this.rdn_prefix = 'cn' 4 | }, 5 | 6 | /** 7 | * Set the rdn prefix 8 | * 9 | * @public 10 | * @memberof LDAP.Definition 11 | * @param {string} prefix - The prefix 12 | * 13 | * @return {Definition} 14 | */ 15 | rdnPrefix: function(prefix) { 16 | this.rdn_prefix = prefix 17 | return this 18 | }, 19 | 20 | dn: function(record) { 21 | if (!record[this.rdn_prefix] && record.dn) return record.dn 22 | return ( 23 | this.rdn_prefix + '=' + record[this.rdn_prefix] + ',' + record.parent_dn 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/base/base-model-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../store/base') 2 | 3 | describe('Attributes', function() { 4 | var store = new Store() 5 | 6 | store.Model('User', function() { 7 | this.attribute('my_str', String, {}) 8 | }) 9 | 10 | before(function() { 11 | return store.ready() 12 | }) 13 | 14 | describe('store instance', function() { 15 | it('has BaseModel', function() { 16 | store.BaseModel.should.be.a.Function() 17 | }) 18 | }) 19 | 20 | describe('Store constructor', function() { 21 | it('has BaseModel', function() { 22 | Store.BaseModel.should.be.a.Function() 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/fixtures/stores/webpack-postgres.js: -------------------------------------------------------------------------------- 1 | var Store 2 | try { 3 | Store = require('openrecord/store/postgres') // to simulate tests from the outside world 4 | } catch (e) { 5 | Store = require('../../../store/postgres') 6 | } 7 | 8 | module.exports = function(database, autoAttributes) { 9 | const store = new Store({ 10 | host: 'localhost', 11 | type: 'postgres', 12 | database: database, 13 | user: 'postgres', 14 | password: '', 15 | autoAttributes: autoAttributes, 16 | autoConnect: false 17 | }) 18 | 19 | store.Model('user', function() {}) 20 | store.Model('post', function() {}) 21 | 22 | return store 23 | } 24 | -------------------------------------------------------------------------------- /lib/stores/oracle/autoload.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.use(function() { 4 | if (this.config.autoLoad) return this.loadAllTables() 5 | }, 100) 6 | }, 7 | 8 | loadAllTables: function() { 9 | const self = this 10 | const sql = 'SELECT table_name FROM user_tables' 11 | 12 | return this.connection.raw(sql).then(function(tableNames) { 13 | tableNames.forEach(function(row) { 14 | var tableName = row.TABLE_NAME 15 | self.Model(self.utils.getModelName(tableName), function() { 16 | // Empty model definition! 17 | }) 18 | }) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stores/sqlite3/autoload.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.use(function() { 4 | if (this.config.autoLoad) return this.loadAllTables() 5 | }, 100) 6 | }, 7 | 8 | loadAllTables: function() { 9 | const self = this 10 | const sql = "SELECT name FROM sqlite_master WHERE type='table'" 11 | 12 | return this.connection.raw(sql).then(function(tableNames) { 13 | tableNames.forEach(function(row) { 14 | var tableName = row.name 15 | self.Model(self.utils.getModelName(tableName), function() { 16 | // Empty model definition! 17 | }) 18 | }) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./_array'), 3 | require('./boolean_array'), 4 | require('./composite'), 5 | require('./date_array'), 6 | require('./datetime_array'), 7 | require('./float_array'), 8 | require('./hstore'), 9 | require('./integer_array'), 10 | require('./json'), 11 | require('./string_array'), 12 | require('./time_array'), 13 | require('./uuid'), 14 | require('./interval'), 15 | require('./tsvector'), 16 | require('./point'), 17 | require('./line'), 18 | require('./path'), 19 | require('./box'), 20 | require('./polygon'), 21 | require('./circle'), 22 | require('./enum') 23 | ] 24 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/boolean.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'boolean', 5 | function(value) { 6 | if (value === null) return null 7 | if (value === 'f') return false 8 | if (value === '0') return false 9 | if (value === 'false') return false 10 | if (value === '') return false 11 | if (value === 0) return false 12 | if (value === false) return false 13 | return true 14 | }, 15 | { 16 | migration: 'boolean', 17 | operators: { 18 | defaults: ['eq', 'not'] 19 | } 20 | } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/stores/ldap/schema.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | mixinCallback: function() { 3 | this.__isContainer = false 4 | }, 5 | 6 | isContainer: function(rdnPrefix) { 7 | if (rdnPrefix) this.rdnPrefix(rdnPrefix) 8 | 9 | this.hasChildren('children', { 10 | from: 'dn', 11 | to: 'parent_dn', 12 | autoSave: true 13 | }) 14 | 15 | this.hasChildren('all_children', { 16 | from: 'dn', 17 | to: 'parent_dn', 18 | recursive: true, 19 | autoSave: true 20 | }) 21 | 22 | this.hasParent('parent', { model: this.modelName, autoSave: true }) 23 | 24 | this.__isContainer = true 25 | 26 | return this 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/stores/ldap/conditions.js: -------------------------------------------------------------------------------- 1 | const parseDN = require('ldapjs').parseDN 2 | 3 | exports.model = { 4 | find: function(dn) { 5 | var self = this.chain() 6 | 7 | if (typeof dn === 'string') { 8 | try { 9 | parseDN(dn) // will throw an error if no valid dn given. Unfortunately there is no ldapjs function to check if a dn is valid... 10 | self.searchRoot(dn) 11 | self.searchScope('base') 12 | return self 13 | } catch (e) { 14 | // continue with normal find... 15 | } 16 | } 17 | 18 | return self.callParent.apply( 19 | self, 20 | this.definition.store.utils.args(arguments) 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/stores/rest/connection.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | /* 4 | * STORE 5 | */ 6 | exports.store = { 7 | mixinCallback: function() { 8 | this.connection = axios.create( 9 | Object.assign( 10 | { 11 | baseURL: this.config.baseUrl || this.config.url 12 | }, 13 | this.config 14 | ) 15 | ) 16 | }, 17 | 18 | close: function(callback) { 19 | callback() 20 | } 21 | } 22 | 23 | /* 24 | * CHAIN 25 | */ 26 | exports.model = { 27 | mixinCallback: function() { 28 | var self = this 29 | this.__defineGetter__('connection', function() { 30 | return self.definition.store.connection 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/datetime.js: -------------------------------------------------------------------------------- 1 | const parse = require('date-fns/parse') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'datetime', 7 | { 8 | read: function(value) { 9 | return value 10 | }, 11 | write: function(value) { 12 | return value 13 | }, 14 | output: function(value) { 15 | if (value === null) return null 16 | return parse(value) 17 | } 18 | }, 19 | { 20 | migration: 'datetime', 21 | operators: { 22 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 23 | } 24 | } 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/stores/sql/query.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | query: function(options) { 6 | if (!options) options = {} 7 | var connection = options.transaction || this.store.connection 8 | 9 | return connection(this.tableName) 10 | } 11 | } 12 | 13 | exports.chain = { 14 | query: function(options) { 15 | if (!options) options = {} 16 | 17 | var connection = options.transaction || this.definition.store.connection 18 | 19 | var query = this.getInternal('query') 20 | if (!query || options.force) { 21 | query = connection(this.definition.tableName) 22 | this.setInternal('query', query) 23 | } 24 | 25 | return query 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/polygon.js: -------------------------------------------------------------------------------- 1 | const geom = require('./_geom') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'polygon', 7 | { 8 | read: function(value) { 9 | if(Array.isArray(value)) return value 10 | if(typeof value === 'string') return geom.convertPoints(value.replace(/(^\(|\)$)/g, '')) 11 | return null 12 | }, 13 | 14 | write: function(value) { 15 | if(Array.isArray(value)) return '(' + geom.pointsToString(value) + ')' 16 | return value 17 | } 18 | }, 19 | { 20 | array: true, 21 | migration: ['polygon'] 22 | } 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/stores/rest/exec.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('openrecord:exec') 2 | 3 | exports.definition = { 4 | mixinCallback: function() { 5 | var self = this 6 | 7 | this.onFind(function(options, data) { 8 | self.store.utils.applyParams(options) 9 | return this.connection.request(options).then(function(result) { 10 | debug(options.method + ' ' + options.url) 11 | data.result = result.data 12 | }) 13 | }) 14 | } 15 | } 16 | 17 | /* 18 | * MODEL 19 | */ 20 | exports.model = { 21 | getExecOptions: function() { 22 | var action = this.getInternal('action') || 'index' 23 | return this.definition.store.utils.clone(this.definition.actions[action]) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/migrations/20140225144123_create_attribute_tests.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.createTable('attribute_tests', function() { 3 | var self = this 4 | 5 | this.run(function() { 6 | self.string('string_attr') 7 | }) 8 | 9 | this.text('text_attr') 10 | this.integer('integer_attr') 11 | this.float('float_attr') 12 | this.boolean('boolean_attr') 13 | this.binary('binary_attr') 14 | this.date('date_attr') 15 | this.datetime('datetime_attr') 16 | this.time('time_attr') 17 | 18 | this.string('with_default_text', { default: 'foo' }) 19 | this.integer('with_default_integer', { default: 55 }) 20 | this.boolean('with_default_boolean', { default: true }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /lib/stores/ldap/data_types/object_class.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'object_class', 5 | { 6 | read: function(value) { 7 | /* istanbul ignore if */ 8 | if (value === null) return null 9 | if (!Array.isArray(value)) value = value.split(',') 10 | 11 | return value 12 | }, 13 | write: function(value) { 14 | /* istanbul ignore if */ 15 | if (value === null) return null 16 | return value 17 | } 18 | }, 19 | { 20 | array: true, 21 | operators: { 22 | default: 'eq', 23 | defaults: ['eq', 'not'] 24 | } 25 | } 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/stores/mysql/autoload.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.use(function() { 4 | if (this.config.autoLoad) return this.loadAllTables() 5 | }, 100) 6 | }, 7 | 8 | loadAllTables: function() { 9 | const self = this 10 | const sql = 11 | "select TABLE_NAME from information_schema.tables WHERE TABLE_SCHEMA='" + 12 | this.config.database + 13 | "'" 14 | 15 | return this.connection.raw(sql).then(function(tableNames) { 16 | tableNames[0].forEach(function(row) { 17 | var tableName = row.TABLE_NAME 18 | self.Model(self.utils.getModelName(tableName), function() { 19 | // Empty model definition! 20 | }) 21 | }) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/binary.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType( 4 | 'binary', 5 | { 6 | read: function(value) { 7 | if (value === null) return null 8 | 9 | if (Buffer.from) return Buffer.from(value, 'binary') 10 | return new Buffer(value, 'binary') // eslint-disable-line node/no-deprecated-api 11 | }, 12 | write: function(buffer) { 13 | if (buffer === null) return null 14 | return buffer.toString('binary') 15 | } 16 | }, 17 | { 18 | migration: 'binary', 19 | extend: Buffer, 20 | operators: { 21 | defaults: ['eq', 'not'] 22 | } 23 | } 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/stores/ldap/search_scope.js: -------------------------------------------------------------------------------- 1 | exports.model = { 2 | searchScope: function(scope) { 3 | var self = this.chain() 4 | 5 | self.setInternal('search_scope', scope) 6 | 7 | if (scope === 'base') { 8 | self.singleResult(false) 9 | } else { 10 | self.clearInternal('single_result') 11 | } 12 | 13 | return self 14 | }, 15 | 16 | recursive: function(recursiv) { 17 | const self = this.limit(null) 18 | if (recursiv === false) return self.searchScope('one') 19 | return self.searchScope('sub') 20 | } 21 | } 22 | 23 | exports.definition = { 24 | mixinCallback: function() { 25 | this.beforeFind(function(options) { 26 | options.scope = this.getInternal('search_scope') || 'sub' 27 | }, 90) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/base/parent.js: -------------------------------------------------------------------------------- 1 | exports.store = exports.definition = exports.model = exports.record = exports.utils = { 2 | callParent: function callParent() { 3 | var Utils = this // utils itself 4 | if (this.utils) Utils = this.utils // store 5 | if (this.store) Utils = this.store.utils // definition 6 | if (this.definition) Utils = this.definition.store.utils // record, model 7 | 8 | var parentFn = callParent.caller._parent 9 | if (typeof parentFn === 'function' && callParent.caller !== parentFn) { 10 | return parentFn.apply(this, Utils.args(arguments)) 11 | } 12 | var lastArg = arguments[arguments.length - 1] 13 | if (lastArg && lastArg._parent) { 14 | return lastArg._parent.apply(this, Utils.args(arguments)) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/stores/sql/plugins/paranoid.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | paranoid: function() { 3 | this.datetime('deleted_at') 4 | this.integer('deleter_id') 5 | } 6 | } 7 | 8 | exports.definition = { 9 | paranoid: function() { 10 | var self = this 11 | 12 | this.scope('withDeleted', function() { 13 | this.setInternal('withDeleted', true) 14 | }) 15 | 16 | this.beforeFind(function() { 17 | var withDeleted = this.getInternal('withDeleted') || false 18 | 19 | if (!withDeleted && self.attributes.deleted_at) { 20 | this.where({ deleted_at: null }) 21 | } 22 | }) 23 | 24 | this.destroy = function(options) { 25 | this.deleted_at = new Date() 26 | return this.save(options) 27 | } 28 | 29 | return this 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/sql/__shared/exec-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': Exec', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store(storeConf) 14 | 15 | store.Model('User', function() {}) 16 | }) 17 | 18 | it('throws an error on unknown table', function() { 19 | return store 20 | .ready(function() { 21 | var User = store.Model('User') 22 | return User.where({ login_like: 'phi' }).exec() 23 | }) 24 | .should.be.rejectedWith(store.Error) // TODO: custom error!! 25 | }) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /test/base/methods-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../../store/base') 4 | 5 | describe('Methods', function() { 6 | var store = new Store() 7 | 8 | store.Model('User', function() { 9 | this.my_method = function() { 10 | this.should.have.property('attributes') 11 | this.should.have.property('errors') 12 | } 13 | }) 14 | 15 | var User, phil 16 | 17 | before(function() { 18 | return store.ready(function() { 19 | User = store.Model('User') 20 | phil = new User() 21 | }) 22 | }) 23 | 24 | it('is defined', function() { 25 | should.exist(phil.my_method) 26 | phil.my_method.should.be.a.Function() 27 | }) 28 | 29 | it('has the right context', function() { 30 | phil.my_method() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | extends: [ 6 | 'digitalbits', 7 | 'prettier' 8 | ], 9 | 10 | env: { 11 | node: true, 12 | mocha: true 13 | }, 14 | 15 | globals: { 16 | testMYSQL: true, 17 | afterMYSQL: true, 18 | beforeMYSQL: true, 19 | beforePG: true, 20 | afterPG: true, 21 | testPG: true, 22 | beforeSQLite: true, 23 | afterSQLite: true, 24 | testSQLite: true, 25 | testOracle: true, 26 | afterOracle: true, 27 | beforeOracle: true, 28 | getOracleConfig: true, 29 | beforeActiveDirectory: true, 30 | afterActiveDirectory: true, 31 | testActiveDirectory: true, 32 | beforeGraphQL: true, 33 | afterGraphQL: true, 34 | LDAP_BASE: true 35 | } 36 | } -------------------------------------------------------------------------------- /lib/stores/postgres/autoload.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.use(function() { 4 | if (this.config.autoLoad) return this.loadAllTables() 5 | }, 100) 6 | }, 7 | 8 | loadAllTables: function() { 9 | const self = this 10 | var schema = 'public' 11 | if (typeof this.config.autoLoad === 'string') schema = this.config.autoLoad 12 | 13 | const sql = 14 | "select table_name from information_schema.tables WHERE table_schema ='" + 15 | schema + 16 | "';" 17 | 18 | return this.connection.raw(sql).then(function(tableNames) { 19 | tableNames.rows.forEach(function(row) { 20 | self.Model(self.utils.getModelName(row.table_name), function() { 21 | // Empty model definition! 22 | }) 23 | }) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/stores/rest/resource.js: -------------------------------------------------------------------------------- 1 | const inflection = require('inflection') 2 | 3 | /* 4 | * DEFINITION 5 | */ 6 | exports.definition = { 7 | mixinCallback: function() { 8 | this.resource = inflection.underscore(inflection.pluralize(this.modelName)) 9 | 10 | if ( 11 | this.store.config.inflection && 12 | this.store.config.inflection[this.resource] 13 | ) { 14 | this.resource = this.store.config.inflection[this.resource] 15 | } 16 | }, 17 | 18 | getName: function() { 19 | return this.resource 20 | } 21 | } 22 | 23 | /* 24 | * STORE 25 | */ 26 | exports.store = { 27 | getByResource: function(resource) { 28 | for (var i in this.models) { 29 | if (this.models[i].definition.resource === resource) { 30 | return this.models[i] 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/stores/sql/plugins/total_count.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | mixinCallback: function() { 3 | var self = this 4 | 5 | this.scope( 6 | 'totalCount', 7 | function(field) { 8 | const through = this.getInternal('through') 9 | let tableName = self.getName() 10 | 11 | if (through) { 12 | tableName = through[through.length - 1] 13 | } 14 | 15 | if (typeof field !== 'string') field = null 16 | var key = field || self.primaryKeys[0] || '*' 17 | 18 | this.count( 19 | this.escapeAttribute( 20 | self.store.utils.toAttributeName(key, [tableName]) 21 | ), 22 | true 23 | ) 24 | .limit() 25 | .offset() 26 | .order(null) 27 | }, 28 | true 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/rest/client/destroy-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../lib/store') 2 | 3 | describe('REST Client: Destroy', function() { 4 | var store 5 | 6 | before(function() { 7 | store = new Store({ 8 | type: 'rest', 9 | url: 'http://localhost:8889', 10 | version: '~1.0' 11 | }) 12 | 13 | store.Model('User', function() { 14 | this.attribute('id', Number, { primary: true }) 15 | this.attribute('login', String) 16 | this.attribute('email', String) 17 | }) 18 | }) 19 | 20 | it('destroys a record (destroy)', function() { 21 | return store.ready(function() { 22 | var User = store.Model('User') 23 | return User.find(3).exec(function(record) { 24 | record.id.should.be.equal(3) 25 | return record.destroy() 26 | }) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /lib/stores/ldap/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./dn'), 3 | require('./schema'), 4 | require('./attributes'), 5 | require('./conditions'), 6 | require('./connection'), 7 | require('./destroy'), 8 | require('./errors'), 9 | require('./exec'), 10 | require('./limit'), 11 | require('./object_class'), 12 | require('./relations'), 13 | require('./save'), 14 | require('./search_root'), 15 | require('./search_scope'), 16 | require('./select'), 17 | require('./utils'), 18 | require('./data_types/_operators'), 19 | require('./data_types/dn_array'), 20 | require('./data_types/dn'), 21 | require('./data_types/object_class'), 22 | require('./relations/belongs_to_many'), 23 | require('./relations/has_children'), 24 | require('./relations/has_many'), 25 | require('./relations/has_parent') 26 | ] 27 | -------------------------------------------------------------------------------- /lib/stores/sql/table_name.js: -------------------------------------------------------------------------------- 1 | const inflection = require('inflection') 2 | 3 | /* 4 | * DEFINITION 5 | */ 6 | exports.definition = { 7 | mixinCallback: function() { 8 | this.tableName = inflection.underscore(inflection.pluralize(this.modelName)) 9 | 10 | if ( 11 | this.store.config.inflection && 12 | this.store.config.inflection[this.tableName] 13 | ) { 14 | this.tableName = this.store.config.inflection[this.tableName] 15 | } 16 | }, 17 | 18 | getName: function() { 19 | return this.tableName 20 | } 21 | } 22 | 23 | /* 24 | * STORE 25 | */ 26 | exports.store = { 27 | getByTableName: function(tableName) { 28 | for (var i in this.models) { 29 | if (this.models[i].definition.tableName === tableName) { 30 | return this.models[i] 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/base/cache.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | this.cache = this.store.getCache(this.modelName) || {} 7 | if (this.cacheDisabled !== true) 8 | this.store.setCache(this.modelName, this.cache) 9 | }, 10 | 11 | getCache: function(key) { 12 | return this.store.utils.clone(this.cache[key]) 13 | }, 14 | 15 | setCache: function(key, cache) { 16 | this.cache[key] = this.store.utils.clone(cache) 17 | } 18 | } 19 | 20 | /* 21 | * STORE 22 | */ 23 | exports.store = { 24 | getCache: function(modelName) { 25 | if (this.cache && this.cache[modelName]) { 26 | return this.cache[modelName] 27 | } 28 | }, 29 | 30 | setCache: function(modelName, cache) { 31 | if (!this.cache) this.cache = {} 32 | this.cache[modelName] = cache 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/stores/ldap/relations.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | relation: function(name, options) { 3 | options = options || {} 4 | const utils = this.store.utils 5 | 6 | options.afterDestroy = function(parent, transOptions) { 7 | if (options.dependent === 'destroy') { 8 | return parent[name].then(function(records) { 9 | const jobs = [] 10 | records.forEach(function(record) { 11 | jobs.push(function() { 12 | return record.destroy(transOptions) 13 | }) 14 | }) 15 | return utils.parallel(jobs) 16 | }) 17 | } 18 | } 19 | 20 | this.callParent(name, options) 21 | }, 22 | 23 | belongsTo: function(name, options) { 24 | options = options || {} 25 | if (!options.to) options.to = 'dn' 26 | return this.callParent(name, options) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/stores/mysql/connection.js: -------------------------------------------------------------------------------- 1 | const Knex = require('knex') 2 | 3 | /* 4 | * STORE 5 | */ 6 | exports.store = { 7 | connect: function() { 8 | this.connection = Knex({ 9 | client: 'mysql', 10 | version: this.config.version || '5.7', 11 | connection: this.config.connection || { 12 | socketPath: this.config.socketPath, 13 | host: this.config.host || this.config.hostname, 14 | port: this.config.port, 15 | user: this.config.user || this.config.username, 16 | password: this.config.password, 17 | database: this.config.database, 18 | charset: this.config.charset 19 | } 20 | }) 21 | this.supportsReturning = true 22 | }, 23 | 24 | close: function(callback) { 25 | if (!this.connection) return callback ? callback() : null 26 | this.connection.client.destroy(callback) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/record-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../lib/store') 4 | 5 | describe('Record: Base', function() { 6 | var store = new Store() 7 | 8 | store.Model('User', function() {}) 9 | var User, phil 10 | 11 | before(function() { 12 | return store.ready(function() { 13 | User = store.Model('User') 14 | phil = new User({ 15 | email: 'phiw@gmx.net' 16 | }) 17 | }) 18 | }) 19 | 20 | it('is a object', function() { 21 | phil.should.be.a.Object() 22 | }) 23 | 24 | it('has validate() mixin method', function() { 25 | should.exist(phil.validate) 26 | phil.validate.should.be.a.Function() 27 | }) 28 | 29 | it('has definition', function() { 30 | should.exist(phil.definition) 31 | }) 32 | 33 | it('has correct model name', function() { 34 | phil.model.should.be.equal(User) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/stores/rest/operators.js: -------------------------------------------------------------------------------- 1 | /* 2 | * STORE 3 | */ 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.addOperator( 7 | 'eq', 8 | function(attr, value, options, cond) { 9 | options.params[attr] = value 10 | }, 11 | { 12 | default: true, 13 | nullifyEmptyArray: true 14 | } 15 | ) 16 | 17 | this.attributeTypes[String].operators = { 18 | default: 'eq', 19 | eq: this.operatorTypes['eq'] 20 | } 21 | 22 | this.attributeTypes[Number].operators = { 23 | default: 'eq', 24 | eq: this.operatorTypes['eq'] 25 | } 26 | 27 | this.attributeTypes[Date].operators = { 28 | default: 'eq', 29 | eq: this.operatorTypes['eq'] 30 | } 31 | 32 | this.attributeTypes[Boolean].operators = { 33 | default: 'eq', 34 | eq: this.operatorTypes['eq'] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/schema/container.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next: unable to test via travis-ci */ 2 | exports.store = { 3 | mixinCallback: function() { 4 | this.Model('Container', function() { 5 | this.isContainer('cn') 6 | this.objectClass = ['top', 'container'] 7 | 8 | this.attribute('name', String) 9 | this.attribute('cn', String) 10 | this.attribute('description', String) 11 | this.attribute('objectGUID', 'guid', { persistent: false }) 12 | this.attribute('uSNChanged', Number, { persistent: false }) 13 | this.attribute('whenChanged', 'date', { persistent: false }) 14 | this.attribute('whenCreated', 'date', { persistent: false }) 15 | 16 | this.validatesPresenceOf('name') 17 | 18 | this.convertWrite('cn', function(cn) { 19 | if (!cn) cn = this.name 20 | return cn 21 | }) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/stores/ldap/connection.js: -------------------------------------------------------------------------------- 1 | const ldap = require('ldapjs') 2 | 3 | /* 4 | * STORE 5 | */ 6 | exports.store = { 7 | ldapjs: ldap, 8 | 9 | connect: function() { 10 | this.connection = ldap.createClient( 11 | this.config.connection || { 12 | url: this.config.url, 13 | bindDN: this.config.user, 14 | bindCredentials: this.config.password, 15 | maxConnections: this.config.maxConnections || 10, 16 | tlsOptions: this.config.tlsOptions, 17 | reconnect: this.config.reconnect || true 18 | } 19 | ) 20 | }, 21 | 22 | close: function() { 23 | this.connection.unbind() 24 | } 25 | } 26 | 27 | /* 28 | * CHAIN 29 | */ 30 | exports.model = { 31 | mixinCallback: function() { 32 | var self = this 33 | this.__defineGetter__('connection', function() { 34 | return self.definition.store.connection 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/date.js: -------------------------------------------------------------------------------- 1 | const parse = require('date-fns/parse') 2 | const format = require('date-fns/format') 3 | 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.addType( 7 | 'date', 8 | { 9 | read: function(value) { 10 | if (value === null) return null 11 | return format(parse(value), 'YYYY-MM-DD') 12 | }, 13 | input: function(value) { 14 | if (value === null) return null 15 | return format(parse(value), 'YYYY-MM-DD') 16 | }, 17 | write: function(value) { 18 | return value 19 | }, 20 | output: function(value) { 21 | return value 22 | } 23 | }, 24 | { 25 | migration: 'date', 26 | operators: { 27 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 28 | } 29 | } 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | openrecord - Active record like ORM for nodejs 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/base/connection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * STORE 3 | */ 4 | exports.store = { 5 | mixinCallback: function() { 6 | var self = this 7 | var connection 8 | var connectCalled = false 9 | 10 | // define setter and getter to manage the connection 11 | Object.defineProperty(this, 'connection', { 12 | enumerable: false, 13 | set: function(_connection) { 14 | if (connection) self.close() // close old connection before we overwrite it with a new one 15 | connection = _connection 16 | }, 17 | 18 | get: function() { 19 | if (!connection && !connectCalled) { 20 | self.connect() 21 | connectCalled = true 22 | } 23 | return connection 24 | } 25 | }) 26 | 27 | if (this.config.autoConnect !== false) { 28 | this.connect() 29 | } 30 | }, 31 | 32 | connect: function() {}, 33 | 34 | close: function() {} 35 | } 36 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/_geom.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // convert '(1,2)' to {x:1, y:2} 3 | convertPoint(str){ 4 | if(typeof str !== 'string') return null 5 | const match = str.match(/^\(([0-9.]+),([0-9.]+)\)$/) 6 | if(match) return {x: parseFloat(match[1]), y: parseFloat(match[2])} 7 | }, 8 | 9 | // convert '(1,2),(4,5)' to [{x:1, y:2},{x: 4, y: 5}] 10 | convertPoints: function(str){ 11 | if(typeof str !== 'string') return null 12 | return str.replace(/,(\([0-9.]+,[0-9.]+\))/g, '|$1') 13 | .split('|') 14 | .map(module.exports.convertPoint) 15 | }, 16 | 17 | pointToString(point){ 18 | if(!point || typeof point !== 'object') return null 19 | return '(' + point.x + ',' + point.y + ')' 20 | }, 21 | 22 | pointsToString(points){ 23 | if(!points || !Array.isArray(points)) return null 24 | return points.map(module.exports.pointToString).join(',') 25 | } 26 | } -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | In order to use OPENRECORD with your database you need to install `openrecord` **and** the corresponding database package. 4 | 5 | 6 | ## For a Postgres Database 7 | ``` 8 | npm i openrecord pg 9 | ``` 10 | 11 | ## For a MySQL Database 12 | ``` 13 | npm i openrecord mysql 14 | ``` 15 | 16 | ## For a SQLite3 Database 17 | ``` 18 | npm i openrecord sqlite3 19 | ``` 20 | 21 | ## For an Oracle Database (EXPERIMENTAL) 22 | ``` 23 | npm i openrecord oracledb 24 | ``` 25 | 26 | ## For a REST Backend (EXPERIMENTAL) 27 | ``` 28 | npm i openrecord axios 29 | ``` 30 | 31 | ## For a LDAP Compatible Backend (e.g. OpenLdap or Microsofts Active Directory) 32 | ``` 33 | npm i openrecord ldapjs 34 | ``` 35 | 36 | 37 | ## GraphQL 38 | 39 | If you you want to use OPENRECORD with [GraphQL](http://graphql.org/graphql-js/), you'll need to install `graphql` as well. 40 | See [GraphQL with OPENRECORD](graphql.md) 41 | -------------------------------------------------------------------------------- /lib/stores/oracle/connection.js: -------------------------------------------------------------------------------- 1 | var Knex = require('knex') 2 | 3 | /* 4 | * STORE 5 | */ 6 | exports.store = { 7 | connect: function() { 8 | this.connection = Knex({ 9 | client: 'oracledb', 10 | connection: this.config.connection || { 11 | host: this.config.host || this.config.hostname, 12 | port: this.config.port, 13 | user: this.config.user || this.config.username, 14 | password: this.config.password, 15 | connectString: this.config.connectString 16 | // fetchAsString: [ 'number', 'clob' ], 17 | // pool: { min: 5, max: 5 } 18 | // database: this.config.database, 19 | // charset: this.config.charset 20 | } 21 | }) 22 | this.supportsReturning = true 23 | }, 24 | 25 | close: function(callback) { 26 | if (!this.connection) return callback ? callback() : null 27 | this.connection.client.destroy(callback) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/stores/ldap/select.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MODEL 3 | */ 4 | exports.model = { 5 | /** 6 | * Specify SQL select fields. Default: * 7 | * @class Model 8 | * @method select 9 | * @param {array} fields - The field names 10 | * 11 | * 12 | * @return {Model} 13 | */ 14 | select: function() { 15 | var self = this.chain() 16 | 17 | var args = this.definition.store.utils.args(arguments) 18 | var fields = [] 19 | fields = fields.concat.apply(fields, args) // flatten 20 | 21 | self.addInternal('select', fields) 22 | 23 | return self 24 | } 25 | } 26 | 27 | /* 28 | * DEFINITION 29 | */ 30 | exports.definition = { 31 | mixinCallback: function() { 32 | // add all attribute names to the search attributes 33 | this.beforeFind(function(options) { 34 | var attributes = this.getInternal('select') 35 | if (attributes) options.attributes = attributes 36 | }, -100) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/stores/ldap/data_types/dn.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | const Utils = this.utils 4 | 5 | this.addType( 6 | 'dn', 7 | { 8 | read: function(value) { 9 | /* istanbul ignore if */ 10 | if (value === null) return null 11 | return Utils.normalizeDn(value) 12 | }, 13 | input: function(value) { 14 | /* istanbul ignore if */ 15 | if (value === null) return null 16 | return Utils.normalizeDn(value, false) // of change the dn on our side, we wont lower case them => writes 17 | }, 18 | write: function(value) { 19 | /* istanbul ignore if */ 20 | if (value === null) return null 21 | return value 22 | } 23 | }, 24 | { 25 | operators: { 26 | default: 'eq', 27 | defaults: ['eq'] 28 | } 29 | } 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/model-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../lib/store') 4 | 5 | describe('Model: Base', function() { 6 | var store = new Store() 7 | 8 | store.Model('User', function() {}) 9 | 10 | var User 11 | before(function() { 12 | return store.ready(function() { 13 | User = store.Model('User') 14 | }) 15 | }) 16 | 17 | it('is a function', function() { 18 | User.should.be.a.Function() 19 | }) 20 | 21 | it('has new() mixin method', function() { 22 | should.exist(User.new) 23 | User.new.should.be.a.Function() 24 | }) 25 | 26 | it('has chain() mixin method', function() { 27 | should.exist(User.chain) 28 | User.chain.should.be.a.Function() 29 | }) 30 | 31 | it('has definition', function() { 32 | should.exist(User.definition) 33 | }) 34 | 35 | it('has correct model name', function() { 36 | User.definition.modelName.should.be.equal('User') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /lib/stores/ldap/controls/tree_delete.js: -------------------------------------------------------------------------------- 1 | const ldap = require('ldapjs') 2 | const util = require('util') 3 | 4 | var Control = ldap.Control 5 | 6 | /// --- API 7 | /* istanbul ignore next */ 8 | function DeleteTreeControl(options) { 9 | if (!options) { 10 | options = {} 11 | } 12 | 13 | options.type = DeleteTreeControl.OID 14 | options.value = null 15 | 16 | Control.call(this, options) 17 | this.value = {} 18 | } 19 | 20 | util.inherits(DeleteTreeControl, Control) 21 | module.exports = DeleteTreeControl 22 | 23 | /* istanbul ignore next */ 24 | DeleteTreeControl.prototype.parse = function parse(buffer) { 25 | return true 26 | } 27 | 28 | /* istanbul ignore next */ 29 | DeleteTreeControl.prototype._toBer = function(ber) {} 30 | 31 | /* istanbul ignore next */ 32 | DeleteTreeControl.prototype._json = function(obj) { 33 | obj.controlValue = this.value 34 | return obj 35 | } 36 | 37 | DeleteTreeControl.OID = '1.2.840.113556.1.4.805' 38 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/line.js: -------------------------------------------------------------------------------- 1 | const geom = require('./_geom') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'line', 7 | { 8 | read: function(value) { 9 | if(Array.isArray(value)) return value 10 | if(typeof value === 'string'){ 11 | const parts = value 12 | .replace(/(^{|}$)/g, '') 13 | .split(',') 14 | .map(parseFloat) 15 | return {A: parts[0], B: parts[1], C: parts[2]} 16 | } 17 | return null 18 | }, 19 | 20 | write: function(value) { 21 | if(Array.isArray(value)) return '[' + geom.pointsToString(value) + ']' 22 | if(typeof value === 'object') return '{' + value.A +',' + value.B + ',' + value.B + '}' 23 | return value 24 | } 25 | }, 26 | { 27 | array: true, 28 | migration: ['line'] 29 | } 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/base/method.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | /** 6 | * Adds a new method to the record 7 | * 8 | * @class Definition 9 | * @method method 10 | * @param {string} name - The name of the method 11 | * @param {function} callback - The function 12 | * 13 | * @return {Definition} 14 | */ 15 | method: function(name, fn) { 16 | if (this.attributes[name]) 17 | throw new Error( 18 | 'A method with the same name as a attribure is not allowed' 19 | ) 20 | this.instanceMethods[name] = fn 21 | 22 | return this 23 | }, 24 | 25 | /** 26 | * Adds a new method to the class 27 | * 28 | * @class Definition 29 | * @method method 30 | * @param {string} name - The name of the method 31 | * @param {function} callback - The function 32 | * 33 | * @return {Definition} 34 | */ 35 | staticMethod: function(name, fn) { 36 | this.staticMethods[name] = fn 37 | 38 | return this 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/base/methods.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | /** 3 | * Add custom methods to your Model`s Records 4 | * just define a function: 5 | * ```js 6 | * this.function_name = function(){ 7 | * //this == Record 8 | * }; 9 | * ``` 10 | * This will automatically add the new method to your Record 11 | * 12 | * @class Definition 13 | * @name Custom Record Methods 14 | * 15 | */ 16 | mixinCallback: function() { 17 | var objKeys = [] 18 | var self = this 19 | 20 | this.use(function() { 21 | // get all current property names 22 | objKeys = Object.keys(this) 23 | }, 90) 24 | 25 | this.on('finished', function() { 26 | // an now search for new ones ==> instance methods for our new model class 27 | 28 | Object.keys(self).forEach(function(name) { 29 | if (objKeys.indexOf(name) === -1) { 30 | self.instanceMethods[name] = self[name] 31 | delete self[name] 32 | } 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/stores/sql/data_types/time.js: -------------------------------------------------------------------------------- 1 | const parse = require('date-fns/parse') 2 | const format = require('date-fns/format') 3 | 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.addType( 7 | 'time', 8 | { 9 | read: function(value) { 10 | return value 11 | }, 12 | input: function(value) { 13 | if (value === null) return null 14 | var dt 15 | 16 | if (typeof value === 'string') { 17 | dt = parse('2000-01-01 ' + value) 18 | } else { 19 | dt = parse(value) 20 | } 21 | 22 | return format(dt, 'HH:mm:ss') 23 | }, 24 | write: function(value) { 25 | return value 26 | }, 27 | output: function(value) { 28 | return value 29 | } 30 | }, 31 | { 32 | migration: 'time', 33 | operators: { 34 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 35 | } 36 | } 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/path.js: -------------------------------------------------------------------------------- 1 | const geom = require('./_geom') 2 | 3 | // TODO: somehow indicate `open` or `closed` paths 4 | // `where the points are the end points of the line segments comprising the path. Square brackets ([]) indicate an open path, while parentheses (()) indicate a closed path. When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed.` 5 | exports.store = { 6 | mixinCallback: function() { 7 | this.addType( 8 | 'path', 9 | { 10 | read: function(value) { 11 | if(Array.isArray(value)) return value 12 | if(typeof value === 'string') return geom.convertPoints(value.replace(/(^(\[|\()|(\]|\))$)/g, '')) 13 | return null 14 | }, 15 | 16 | write: function(value) { 17 | if(Array.isArray(value)) return '[' + geom.pointsToString(value) + ']' 18 | return value 19 | } 20 | }, 21 | { 22 | array: true, 23 | migration: ['path', 'lseg'] 24 | } 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/stores/sql/plugins/serialize.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | serialize: function(attribute, serializer) { 3 | serializer = serializer || JSON 4 | 5 | this.convertOutput( 6 | attribute, 7 | function(value) { 8 | if (value === null) return null 9 | if (typeof value === 'object') return value 10 | try { 11 | return serializer.parse(value) 12 | } catch (e) { 13 | throw new Error( 14 | 'Serialize error for attribute "' + attribute + '": ' + e 15 | ) 16 | } 17 | }, 18 | false 19 | ) 20 | 21 | this.convertInput( 22 | attribute, 23 | function(value) { 24 | if (value === null) return null 25 | if (typeof value === 'string') return value 26 | try { 27 | return serializer.stringify(value) 28 | } catch (e) { 29 | throw new Error( 30 | 'Serialize error for attribute "' + attribute + '": ' + e 31 | ) 32 | } 33 | }, 34 | false 35 | ) 36 | 37 | return this 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/stores/ldap/object_class.js: -------------------------------------------------------------------------------- 1 | const inflection = require('inflection') 2 | 3 | /* 4 | * DEFINITION 5 | */ 6 | exports.definition = { 7 | mixinCallback: function() { 8 | var self = this 9 | 10 | self.objectClass = inflection.underscore(self.modelName) 11 | 12 | // add objectClass=user filter 13 | self.beforeFind(function() { 14 | if (this.getInternal('without_object_class') !== true) { 15 | this.where({ objectClass: self.getName() }) // e.g. objectClass=user 16 | } 17 | }, -9) 18 | }, 19 | 20 | getName: function() { 21 | if (Array.isArray(this.objectClass)) 22 | return this.objectClass[this.objectClass.length - 1] 23 | return this.objectClass 24 | } 25 | } 26 | 27 | /* 28 | * STORE 29 | */ 30 | exports.store = { 31 | getByObjectClass: function(objectClass) { 32 | if (!objectClass) return null 33 | for (var i in this.models) { 34 | if ( 35 | this.models[i].definition.objectClass.toString() === 36 | objectClass.toString() 37 | ) { 38 | return this.models[i] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/stores/sql/attributes.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | mixinCallback: function() { 3 | this.use(function() { 4 | var attributes = this.getCache('attributes') 5 | if (attributes) { 6 | return this.setTableAttributes(attributes) 7 | } 8 | 9 | if (!this.tableName) return 10 | if (!this.store.loadTableAttributes) return 11 | if (this.store.config.autoAttributes === false) return 12 | 13 | const self = this 14 | 15 | return this.store 16 | .loadTableAttributes(this.tableName) 17 | .then(function(attributes) { 18 | self.setCache('attributes', attributes) 19 | return self.setTableAttributes(attributes) 20 | }) 21 | }, 80) 22 | }, 23 | 24 | setTableAttributes: function(attributes) { 25 | var self = this 26 | 27 | attributes.forEach(function(attr) { 28 | self.attribute(attr.name, attr.type, attr.options) 29 | attr.validations.forEach(function(validation) { 30 | self[validation.name].apply(self, [attr.name].concat(validation.args)) 31 | }) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Philipp Waldmann (@APhilWaldmann) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lib/stores/postgres/connection.js: -------------------------------------------------------------------------------- 1 | const Knex = require('knex') 2 | 3 | /* 4 | * STORE 5 | */ 6 | exports.store = { 7 | connect: function() { 8 | const pg = require('pg') 9 | 10 | // convert e.g. count(*) to integer instead of a string 11 | // see https://github.com/tgriesser/knex/issues/387 12 | pg.types.setTypeParser(20, 'text', parseInt) 13 | 14 | var connectionConfig = { 15 | client: 'pg', 16 | connection: this.config.connection || { 17 | host: this.config.host || this.config.hostname, 18 | port: this.config.port, 19 | user: this.config.user || this.config.username, 20 | password: this.config.password, 21 | database: this.config.database, 22 | charset: this.config.charset 23 | } 24 | } 25 | if (this.config.pool) connectionConfig['pool'] = this.config.pool 26 | 27 | this.connection = Knex(connectionConfig) 28 | this.supportsReturning = true 29 | }, 30 | 31 | close: function(callback) { 32 | if (!this.connection) return callback ? callback() : null 33 | this.connection.client.destroy(callback) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/data_types/date.js: -------------------------------------------------------------------------------- 1 | const parse = require('date-fns/parse') 2 | const format = require('date-fns/format') 3 | /* istanbul ignore next: unable to test via travis-ci */ 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.addType( 7 | 'date', 8 | { 9 | read: function(value) { 10 | if (value === null) return null 11 | return new Date( 12 | value.replace( 13 | /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(.{2})$/, 14 | '$1.$2.$3 $4:$5:$6.$7' 15 | ) 16 | ) 17 | }, 18 | write: function(value) { 19 | if (value === null) return null 20 | return format(parse(value), 'YYYYMMDDHHmmss.S') + 'Z' 21 | }, 22 | output: function(value) { 23 | if (value === null) return null 24 | return format(parse(value), 'YYYY-MM-DD HH:mm:ss') 25 | } 26 | }, 27 | { 28 | operators: { 29 | default: 'eq', 30 | defaults: ['eq', 'gt', 'gte', 'lt', 'lte', 'not'] 31 | } 32 | } 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | sudo: required 4 | 5 | env: 6 | global: 7 | - ORACLE_COOKIE=sqldev 8 | - ORACLE_FILE=oracle11g/xe/oracle-xe-11.2.0-1.0.x86_64.rpm.zip 9 | #- ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe 10 | - OCI_LIB_DIR=/u01/app/oracle/product/11.2.0/xe/lib 11 | - LD_LIBRARY_PATH=/u01/app/oracle/product/11.2.0/xe/lib 12 | - ORACLE_SID=XE 13 | 14 | 15 | services: 16 | - mysql 17 | - postgresql 18 | 19 | addons: 20 | postgresql: "9.6" 21 | apt: 22 | packages: 23 | - postgresql-9.6-postgis-2.3 24 | 25 | before_install: 26 | - mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'travis'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;"; 27 | # - wget https://raw.githubusercontent.com/Vincit/travis-oracledb-xe/master/accept_the_license_agreement_for_oracledb_xe_11g_and_install.sh 28 | # - bash ./accept_the_license_agreement_for_oracledb_xe_11g_and_install.sh 29 | - export PATH=$ORACLE_HOME/bin/:$PATH 30 | # - sqlplus -V 31 | - psql --version 32 | - mysql --version 33 | # - npm i oracledb 34 | 35 | node_js: 36 | - "10" 37 | - "12" 38 | 39 | after_success: npm run report-coverage 40 | -------------------------------------------------------------------------------- /lib/stores/rest/urls.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | mixinCallback: function() { 3 | var res = this.resource 4 | var path = this.store.config.path || '' 5 | var baseParams = this.store.config.baseParams 6 | 7 | this.actions = { 8 | index: { method: 'get', url: '/' + path + res, params: baseParams || {} }, 9 | show: { 10 | method: 'get', 11 | url: '/' + path + res + '/:id', 12 | params: baseParams || {} 13 | }, 14 | create: { 15 | method: 'post', 16 | url: '/' + path + res, 17 | params: baseParams || {} 18 | }, 19 | update: { 20 | method: 'put', 21 | url: '/' + path + res + '/:id', 22 | params: baseParams || {} 23 | }, 24 | destroy: { 25 | method: 'delete', 26 | url: '/' + path + res + '/:id', 27 | params: baseParams || {} 28 | } 29 | } 30 | }, 31 | 32 | addBaseParam: function(name, value) { 33 | for (var action in this.actions) { 34 | if (this.actions.hasOwnProperty(action)) { 35 | this.actions[action].params[name] = value 36 | } 37 | } 38 | return this 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/sql/postgres/raw-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../store/postgres') 2 | 3 | describe('Postgres: Raw Query', function() { 4 | var store 5 | var database = 'raw_test' 6 | 7 | before(function(next) { 8 | this.timeout(5000) 9 | beforePG( 10 | database, 11 | [ 12 | 'CREATE TABLE users(id serial primary key, login TEXT NOT NULL, email TEXT)', 13 | "INSERT INTO users(login, email) VALUES('phil', 'phil@mail.com')" 14 | ], 15 | next 16 | ) 17 | }) 18 | 19 | before(function() { 20 | store = new Store({ 21 | host: 'localhost', 22 | type: 'postgres', 23 | database: database, 24 | user: 'postgres', 25 | password: '' 26 | }) 27 | 28 | store.Model('user', function() {}) 29 | }) 30 | 31 | after(function(next) { 32 | afterPG(database, next) 33 | }) 34 | 35 | it('raw() runs the raw sql query', function() { 36 | return store.ready(function() { 37 | var User = store.Model('User') 38 | 39 | return User.raw('SELECT COUNT(*) FROM users').then(function(result) { 40 | result.rows.should.be.eql([{ count: 1 }]) 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /lib/stores/oracle/data_types/date.js: -------------------------------------------------------------------------------- 1 | const parse = require('date-fns/parse') 2 | const format = require('date-fns/format') 3 | 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.addType( 7 | 'date', 8 | { 9 | read: function(value) { 10 | if (value === null) return null 11 | return format(parse(value), 'YYYY-MM-DD') 12 | }, 13 | input: function(value) { 14 | if (value === null) return null 15 | return format(parse(value), 'YYYY-MM-DD') 16 | }, 17 | write: function(value) { 18 | return new Date(value) 19 | }, 20 | output: function(value) { 21 | return value 22 | }, 23 | condition: function(value) { 24 | const self = this 25 | if (Array.isArray(value)) 26 | return value.map(function(d) { 27 | return self.condition(d) 28 | }) 29 | return new Date(value) 30 | } 31 | }, 32 | { 33 | migration: 'date', 34 | operators: { 35 | defaults: ['eq', 'not', 'gt', 'gte', 'lt', 'lte', 'between'] 36 | } 37 | } 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/sql/mysql/__helper.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec 2 | 3 | global.beforeMYSQL = function(db, sql, next) { 4 | exec('mysql -u root -e "DROP DATABASE ' + db + '"', function(_, result) { 5 | // eslint-disable-line 6 | exec('mysql -u root -e "create database ' + db + '"', function(_) { 7 | // eslint-disable-line 8 | exec('mysql ' + db + ' -e "' + sql.join(';') + '" -u root', function( 9 | err, 10 | result 11 | ) { 12 | if (err) throw new Error(err) 13 | next() 14 | }) 15 | }) 16 | }) 17 | } 18 | 19 | global.afterMYSQL = function(db, next) { 20 | next() 21 | } 22 | 23 | global.testMYSQL = function(name, queries, prefix) { 24 | var db = name.replace('/', '_') + '_test' 25 | require('../__shared/' + name + '-test' + (prefix || ''))( 26 | 'SQL (MySQL)', 27 | function(next) { 28 | beforeMYSQL(db, queries, next) 29 | }, 30 | function(next, store) { 31 | store.close(function() {}) 32 | afterMYSQL(db, next) 33 | }, 34 | { 35 | host: 'localhost', 36 | type: 'mysql', 37 | database: db, 38 | user: 'travis', 39 | password: '' 40 | } 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /lib/base/expect_result.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | const Store = require('../store') 4 | 5 | Store.addExceptionType(function RecordNotFoundError(Model) { 6 | Error.apply(this) 7 | this.message = "Can't find any record for " + Model.definition.modelName 8 | }) 9 | } 10 | } 11 | 12 | /* 13 | * MODEL 14 | */ 15 | exports.model = { 16 | /** 17 | * When called, it will throw an error if the resultset is empty 18 | * @class Model 19 | * @method expectResult 20 | * 21 | * @see Model.get 22 | * 23 | * @return {Model} 24 | */ 25 | expectResult: function() { 26 | var self = this.chain() 27 | 28 | self.setInternal('expectResult', true) 29 | 30 | return self 31 | } 32 | } 33 | 34 | /* 35 | * DEFINITION 36 | */ 37 | exports.definition = { 38 | mixinCallback: function() { 39 | const Store = require('../store') 40 | 41 | this.afterFind(function(data) { 42 | var expectResult = this.getInternal('expectResult') 43 | 44 | if (expectResult && (!data.result || data.result.length === 0)) { 45 | return Promise.reject(new Store.RecordNotFoundError(this)) 46 | } 47 | }, 10) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/sql/table_name-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../store/sql') 2 | 3 | describe('SQL: Table Name', function() { 4 | var store 5 | 6 | before(function() { 7 | store = new Store({ 8 | type: 'sql' 9 | }) 10 | 11 | store.Model('User', function() {}) 12 | store.Model('CamelCasedTableName', function() {}) 13 | }) 14 | 15 | it('has the right table name', function() { 16 | return store.ready(function() { 17 | var User = store.Model('User') 18 | User.definition.tableName.should.be.equal('users') 19 | }) 20 | }) 21 | 22 | it('has the right table name on camelcased models', function() { 23 | return store.ready(function() { 24 | var CamelCasedTableName = store.Model('CamelCasedTableName') 25 | CamelCasedTableName.definition.tableName.should.be.equal( 26 | 'camel_cased_table_names' 27 | ) 28 | }) 29 | }) 30 | 31 | it("returns a model by it's table name", function() { 32 | return store.ready(function() { 33 | var CamelCasedTableName = store.getByTableName('camel_cased_table_names') 34 | CamelCasedTableName.definition.tableName.should.be.equal( 35 | 'camel_cased_table_names' 36 | ) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /lib/base/dynamic_loading.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob') 2 | 3 | exports.utils = { 4 | require: function(path, options) { 5 | if (!options) options = {} 6 | if (!Array.isArray(path)) path = [path] 7 | 8 | var files = [] 9 | var tmp = [] 10 | var i 11 | 12 | if (options.includePathNames === true) { 13 | tmp = {} 14 | } 15 | 16 | for (i in path) { 17 | var f = glob.sync(path[i], options) 18 | if (f.length === 0 && !path[i].match(/[/\\]/)) f = [path[i]] 19 | files = files.concat(f) 20 | } 21 | 22 | for (i in files) { 23 | try { 24 | var plugin = require(files[i]) 25 | } catch (e) { 26 | throw new Error( 27 | 'File not found: ' + 28 | files[i] + 29 | '. If you use webpack or a similar bundler, make sure that plugins aren\'t loaded via a path, but with require("path")' 30 | ) 31 | } 32 | if (options.only) plugin = plugin[options.only] 33 | 34 | if (plugin) { 35 | if (options.includePathNames === true) { 36 | tmp[files[i]] = plugin 37 | } else { 38 | tmp.push(plugin) 39 | } 40 | } 41 | } 42 | 43 | return tmp 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/stores/ldap/data_types/dn_array.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | exports.store = { 3 | mixinCallback: function() { 4 | const Utils = this.utils 5 | 6 | this.addType( 7 | 'dn_array', 8 | { 9 | read: function(values) { 10 | if (values == null) return [] 11 | if (!Array.isArray(values)) values = [values] 12 | return Utils.normalizeDn(values) 13 | }, 14 | write: function(values) { 15 | if (!values) return [] 16 | if (!Array.isArray(values)) values = [values] 17 | return values 18 | }, 19 | input: function(values) { 20 | if (values == null) return [] 21 | if (!Array.isArray(values)) values = [values] 22 | return Utils.normalizeDn(values) 23 | }, 24 | output: function(values) { 25 | if (values === null) return [] 26 | return values 27 | } 28 | }, 29 | { 30 | array: true, 31 | defaults: { 32 | default: [], 33 | track_object_changes: true 34 | }, 35 | operators: { 36 | default: 'eq', 37 | defaults: ['eq'] 38 | } 39 | } 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/stores/ldap/errors.js: -------------------------------------------------------------------------------- 1 | const ldap = require('ldapjs') 2 | 3 | exports.store = { 4 | mixinCallback: function() { 5 | var self = this 6 | 7 | self.ldap_errors = [] 8 | self.connection.on('error', function(err) { 9 | throw err 10 | }) 11 | }, 12 | 13 | addLdapValidationError: function(errorType, msgMatch, attribute, error) { 14 | if (ldap[errorType]) { 15 | this.ldap_errors.push({ 16 | cls: ldap[errorType], 17 | match: msgMatch, 18 | attribute: attribute, 19 | error: error 20 | }) 21 | } 22 | }, 23 | 24 | convertLdapErrorToValidationError: function(record, error) { 25 | for (var i = 0; i < this.ldap_errors.length; i++) { 26 | // check error class 27 | if (error instanceof this.ldap_errors[i].cls) { 28 | // check message match 29 | if (error.message.match(this.ldap_errors[i].match)) { 30 | // add validation error + return null to avoid promise rejection 31 | record.errors.add( 32 | this.ldap_errors[i].attribute, 33 | this.ldap_errors[i].error 34 | ) 35 | return record.errors 36 | } 37 | } 38 | } 39 | 40 | return error 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/stores/sql/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./attributes'), 3 | require('./aggregate_functions'), 4 | require('./conditions'), 5 | require('./convert'), 6 | require('./destroy'), 7 | require('./exceptions'), 8 | require('./exec'), 9 | require('./group'), 10 | require('./interceptors'), 11 | require('./joins'), 12 | require('./limit'), 13 | require('./order'), 14 | require('./query'), 15 | require('./raw'), 16 | require('./relations'), 17 | require('./save'), 18 | require('./select'), 19 | require('./table_name'), 20 | require('./transaction'), 21 | require('./utils'), 22 | require('./validations'), 23 | require('./migrations/column_methods'), 24 | require('./migrations/data_types'), 25 | require('./migrations/migrations'), 26 | require('./migrations/polymorph'), 27 | require('./migrations/raw'), 28 | require('./migrations/run'), 29 | require('./migrations/seed'), 30 | require('./migrations/table_methods'), 31 | require('./plugins/nested_set'), 32 | require('./plugins/paranoid'), 33 | require('./plugins/serialize'), 34 | require('./plugins/sorted_list'), 35 | require('./plugins/stampable'), 36 | require('./plugins/total_count') 37 | ].concat(require('./data_types')) 38 | -------------------------------------------------------------------------------- /lib/base/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./parent'), 3 | require('./interceptors'), 4 | require('./attributes'), 5 | require('./cache'), 6 | require('./chainable'), 7 | require('./collection'), 8 | require('./connection'), 9 | require('./context'), 10 | require('./convert'), 11 | require('./data_types'), 12 | require('./error'), 13 | require('./every'), 14 | require('./inspect'), 15 | require('./json'), 16 | require('./limit'), 17 | require('./method'), 18 | require('./methods'), 19 | require('./exec'), 20 | require('./expect_result'), 21 | require('./include'), 22 | require('./mixin'), 23 | require('./operators'), 24 | require('./primary_keys'), 25 | require('./conditions'), 26 | require('./save'), 27 | require('./relations'), 28 | require('./relations/utils'), 29 | require('./relations/belongs_to_many'), 30 | require('./relations/belongs_to'), 31 | require('./relations/belongs_to_polymorphic'), 32 | require('./relations/has_many'), 33 | require('./relations/has_many_through'), 34 | require('./relations/has_one'), 35 | require('./relations/has'), 36 | require('./relations/bulk_fetch'), 37 | require('./scope'), 38 | require('./validations'), 39 | require('./utils') 40 | ] 41 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.toArrayCastTypes = function(fromType) { 4 | var type = this.attributeTypes[fromType] 5 | if (type) { 6 | var castfn = function(castType, attribute) { 7 | return function(value) { 8 | if (value === null) return null 9 | if (typeof value === 'string' && value.match(/^\{(.*)\}$/)) { 10 | value = value.replace(/(^\{|\}$)/g, '') 11 | if (value === '') { 12 | value = [] 13 | } else { 14 | value = value.split(',') 15 | } 16 | } 17 | 18 | if (!Array.isArray(value)) value = [value] 19 | 20 | for (var i = 0; i < value.length; i++) { 21 | value[i] = type.cast[castType].call(this, value[i], attribute) 22 | } 23 | 24 | return value 25 | } 26 | } 27 | 28 | var tmp = {} 29 | 30 | for (var castType in type.cast) { 31 | if (typeof type.cast[castType] === 'function') { 32 | tmp[castType] = castfn(castType) 33 | } 34 | } 35 | 36 | return tmp 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/sql/__shared/plugins/serialize-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../../store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': Serialize', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store(storeConf) 14 | 15 | store.Model('User', function() { 16 | this.serialize('config') 17 | }) 18 | }) 19 | 20 | it('saves serialized data', function() { 21 | return store.ready(function() { 22 | var User = store.Model('User') 23 | return User.create({ 24 | login: 'phil', 25 | config: { 26 | some: { 27 | nested: ['data'] 28 | } 29 | } 30 | }) 31 | }) 32 | }) 33 | 34 | it('reads serialized data', function() { 35 | return store.ready(function() { 36 | var User = store.Model('User') 37 | return User.find(1).exec(function(phil) { 38 | phil.config.should.be.eql({ 39 | some: { 40 | nested: ['data'] 41 | } 42 | }) 43 | }) 44 | }) 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/integer_array.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | this.addType('integer_array', this.toArrayCastTypes('integer'), { 4 | array: true, 5 | migration: { 6 | integerArray: 'integer[]' 7 | }, 8 | 9 | operators: { 10 | default: 'in', 11 | in: { 12 | defaultMethod: function(attr, value, query, cond) { 13 | query.whereRaw(attr + ' @> ARRAY[?]::integer[]', [value]) 14 | }, 15 | 16 | nullifyEmptyArray: true, 17 | on: { 18 | all: false, 19 | number: true, 20 | string: true, 21 | array: function(attr, value, query, cond) { 22 | value = value.map(function(i) { 23 | return parseInt(i, 10) 24 | }) 25 | query.whereRaw(attr + ' && ?::integer[]', [value]) 26 | }, 27 | attribute: function(attr, value, query, cond) { 28 | query.whereRaw(attr + ' @> ARRAY[' + value + ']::integer[]') 29 | }, 30 | attribute_array: function(attr, value, query, cond) { 31 | query.whereRaw(attr + ' && ' + value) 32 | } 33 | } 34 | } 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/graphql/relations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | const DEFAULT_OPTIONS = { 5 | nodes: function() { 6 | return this 7 | }, 8 | totalCount: function() { 9 | return this.totalCount() 10 | } 11 | } 12 | 13 | exports.definition = { 14 | relation: function(name, options) { 15 | if (options.graphql) { 16 | if (options.graphql === 'relay') { 17 | options.graphql = Object.assign({}, DEFAULT_OPTIONS) 18 | } 19 | 20 | if (typeof options.graphql !== 'object') 21 | throw new Error('graphql option must be an object') 22 | 23 | options.getter = function() { 24 | // this.relations is the relations object of the record! 25 | var result = this['_' + name] 26 | 27 | if (result === undefined) { 28 | result = options.collection(this) 29 | } 30 | 31 | result.__multi_resolver = true 32 | 33 | const obj = {} 34 | Object.keys(options.graphql).forEach(function(key) { 35 | if (typeof options.graphql[key] !== 'function') 36 | throw new Error('graphql.' + key + ' must be a function') 37 | obj[key] = options.graphql[key].bind(result) 38 | }) 39 | 40 | return Promise.resolve(obj) 41 | } 42 | } 43 | 44 | this.callParent(name, options) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/stores/sql/conditions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | mixinCallback: function() { 6 | this.onRawCondition(function(chain, condition, query) { 7 | for (var i = 0; i < condition.args.length; i++) { 8 | // Hacky fix for a knex problem! 9 | // TODO: still necessary? 10 | if (Array.isArray(condition.args[i])) { 11 | var len = condition.args[i].length 12 | condition.args.splice.apply( 13 | condition.args, 14 | [i, 1].concat(condition.args[i]) 15 | ) 16 | 17 | var index = 0 18 | condition.query = condition.query.replace(/\?/g, function() { 19 | if (index === i) { 20 | var tmp = [] 21 | for (var k = 0; k < len; k++) { 22 | tmp.push('?') 23 | } 24 | 25 | index++ 26 | return tmp.join(',') 27 | } 28 | index++ 29 | return '?' 30 | }) 31 | 32 | i += len 33 | } 34 | } 35 | 36 | if (query) { 37 | return query.whereRaw(condition.query, condition.args) 38 | } 39 | 40 | // see sql/group.js: 94 41 | return function(query) { 42 | query.whereRaw(condition.query, condition.args) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/data_types/group_type.js: -------------------------------------------------------------------------------- 1 | var GroupTypeBitmask = { 2 | BUILTIN_LOCAL_GROUP: 1, 3 | ACCOUNT_GROUP: 2, 4 | RESOURCE_GROUP: 4, 5 | UNIVERSAL_GROUP: 8, 6 | APP_BASIC_GROUP: 16, 7 | APP_QUERY_GROUP: 32, 8 | SECURITY_ENABLED: -2147483648 9 | } 10 | 11 | /* istanbul ignore next: unable to test via travis-ci */ 12 | exports.store = { 13 | mixinCallback: function() { 14 | this.addType( 15 | 'group_type', 16 | { 17 | read: function(value) { 18 | if (typeof value === 'string') value = parseInt(value, 10) 19 | 20 | var obj = {} 21 | for (var attrName in GroupTypeBitmask) { 22 | obj[attrName] = 23 | (value & GroupTypeBitmask[attrName]) === 24 | GroupTypeBitmask[attrName] 25 | } 26 | 27 | return obj 28 | }, 29 | 30 | write: function(value) { 31 | if (!value) value = {} 32 | var bitmask = 0 33 | for (var attrName in GroupTypeBitmask) { 34 | if (value[attrName] === true) bitmask += GroupTypeBitmask[attrName] 35 | } 36 | return bitmask 37 | } 38 | }, 39 | { 40 | binary: true, 41 | operators: { 42 | default: 'eq', 43 | defaults: ['eq', 'not'] 44 | } 45 | } 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/rest/client/resource_name-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../lib/store') 2 | 3 | describe('REST Client: Resource Name', function() { 4 | var store 5 | 6 | before(function() { 7 | store = new Store({ 8 | type: 'rest', 9 | url: 'http://localhost:8889', 10 | version: '~1.0' 11 | }) 12 | 13 | store.Model('User', function() {}) 14 | store.Model('CamelCasedResourceName', function() {}) 15 | }) 16 | 17 | it('has the right resource name', function() { 18 | return store.ready(function() { 19 | var User = store.Model('User') 20 | User.definition.resource.should.be.equal('users') 21 | }) 22 | }) 23 | 24 | it('has the right resource name on camelcased models', function() { 25 | return store.ready(function() { 26 | var CamelCasedResourceName = store.Model('CamelCasedResourceName') 27 | CamelCasedResourceName.definition.resource.should.be.equal( 28 | 'camel_cased_resource_names' 29 | ) 30 | }) 31 | }) 32 | 33 | it("returns a model by it's resource name", function() { 34 | return store.ready(function() { 35 | var CamelCasedResourceName = store.getByResource( 36 | 'camel_cased_resource_names' 37 | ) 38 | CamelCasedResourceName.definition.resource.should.be.equal( 39 | 'camel_cased_resource_names' 40 | ) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/data_types/sid.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next: unable to test via travis-ci */ 2 | exports.store = { 3 | mixinCallback: function() { 4 | // Windows SID 5 | this.addType( 6 | 'sid', 7 | { 8 | read: function(value) { 9 | if (value === null) return null 10 | 11 | var hex 12 | var i 13 | var tmp 14 | 15 | if (Buffer.from) hex = Buffer.from(value, 'base64') 16 | else hex = new Buffer(value, 'base64') // eslint-disable-line node/no-deprecated-api 17 | 18 | hex = hex.toString('hex').toUpperCase() 19 | var parts = hex.match(/.{2}/g) 20 | var output = ['S'] 21 | 22 | output.push(parseInt(parts[0], 16)) 23 | output.push(parseInt(parts[7], 16)) 24 | 25 | for (i = 8; i < parts.length; i += 4) { 26 | tmp = '' 27 | for (var x = 3; x >= 0; x--) { 28 | tmp += parts[i + x] 29 | } 30 | output.push(parseInt(tmp, 16)) 31 | } 32 | 33 | return output.join('-') 34 | }, 35 | write: function(value) { 36 | return value 37 | } 38 | }, 39 | { 40 | binary: true, 41 | operators: { 42 | default: 'eq', 43 | defaults: ['eq', 'not'] 44 | } 45 | } 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/data_types/timestamp.js: -------------------------------------------------------------------------------- 1 | const parse = require('date-fns/parse') 2 | const format = require('date-fns/format') 3 | 4 | /* istanbul ignore next: unable to test via travis-ci */ 5 | exports.store = { 6 | mixinCallback: function() { 7 | // Windows timestamp (100 nanosecond passed since 1.1.1601 00:00:00) e.g. 129822505817745000 8 | this.addType( 9 | 'timestamp', 10 | { 11 | read: function(value) { 12 | if ( 13 | value === null || 14 | value === '0' || 15 | value === '9223372036854775807' 16 | ) 17 | return null 18 | return new Date(parseInt(value, 10) / 10000 - 11644473600000) 19 | }, 20 | write: function(value) { 21 | if (value === null) return null 22 | if (value === 0 || value === -1) return value 23 | return ( 24 | 11644473600000 + 25 | parseInt(format(parse(value), 'X'), 10) * 10000 26 | ).toString() 27 | }, 28 | output: function(value) { 29 | if (value === null) return null 30 | return format(parse(value), 'YYYY-MM-DD HH:mm:ss') 31 | } 32 | }, 33 | { 34 | operators: { 35 | default: 'eq', 36 | defaults: ['eq', 'gt', 'gte', 'lt', 'lte', 'not'] 37 | } 38 | } 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/schema/group.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next: unable to test via travis-ci */ 2 | exports.store = { 3 | mixinCallback: function() { 4 | this.Model('Group', function() { 5 | this.rdnPrefix('cn') 6 | this.objectClass = ['top', 'group'] 7 | 8 | this.attribute('name', String, { emit_events: true, persistent: false }) 9 | this.attribute('sAMAccountName', String) 10 | this.attribute('groupType', 'group_type') 11 | this.attribute('description', String) 12 | 13 | this.attribute('objectGUID', 'guid', { persistent: false }) 14 | this.attribute('objectSid', 'sid', { persistent: false }) 15 | 16 | this.attribute('uSNChanged', Number, { persistent: false }) 17 | this.attribute('whenChanged', 'date', { persistent: false }) 18 | this.attribute('whenCreated', 'date', { persistent: false }) 19 | 20 | this.attribute('member', 'dn_array') 21 | 22 | this.validatesPresenceOf('name') 23 | 24 | this.hasParent('ou', { model: 'OrganizationalUnit', autoSave: true }) 25 | this.belongsToMany('members', { from: 'member', autoSave: true }) 26 | 27 | var self = this 28 | this.on('name_changed', function(record, oldValue, value) { 29 | if (value) { 30 | record.cn = value 31 | if (record.parent_dn) record.dn = self.dn(record) 32 | } 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/ldap/client/__shared/include-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../../lib/store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': Include (' + storeConf.url + ')', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store(storeConf) 14 | }) 15 | 16 | it('get child objects (one level)!', function() { 17 | return store.ready(function() { 18 | var Ou = store.Model('OrganizationalUnit') 19 | return Ou.searchRoot('ou=openrecord,' + LDAP_BASE) 20 | .include('children') 21 | .exec(function(ous) { 22 | ous.length.should.be.equal(1) 23 | ous[0].children.length.should.be.equal(4) 24 | ous[0].children[0].children.length.should.be.equal(0) 25 | }) 26 | }) 27 | }) 28 | 29 | it('get all recursive child objects!', function() { 30 | return store.ready(function() { 31 | var Ou = store.Model('OrganizationalUnit') 32 | return Ou.searchRoot('ou=openrecord,' + LDAP_BASE) 33 | .include('all_children') 34 | .exec(function(ous) { 35 | ous.length.should.be.equal(1) 36 | ous[0].all_children.length.should.not.be.equal(0) 37 | }) 38 | }) 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /test/sql/__shared/data_types-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': Data Types', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store(storeConf) 14 | 15 | store.Model('User', function() {}) 16 | }) 17 | 18 | describe('cast()', function() { 19 | it('casts BLOB to string', function() { 20 | return store.ready(function() { 21 | var User = store.Model('User') 22 | return User.definition 23 | .cast('my_blob', 45454, 'read') 24 | .should.be.equal('45454') 25 | }) 26 | }) 27 | 28 | it('casts INTEGER to number', function() { 29 | return store.ready(function() { 30 | var User = store.Model('User') 31 | return User.definition 32 | .cast('my_integer', '123.55', 'read') 33 | .should.be.equal(123) 34 | }) 35 | }) 36 | 37 | it('casts REAL to float', function() { 38 | return store.ready(function() { 39 | var User = store.Model('User') 40 | return User.definition 41 | .cast('my_real', '123.55', 'read') 42 | .should.be.equal(123.55) 43 | }) 44 | }) 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /test/sql/postgres/__helper.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec 2 | 3 | global.beforePG = function(db, sql, next) { 4 | /* exec('psql -c "SELECT pid FROM pg_stat_activity where pid <> pg_backend_pid()" -U postgres', function(err, result){ 5 | console.log('Connected', result); 6 | }); */ 7 | exec('psql -c "DROP DATABASE ' + db + '" -U postgres', function(_, result) { 8 | // eslint-disable-line 9 | exec('psql -c "create database ' + db + '" -U postgres', function( 10 | _, 11 | result 12 | ) { 13 | // eslint-disable-line 14 | exec('psql ' + db + ' -c "' + sql.join(';') + '" -U postgres', function( 15 | error, 16 | result 17 | ) { 18 | if (error) throw new Error(error) 19 | next() 20 | }) 21 | }) 22 | }) 23 | } 24 | 25 | global.afterPG = function(db, next) { 26 | next() 27 | } 28 | 29 | global.testPG = function(name, queries, prefix) { 30 | var db = name.replace('/', '_') + '_test' 31 | require('../__shared/' + name + '-test' + (prefix || ''))( 32 | 'SQL (Postgres)', 33 | function(next) { 34 | this.timeout(5000) 35 | beforePG(db, queries, next) 36 | }, 37 | function(next, store) { 38 | store.close(function() {}) 39 | afterPG(db, next) 40 | }, 41 | { 42 | host: 'localhost', 43 | type: 'postgres', 44 | database: db, 45 | user: 'postgres', 46 | password: '' 47 | } 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /test/base/data_types-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../../store/base') 4 | 5 | describe('Data Types', function() { 6 | var store = new Store() 7 | 8 | describe('getType()', function() { 9 | it('exists', function() { 10 | should.exist(store.getType) 11 | store.getType.should.be.a.Function() 12 | }) 13 | 14 | var type = store.getType(String) 15 | 16 | it('returns the correct type', function() { 17 | type.should.have.property('cast') 18 | type.cast.should.have.property('input') 19 | type.cast.should.have.property('output') 20 | }) 21 | 22 | it('casts an integer to a string', function() { 23 | type.cast.input(1234).should.be.equal('1234') 24 | }) 25 | }) 26 | 27 | describe('addType()', function() { 28 | it('exists', function() { 29 | should.exist(store.addType) 30 | store.addType.should.be.a.Function() 31 | }) 32 | 33 | var type = store.addType(RegExp, function(value) { 34 | return new RegExp(value) 35 | }) 36 | 37 | type = store.getType(RegExp) 38 | 39 | it('returns the correct type', function() { 40 | type.should.have.property('cast') 41 | type.cast.should.have.property('input') 42 | type.cast.should.have.property('output') 43 | }) 44 | 45 | it('casts an string to an regexp', function() { 46 | type.cast.input('(.*)').should.be.an.instanceOf(RegExp) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/base/scope-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../../store/base') 4 | 5 | describe('Scope', function() { 6 | var store = new Store() 7 | 8 | store.Model('User', function() { 9 | this.scope('active', function() { 10 | this.should.have.property('new') 11 | }) 12 | }) 13 | 14 | var User 15 | before(function() { 16 | return store.ready(function() { 17 | User = store.Model('User') 18 | }) 19 | }) 20 | 21 | describe('scope()', function() { 22 | it('has defined scope', function() { 23 | should.exist(User.active) 24 | }) 25 | 26 | it('scope is chainable', function() { 27 | should.exist(User.active().new) 28 | }) 29 | }) 30 | }) 31 | 32 | describe('Default Scope', function() { 33 | var store = new Store() 34 | 35 | store.Model('User', function() { 36 | this.defaultScope('test') 37 | 38 | this.scope('test', function() { 39 | this.temporaryDefinition().instanceMethods['test'] = function() { 40 | return 'test' 41 | } 42 | }) 43 | 44 | this.scope('admin', function() {}) 45 | }) 46 | 47 | var User 48 | before(function() { 49 | return store.ready(function() { 50 | User = store.Model('User') 51 | }) 52 | }) 53 | 54 | describe('scope()', function() { 55 | it('calls the default scope', function() { 56 | var a = User.admin().new() 57 | a.test().should.equal('test') 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/sql/__shared/autoload-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': AutoLoad', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store( 14 | Object.assign( 15 | { 16 | autoLoad: true 17 | }, 18 | storeConf 19 | ) 20 | ) 21 | 22 | store.Model('Avatar', function() { 23 | this.scope('test', function() {}) 24 | }) 25 | }) 26 | 27 | it('Models are loaded', function() { 28 | return store.ready(function() { 29 | store.models.should.have.keys( 30 | 'user', 31 | 'post', 32 | 'thread', 33 | 'avatar', 34 | 'unreadpost', 35 | 'polything' 36 | ) 37 | }) 38 | }) 39 | 40 | it('Model attributes are loaded', function() { 41 | return store.ready(function() { 42 | store.models.user.definition.attributes.should.have.keys( 43 | 'id', 44 | 'login', 45 | 'email', 46 | 'created_at' 47 | ) 48 | }) 49 | }) 50 | 51 | it('Custom overwrites work', function() { 52 | return store.ready(function() { 53 | store.models.avatar.test.should.be.a.Function() 54 | }) 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/json.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | var store = this 4 | 5 | this.addType( 6 | 'json', 7 | { 8 | output: function(val) { 9 | if (!val) return {} 10 | return val 11 | }, 12 | 13 | read: function(val) { 14 | if (val instanceof Object) return val 15 | if (val instanceof String) return JSON.parse(val) 16 | if (val === null) return null 17 | 18 | return val 19 | }, 20 | 21 | write: function(object) { 22 | if (object === null) return null 23 | if (typeof object === 'string') return object 24 | 25 | return JSON.stringify(object) 26 | } 27 | }, 28 | { 29 | array: true, 30 | migration: ['json', 'jsonb'], 31 | operators: { 32 | defaults: ['eq'], 33 | 34 | eq: { 35 | defaultMethod: function(attr, value, query, cond) { 36 | var key = Object.keys(value)[0] 37 | value = value[key] 38 | query.whereRaw(attr + "->>'" + key + "' = ?", [value]) 39 | } 40 | } 41 | }, 42 | 43 | sorter: function(name) { 44 | var tmp = name.match(/(.+)\.([a-zA-Z_-]+)$/) 45 | if (tmp) { 46 | return store.connection.raw(tmp[1] + "->>'" + tmp[2] + "'") 47 | } 48 | return name 49 | } 50 | } 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/base/context.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MODEL 3 | */ 4 | exports.model = { 5 | /** 6 | * Adds a context object to your Model which could be used by your Hooks, Validation or Events via `this.context` 7 | * This is especially usefull need to differentiate things based on e.g. the cookie. Just set the context to the current request (`Model.setContext(req).create(params))` and use `this.context` inside your `beforeCreate()` hook. 8 | * The `context` Variable is available on your Model an all it's Records. 9 | * 10 | * @class Model 11 | * @method setContext 12 | * @param {object} context - Your context object 13 | * 14 | * @return {Model} 15 | */ 16 | setContext: function(context) { 17 | var self = this.chain() 18 | 19 | self.setInternal('context', context) 20 | 21 | return self 22 | } 23 | } 24 | 25 | /* 26 | * CHAIN 27 | */ 28 | exports.chain = { 29 | mixinCallback: function() { 30 | this.__defineGetter__('context', function() { 31 | const parentRecord = this.getInternal('relation_to') 32 | var context = this.getInternal('context') 33 | 34 | if (context) return context 35 | 36 | // otherwise ask it's parent record 37 | if (parentRecord) return parentRecord.context 38 | }) 39 | } 40 | } 41 | 42 | /* 43 | * RECORD 44 | */ 45 | exports.record = { 46 | mixinCallback: function() { 47 | this.__defineGetter__('context', function() { 48 | if (this.__chainedModel) return this.__chainedModel.context 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/stores/postgres/data_types/tsvector.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | var store = this 4 | 5 | this.addType( 6 | 'tsvector', 7 | { 8 | output: function(val) { 9 | if (!val) return {} 10 | return val 11 | }, 12 | 13 | read: function(value) { 14 | var obj = {} 15 | 16 | // quick and dirty tsvector to object converter 17 | const token = value.match(/('.+?'(:([0-9A-D]|,)+|))/g) 18 | 19 | if(token){ 20 | token.forEach(function(v){ 21 | var txt = v.replace(/('(.+?)'(:([0-9A-D]|,)+|))/, '$2') 22 | obj[txt] = v.replace(/('(.+?)'(:([0-9A-D]|,)+|))/, '$3').split(',') 23 | obj[txt][0] = obj[txt][0].replace(':', '') 24 | if(obj[txt][0] === '') obj[txt] = [] 25 | }) 26 | }else{ 27 | obj[token] = [] 28 | } 29 | 30 | return obj 31 | }, 32 | 33 | write: function(value) { 34 | return store.connection.raw("to_tsvector(?)", [value]) 35 | } 36 | }, 37 | { 38 | array: true, 39 | migration: ['tsvector'], 40 | operators: { 41 | default: ['matches'], 42 | 43 | matches: { 44 | defaultMethod: function(attr, value, query, cond) { 45 | query.whereRaw(attr + "@@ to_tsquery(?)", [value]) 46 | } 47 | } 48 | } 49 | } 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/base/events-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../../store/base') 4 | 5 | describe('Events', function() { 6 | var store = new Store() 7 | 8 | before(function() { 9 | return store.ready() 10 | }) 11 | 12 | store.Model('User', function() { 13 | var self = this 14 | 15 | describe('Definition', function() { 16 | describe('emit()', function() { 17 | it('methods exists', function() { 18 | self.emit.should.be.a.Function() 19 | self.on.should.be.a.Function() 20 | }) 21 | }) 22 | }) 23 | 24 | self.attribute('login', String) 25 | 26 | self.on('record_to_definition_test_event', function(arg1, arg2, done) { 27 | this.login.should.be.equal('phil') 28 | arg1.should.be.equal('argument1') 29 | arg2.should.be.equal('argument2') 30 | done() 31 | }) 32 | }) 33 | 34 | describe('Model', function() { 35 | describe('emit()', function() { 36 | it('does not exists', function() { 37 | var User = store.Model('User') 38 | should.not.exist(User.emit) 39 | should.not.exist(User.on) 40 | }) 41 | }) 42 | }) 43 | 44 | describe('Record', function() { 45 | describe('emit()', function() { 46 | it('does not exists', function() { 47 | var User = store.Model('User') 48 | var record = new User() 49 | 50 | should.not.exist(record.emit) 51 | should.not.exist(record.on) 52 | }) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /lib/stores/ldap/destroy.js: -------------------------------------------------------------------------------- 1 | const DeleteTreeControl = require('./controls/tree_delete') 2 | 3 | /* 4 | * RECORD 5 | */ 6 | exports.record = { 7 | /** 8 | * Destroy a record 9 | * @class Record 10 | * @method destroy 11 | * 12 | * @callback 13 | * @param {boolean} result - will be true if the destroy was successful 14 | * @this Record 15 | * 16 | * @return {Record} 17 | */ 18 | destroy: function(options) { 19 | var self = this 20 | 21 | options = options || {} 22 | 23 | return self 24 | .callInterceptors('beforeDestroy', [self, options]) 25 | .then(function() { 26 | return new Promise(function(resolve, reject) { 27 | self.model.definition.store.connection.del( 28 | self.dn, 29 | options.controls || [], 30 | function(err) { 31 | if (err) 32 | return reject( 33 | self.definition.store.convertLdapErrorToValidationError( 34 | self, 35 | err 36 | ) 37 | ) 38 | 39 | resolve(self.callInterceptors('afterDestroy', [self, options])) 40 | } 41 | ) 42 | }) 43 | }) 44 | .then(function(record) { 45 | record.__exists = false 46 | return record 47 | }) 48 | }, 49 | 50 | destroyAll: function(resolve, reject) { 51 | return this.destroy(resolve, reject, { 52 | controls: [new DeleteTreeControl()] 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/sql/__shared/dependent_nullify-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Store = require('../../../store') 3 | 4 | module.exports = function(title, beforeFn, afterFn, storeConf) { 5 | describe(title + ': Nullify dependent', function() { 6 | var store 7 | 8 | before(beforeFn) 9 | after(function(next) { 10 | afterFn(next, store) 11 | }) 12 | 13 | before(function() { 14 | store = new Store(storeConf) 15 | 16 | store.Model('User', function() { 17 | this.hasMany('posts') 18 | this.hasMany('threads') 19 | }) 20 | store.Model('Post', function() { 21 | this.belongsTo('user') 22 | this.belongsTo('thread') 23 | }) 24 | store.Model('Thread', function() { 25 | this.belongsTo('user') 26 | this.hasMany('posts', { dependent: 'nullify' }) 27 | }) 28 | }) 29 | 30 | it('hasMany', function() { 31 | return store.ready(function() { 32 | var Thread = store.Model('Thread') 33 | var Post = store.Model('Post') 34 | 35 | return Thread.find(1) 36 | .then(function(thread) { 37 | return thread.destroy() 38 | }) 39 | .then(function() { 40 | return Post.find([1, 2]) 41 | }) 42 | .then(function(posts) { 43 | posts.length.should.be.equal(2) 44 | should.not.exist(posts[0].thread_id) 45 | should.not.exist(posts[1].thread_id) 46 | }) 47 | }) 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /test/sql/sqlite3/__helper.js: -------------------------------------------------------------------------------- 1 | var sqlite = require('sqlite3') 2 | var fs = require('fs') 3 | var async = require('async') 4 | 5 | global.beforeSQLite = function(file, sql, next) { 6 | afterSQLite(file) 7 | var db = new sqlite.Database(file) 8 | 9 | var tmp = [] 10 | for (var i in sql) { 11 | ;(function(sql) { 12 | tmp.push(function(next) { 13 | // convert mysql or postgres sql to sqlite3 format 14 | sql = sql 15 | .replace('serial primary key', 'INTEGER PRIMARY KEY AUTOINCREMENT') 16 | .replace(/VALUES\(.+\)/, function(values) { 17 | return values 18 | .replace(/'/g, '"') 19 | .replace(/true/g, '1') 20 | .replace(/false/g, '0') 21 | }) 22 | 23 | db.run(sql, function(err, result) { 24 | if (err) throw err 25 | next() 26 | }) 27 | }) 28 | })(sql[i]) 29 | } 30 | 31 | async.series(tmp, function() { 32 | db.close() 33 | next() 34 | }) 35 | } 36 | 37 | global.afterSQLite = function(file) { 38 | if (fs.existsSync(file)) fs.unlinkSync(file) 39 | } 40 | 41 | global.testSQLite = function(name, queries, prefix) { 42 | var db = name.replace('/', '_') + '_test' 43 | 44 | require('../__shared/' + name + '-test' + (prefix || ''))( 45 | 'SQL (SQLite3)', 46 | function(next) { 47 | beforeSQLite(db, queries, next) 48 | }, 49 | function(next) { 50 | afterSQLite(db) 51 | next() 52 | }, 53 | { 54 | type: 'sqlite3', 55 | file: db 56 | } 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /lib/stores/ldap/utils.js: -------------------------------------------------------------------------------- 1 | const parseDN = require('ldapjs').parseDN 2 | 3 | exports.utils = { 4 | normalizeDn: function(dn, lowercased) { 5 | if (Array.isArray(dn)) { 6 | for (var i = 0; i < dn.length; i++) { 7 | dn[i] = this.normalizeDn(dn[i]) 8 | } 9 | 10 | return dn 11 | } 12 | try { 13 | dn = parseDN(dn.toString()) 14 | .toString() 15 | .replace(/, /g, ',') 16 | if (lowercased !== false) dn = dn.toLowerCase() 17 | return dn 18 | } catch (e) { 19 | return null 20 | } 21 | }, 22 | 23 | mergeBinary: function(chain, entry, obj) { 24 | obj = obj || entry.object 25 | 26 | var attr 27 | 28 | obj.objectClass = obj.objectClass || obj.objectclass 29 | 30 | if (chain.options.polymorph) { 31 | attr = chain.definition.store.getAllAvailableAttributes(true) 32 | for (var i = 0; i < attr.length; i++) { 33 | if (entry.raw[attr[i]]) { 34 | // .raw from ldapjs 35 | obj[attr[i]] = entry.raw[attr[i]] 36 | } 37 | } 38 | } else { 39 | for (var name in chain.definition.attributes) { 40 | if (chain.definition.attributes.hasOwnProperty(name)) { 41 | attr = chain.definition.attributes[name] 42 | if (attr.type.binary && entry.raw[name]) { 43 | // .raw from ldapjs 44 | obj[name] = entry.raw[name] 45 | } 46 | } 47 | } 48 | } 49 | 50 | if (obj.dn) { 51 | obj.dn = this.normalizeDn(obj.dn) 52 | } 53 | 54 | return obj 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/base/save.js: -------------------------------------------------------------------------------- 1 | /* 2 | * RECORD 3 | */ 4 | exports.record = { 5 | /** 6 | * Save the current record 7 | * @class Record 8 | * @method save 9 | */ 10 | save: function(options) { 11 | var self = this 12 | options = options || {} 13 | 14 | if (typeof options === 'function') throw new Error('then!') 15 | return self 16 | .validate() 17 | .then(function() { 18 | return self._create_or_update(options) 19 | }) 20 | .then(function() { 21 | // eliminate changes -> we just saved it to the database! 22 | self.changes = {} 23 | }) 24 | .then(function() { 25 | return self 26 | }) 27 | }, 28 | 29 | update: function(data, options) { 30 | return this.set(data).save(options) 31 | }, 32 | 33 | _create_or_update: function() { 34 | throw new Error('not implemented') 35 | } 36 | } 37 | 38 | /* 39 | * CHAIN 40 | */ 41 | exports.chain = { 42 | save: function(options) { 43 | const self = this 44 | var records = self 45 | 46 | if (options.ignore) { 47 | records = records.filter(function(record) { 48 | return options.ignore.indexOf(record) === -1 49 | }) 50 | } 51 | 52 | return self 53 | ._runLazyOperation(options) 54 | .then(function() { 55 | return self.store.utils.parallel( 56 | records.map(function(record) { 57 | return record.save(options) 58 | }) 59 | ) 60 | }) 61 | .then(function() { 62 | self._resolved() 63 | return self 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/stores/ldap/relations/has_parent.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | hasParent: function(name, options) { 3 | options = options || {} 4 | options.from = 'parent_dn' 5 | options.to = 'dn' 6 | options.bulkFetch = false 7 | 8 | options.loadFromRecord = 9 | options.loadFromRecord || 10 | function(parentRecord, include) { 11 | if (options.model) { 12 | const query = options.model.find(parentRecord.parent_dn) 13 | if (options.conditions) query.where(options.conditions) // conditions on the relation 14 | if (options.scope) query[options.scope].apply(query, include.args) // scope on the relation 15 | if (include.conditions) query.where(include.conditions) // nested conditions via `where({relation_name: {...conditions...}})` 16 | if (include.children) query.include(include.children) // nested includes via `include({relation_name: {nested_relation_name: { ... }}})` 17 | if (include.scope) query[include.scope].apply(query, include.args) // scope defined via `include({relation_name: {$scope: 'myscope'}})` 18 | 19 | return query.then(function(result) { 20 | parentRecord.relations[name] = query // result could be a single record, so we assign the query => collection 21 | return result 22 | }) 23 | } 24 | 25 | throw new Error( 26 | 'You need to implement your own `loadFromRecord` function! (relation: ' + 27 | name + 28 | ')' 29 | ) 30 | } 31 | 32 | return this.belongsTo(name, options) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/schema/organizational_unit.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next: unable to test via travis-ci */ 2 | exports.store = { 3 | mixinCallback: function() { 4 | this.Model('OrganizationalUnit', function() { 5 | this.isContainer('ou') 6 | this.objectClass = ['top', 'organizationalUnit'] 7 | 8 | this.attribute('name', String, { emit_events: true, persistent: false }) 9 | this.attribute('description', String) 10 | this.attribute('objectGUID', 'guid', { persistent: false }) 11 | this.attribute('uSNChanged', Number, { persistent: false }) 12 | this.attribute('whenChanged', 'date', { persistent: false }) 13 | this.attribute('whenCreated', 'date', { persistent: false }) 14 | 15 | this.validatesPresenceOf('name') 16 | 17 | var self = this 18 | this.on('name_changed', function(record, oldValue, value) { 19 | if (value) { 20 | record.ou = value 21 | if (record.parent_dn) record.dn = self.dn(record) 22 | } 23 | }) 24 | 25 | this.hasChildren('ous', { 26 | model: 'OrganizationalUnit', 27 | conditions: { objectClass: 'organizationalUnit' } 28 | }) 29 | this.hasChildren('users', { 30 | model: 'User', 31 | conditions: { objectClass: 'user' } 32 | }) 33 | this.hasChildren('groups', { 34 | model: 'Group', 35 | conditions: { objectClass: 'group' } 36 | }) 37 | this.hasChildren('computers', { 38 | model: 'Computer', 39 | conditions: { objectClass: 'group' } 40 | }) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/webpack.md: -------------------------------------------------------------------------------- 1 | # Bundle your store with Webpack 2 | 3 | If you are using *sqlite3*, *postgres*, *mysql* and *oracle* OPENRECORD will automatically load all models and its attribute definitions from your database (If `autoLoad` or `autoAttributes` is active). 4 | To avoid this overhead (e.g. in a [serverless](https://serverless.com/) environment), you can use [webpack](https://webpack.js.org/) and the OPENRECORD webpack plugin to build your code with cached model and attribute definitions. 5 | 6 | In your [webpack config]() add the following plugin: 7 | 8 | ```js 9 | const OpenrecordCache = require('openrecord/webpack') 10 | const myStore = require('./store.js') 11 | 12 | module.exports = { 13 | // ... your webpack config 14 | plugins: [ 15 | new OpenrecordCache(myStore) 16 | // ... other webpack plugins 17 | ] 18 | } 19 | ``` 20 | 21 | The plugin constructor takes your store as its only input. 22 | So you have to `export` your store. e.g. 23 | 24 | ```js 25 | // ./store.js 26 | const Store = require('openrecord') 27 | const store = new Store({ 28 | //... your store config 29 | }) 30 | 31 | module.exports = store 32 | ``` 33 | 34 | !> This plugin wont work in [watch](https://webpack.js.org/configuration/watch/) mode! 35 | 36 | Your store will be initialized on build, so make sure a connection to your database could be established. 37 | After the store is ready, it will take the model and attribute definition and write it into a cache. This cache will be bundled with your source and automatically used. 38 | So your bundled store will be faster at start, because it won't query your database for schema information. -------------------------------------------------------------------------------- /test/sql/oracle/clear.sql: -------------------------------------------------------------------------------- 1 | BEGIN 2 | FOR cur_rec IN (SELECT object_name, object_type 3 | FROM user_objects 4 | WHERE object_type IN 5 | ('TABLE', 6 | 'VIEW', 7 | 'PACKAGE', 8 | 'PROCEDURE', 9 | 'FUNCTION', 10 | 'SEQUENCE', 11 | 'SYNONYM', 12 | 'PACKAGE BODY' 13 | )) 14 | LOOP 15 | BEGIN 16 | IF cur_rec.object_type = 'TABLE' 17 | THEN 18 | EXECUTE IMMEDIATE 'DROP ' 19 | || cur_rec.object_type 20 | || ' "' 21 | || cur_rec.object_name 22 | || '" CASCADE CONSTRAINTS'; 23 | ELSE 24 | EXECUTE IMMEDIATE 'DROP ' 25 | || cur_rec.object_type 26 | || ' "' 27 | || cur_rec.object_name 28 | || '"'; 29 | END IF; 30 | EXCEPTION 31 | WHEN OTHERS 32 | THEN 33 | DBMS_OUTPUT.put_line ( 'FAILED: DROP ' 34 | || cur_rec.object_type 35 | || ' "' 36 | || cur_rec.object_name 37 | || '"' 38 | ); 39 | END; 40 | END LOOP; 41 | END; 42 | / 43 | -------------------------------------------------------------------------------- /lib/stores/sql/select.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MODEL 3 | */ 4 | exports.model = { 5 | /** 6 | * Specify SQL select fields. Default: * 7 | * @class Model 8 | * @method select 9 | * @param {array} fields - The field names 10 | * 11 | * 12 | * @return {Model} 13 | */ 14 | select: function() { 15 | var self = this.chain() 16 | 17 | var args = this.definition.store.utils.args(arguments) 18 | var fields = [] 19 | fields = fields.concat.apply(fields, args) // flatten 20 | 21 | self.addInternal('select', fields) 22 | 23 | return self 24 | } 25 | } 26 | 27 | /* 28 | * DEFINITION 29 | */ 30 | exports.definition = { 31 | mixinCallback: function() { 32 | var self = this 33 | 34 | this.beforeFind(function(query) { 35 | const custom = this.getInternal('select') 36 | var select = [] 37 | var star = true 38 | 39 | if (custom) { 40 | select = custom 41 | star = false 42 | 43 | var asJson = false 44 | 45 | select.forEach(function(field, index) { 46 | // check for function calls => don't escape them! 47 | if (field.match(/(\(|\))/)) { 48 | select[index] = self.store.connection.raw(field) 49 | asJson = true 50 | } 51 | }) 52 | 53 | if (asJson) { 54 | this.asJson() 55 | this.asRaw() 56 | } else { 57 | this.setInternal('allowed_attributes', select) 58 | } 59 | } 60 | 61 | if (!star) { 62 | this.setInternal('has_selects', true) 63 | query.select(select) 64 | } 65 | 66 | return true 67 | }, -50) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/ldap/client/destroy-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Store = require('../../../lib/store') 3 | 4 | describe('LDAP Client: Destroy', function() { 5 | var store 6 | 7 | before(function() { 8 | store = new Store({ 9 | type: 'ldap', 10 | url: 'ldap://0.0.0.0:1389', 11 | base: 'dc=test', 12 | user: 'cn=root', 13 | password: 'secret', 14 | autoSave: true 15 | }) 16 | 17 | store.Model('User', function() { 18 | this.attribute('username', String) 19 | this.attribute('memberOf', 'dn_array') 20 | 21 | this.hasParent('ou') 22 | this.hasMany('groups', { container: 'children', to: 'member' }) 23 | }) 24 | 25 | store.Model('Group', function() { 26 | this.attribute('name', String) 27 | this.attribute('member', 'dn_array') 28 | 29 | this.hasParent('ou') 30 | this.belongsToMany('members', { from: 'member' }) 31 | }) 32 | 33 | store.Model('Ou', function() { 34 | this.isContainer('ou') 35 | this.attribute('name', String) 36 | }) 37 | }) 38 | 39 | it('destroys a single record', function() { 40 | return store.ready(function() { 41 | var User = store.Model('User') 42 | 43 | return User.find('cn=destroy_me, ou=destroy, dc=test').exec(function( 44 | user 45 | ) { 46 | user.username.should.be.equal('destroy_me') 47 | 48 | return user.destroy(function() { 49 | return User.find('cn=destroy_me, ou=destroy, dc=test').exec(function( 50 | user 51 | ) { 52 | should.not.exists(user) 53 | }) 54 | }) 55 | }) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /lib/stores/sql/exec.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('openrecord:exec') 2 | 3 | exports.definition = { 4 | mixinCallback: function() { 5 | const Store = require('../../store') 6 | 7 | // var self = this 8 | 9 | this.onFind(function(query, data) { 10 | if (data.result) return 11 | 12 | // measure just the sql execution time 13 | const startTime = process.hrtime() 14 | 15 | return query 16 | .then(function(response) { 17 | const endTime = process.hrtime(startTime) 18 | const ms = 19 | parseInt((endTime[0] * 1e9 + endTime[1]) / 1000000 * 100) / 100 20 | debug('[' + ms + 'ms] ' + query.toString()) 21 | 22 | data.result = response 23 | }) 24 | .catch(function(error) { 25 | debug(query.toString()) 26 | data.error = new Store.SQLError(error) 27 | throw data.error 28 | }) 29 | }) 30 | } 31 | } 32 | 33 | /* 34 | * MODEL 35 | */ 36 | exports.model = { 37 | getExecOptions: function() { 38 | return this.query() 39 | }, 40 | 41 | toSql: function(callback) { 42 | var sql 43 | var query = this.query() 44 | 45 | if (typeof callback !== 'function') return 46 | 47 | // make async? 48 | return this.callInterceptors('beforeFind', [query]) 49 | .then(function() { 50 | sql = query.toString() 51 | 52 | if (process.env.NODE_ENV === 'test') { 53 | sql = sql 54 | .replace(/`/g, '"') 55 | .replace(/'(\d+)'/g, '$1') 56 | .replace(/ as /g, ' ') 57 | } 58 | 59 | return sql 60 | }) 61 | .then(callback) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/base/operators.js: -------------------------------------------------------------------------------- 1 | /* 2 | * STORE 3 | */ 4 | exports.store = { 5 | mixinCallback: function() { 6 | this.operatorTypes = {} 7 | this.operatorDefault = null 8 | }, 9 | 10 | // register global operators - could be overwritten per data type 11 | addOperator: function(name, fn, options) { 12 | /* istanbul ignore next */ 13 | if (!name || typeof name !== 'string') throw new Error('No name given') 14 | 15 | if (typeof fn === 'object') { 16 | options = fn 17 | fn = null 18 | } 19 | 20 | options = options || {} 21 | name = name.toLowerCase() 22 | 23 | /* istanbul ignore next */ 24 | if (this.operatorTypes[name]) 25 | throw new Error("Operator '" + name + "' already exists") 26 | 27 | if (typeof fn === 'function') options.defaultMethod = fn 28 | 29 | if (options.on && options.on.all === undefined) options.on.all = true 30 | 31 | this.operatorTypes[name] = options 32 | if (options.default) this.operatorDefault = name 33 | }, 34 | 35 | /* istanbul ignore next */ 36 | getOperator: function(name) { 37 | if (typeof name === 'string') name = name.toLowerCase() 38 | return this.operatorTypes[name] || this.operatorTypes[this.operatorDefault] 39 | }, 40 | 41 | appendOperator: function(type, operator) { 42 | /* istanbul ignore next */ 43 | if (!this.attributeTypes[type]) 44 | throw new Error("Can't find type '" + type + "'") 45 | /* istanbul ignore next */ 46 | if (!this.operatorTypes[operator]) 47 | throw new Error("Can't find operator '" + operator + "'") 48 | 49 | this.attributeTypes[type].operators[operator] = this.operatorTypes[operator] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/stores/rest/destroy.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('openrecord:destroy') 2 | 3 | /* 4 | * RECORD 5 | */ 6 | exports.record = { 7 | /** 8 | * Destroy a record 9 | * @class Record 10 | * @method destroy 11 | * 12 | * @callback 13 | * @param {boolean} result - will be true if the destroy was successful 14 | * @this Record 15 | * 16 | * @return {Promise} 17 | */ 18 | destroy: function() { 19 | const Utils = this.definition.store.utils 20 | var self = this 21 | var primaryKeys = this.definition.primaryKeys 22 | 23 | var options = Utils.clone(self.definition.actions['destroy']) 24 | 25 | for (var i = 0; i < primaryKeys.length; i++) { 26 | options.params[primaryKeys[i]] = this[primaryKeys[i]] 27 | } 28 | 29 | Utils.applyParams(options) 30 | 31 | return self 32 | .callInterceptors('beforeDestroy', [self, options]) 33 | .then(function() { 34 | return self.model.definition.store.connection.delete(options.url) 35 | }) 36 | .then(function(result) { 37 | debug('delete ' + options.url) 38 | // var validationErrors = obj[self.errorParam || 'error'] 39 | // if(typeof validationErrors === 'object' && Object.keys(validationErrors) > 0){ 40 | // this.errors.set(validationErrors) 41 | // return resolve(false) 42 | // } 43 | var data = result.data[this.rootParam || 'data'] 44 | 45 | if (data) { 46 | self.set(data) 47 | } 48 | 49 | return self.callInterceptors('afterDestroy', [self, options]) 50 | }) 51 | .then(function(record) { 52 | record.__exists = false 53 | return record 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/graphql/data_types.js: -------------------------------------------------------------------------------- 1 | exports.store = { 2 | mixinCallback: function() { 3 | var store = this 4 | store.generateGraphQLTypes() 5 | }, 6 | 7 | /* 8 | GraphQLBoolean 9 | GraphQLEnumType 10 | GraphQLFloat 11 | GraphQLID 12 | GraphQLInputObjectType 13 | GraphQLInt 14 | GraphQLInterfaceType 15 | GraphQLList 16 | GraphQLNonNull 17 | GraphQLObjectType 18 | GraphQLScalarType 19 | GraphQLSchema 20 | GraphQLString 21 | GraphQLUnionType 22 | */ 23 | generateGraphQLTypes: function() { 24 | var store = this 25 | Object.keys(this.attributeTypes).forEach(function(key) { 26 | var typeDef = store.attributeTypes[key] 27 | var name = typeDef.name 28 | var type 29 | var isList = false 30 | 31 | if (/^\w+_array$/.test(name)) { 32 | name = name.replace('_array', '') 33 | isList = true 34 | } 35 | 36 | switch (name) { 37 | case String: 38 | case 'string': 39 | case 'text': 40 | type = 'String' 41 | break 42 | 43 | case 'integer': 44 | type = 'Int' 45 | break 46 | 47 | case Number: 48 | case 'float': 49 | type = 'Float' 50 | break 51 | 52 | case Boolean: 53 | case 'boolean': 54 | type = 'Boolean' 55 | break 56 | 57 | case Array: 58 | isList = true 59 | type = 'String' // TODO: Remove in 3.0 ?! Does not make any sense?! 60 | break 61 | 62 | default: 63 | type = 'String' 64 | } 65 | 66 | if (isList) { 67 | type = '[' + type + ']' 68 | } 69 | 70 | typeDef.graphQLTypeName = type 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/definition-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../lib/store') 4 | var Definition = require('../lib/definition') 5 | 6 | describe('Definition: Base', function() { 7 | var store = new Store() 8 | 9 | var User, Post 10 | 11 | store.Model('User', function() { 12 | var self = this 13 | 14 | it('has the correct context', function() { 15 | self.should.be.an.instanceOf(Definition) 16 | }) 17 | 18 | it('has validates() mixin method', function() { 19 | should.exist(self.validates) 20 | self.validates.should.be.a.Function() 21 | }) 22 | 23 | it('has setter() mixin method', function() { 24 | should.exist(self.setter) 25 | self.setter.should.be.a.Function() 26 | }) 27 | 28 | it('has getter() mixin method', function() { 29 | should.exist(self.getter) 30 | self.getter.should.be.a.Function() 31 | }) 32 | 33 | it('has event methods', function() { 34 | should.exist(self.on) 35 | self.on.should.be.a.Function() 36 | 37 | should.exist(self.emit) 38 | self.emit.should.be.a.Function() 39 | 40 | should.exist(self._events) 41 | }) 42 | }) 43 | 44 | store.Model('Post', function() { 45 | this.validatesPresenceOf('title') 46 | }) 47 | 48 | before(function() { 49 | return store.ready(function() { 50 | User = store.Model('User') 51 | Post = store.Model('Post') 52 | }) 53 | }) 54 | 55 | it('every model has it own definition', function() { 56 | User.definition.should.not.be.equal(Post.definition) 57 | }) 58 | 59 | it('has no shared variables between models', function() { 60 | User.definition.validations.should.not.be.equal(Post.definition.validations) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /lib/stores/sql/transaction.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('openrecord:transaction') 2 | 3 | /* 4 | * MODEL 5 | */ 6 | exports.model = { 7 | /** 8 | * Add the current query into a transaction 9 | * 10 | * @class Model 11 | * @method transaction 12 | * @param {object} transaction - The transaction object 13 | * 14 | * @return {Collection} 15 | */ 16 | useTransaction: function(transaction) { 17 | var self = this.chain() 18 | 19 | self.setInternal('transaction', transaction) 20 | 21 | return self 22 | }, 23 | 24 | getExecOptions: function() { 25 | var transaction = this.getInternal('transaction') 26 | 27 | return this.query({ transaction: transaction }) 28 | } 29 | } 30 | 31 | /* 32 | * RECORD 33 | */ 34 | exports.store = { 35 | /** 36 | * TODO:... write documentation 37 | * 38 | * @class Record 39 | * @method transaction 40 | * @param {object} transaction - The transaction object 41 | * 42 | */ 43 | startTransaction: function(options, callback) { 44 | // var self = this 45 | 46 | if (typeof options === 'function') { 47 | callback = options 48 | options = {} 49 | } 50 | 51 | if (options.transaction) { 52 | return callback(options.transaction) 53 | } else { 54 | return this.connection 55 | .transaction(function(transaction) { 56 | options.transaction = transaction 57 | return callback(transaction) 58 | }) 59 | .then(function(result) { 60 | debug('commit') 61 | return result 62 | }) 63 | .catch(function(e) { 64 | // TODO: do something with e.g. 'afterSave' rollback message!?! 65 | debug('rollback') 66 | throw e 67 | }) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/sql/__shared/autojoin-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Store = require('../../../store') 3 | 4 | module.exports = function(title, beforeFn, afterFn, storeConf) { 5 | describe(title + ': AutoJoin', function() { 6 | var store 7 | 8 | before(beforeFn) 9 | after(function(next) { 10 | afterFn(next, store) 11 | }) 12 | 13 | before(function() { 14 | store = new Store(storeConf) 15 | 16 | store.Model('User', function() { 17 | this.hasMany('posts') 18 | this.hasMany('threads') 19 | this.autoJoin() 20 | }) 21 | store.Model('Post', function() { 22 | this.belongsTo('user') 23 | this.belongsTo('thread') 24 | this.autoJoin() 25 | }) 26 | store.Model('Thread', function() { 27 | this.belongsTo('user') 28 | this.hasMany('posts') 29 | this.autoJoin() 30 | }) 31 | }) 32 | 33 | describe('autoJoin()', function() { 34 | it('returns the right results on nested joins with nested conditions', function() { 35 | return store.ready(function() { 36 | var Thread = store.Model('Thread') 37 | return Thread.where( 38 | { posts: { user: { login_like: 'phi' } } }, 39 | { title_like: 'first' } 40 | ) 41 | .order('title', 'posts_user.id') 42 | .exec(function(result) { 43 | result[0].title.should.be.equal('first thread') 44 | result[0]._posts.length.should.be.equal(2) 45 | result[0]._posts[0]._user.login.should.be.equal('phil') 46 | result[0]._posts[1]._user.login.should.be.equal('phil') 47 | should.not.exist(result[1]) 48 | }) 49 | }) 50 | }) 51 | }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /test/sql/aggregate_function-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../store/sql') 2 | 3 | describe('SQL: Aggregate Functions', function() { 4 | var store 5 | var User 6 | 7 | before(function() { 8 | store = new Store({ 9 | type: 'sql' 10 | }) 11 | 12 | store.Model('User', function() { 13 | this.attribute('salary', Number) 14 | }) 15 | 16 | return store.ready(function() { 17 | User = store.Model('User') 18 | }) 19 | }) 20 | 21 | describe('count()', function() { 22 | it('has method', function() { 23 | User.count.should.be.a.Function() 24 | }) 25 | 26 | it('has the right internal variables', function() { 27 | var Chained = User.count('salary') 28 | Chained.getInternal('count').should.be.equal('salary') 29 | }) 30 | }) 31 | 32 | describe('sum()', function() { 33 | it('has method', function() { 34 | User.sum.should.be.a.Function() 35 | }) 36 | 37 | it('has the right internal variables', function() { 38 | var Chained = User.sum('salary') 39 | Chained.getInternal('sum').should.be.equal('salary') 40 | }) 41 | }) 42 | 43 | describe('max()', function() { 44 | it('has method', function() { 45 | User.max.should.be.a.Function() 46 | }) 47 | 48 | it('has the right internal variables', function() { 49 | var Chained = User.max('salary') 50 | Chained.getInternal('max').should.be.equal('salary') 51 | }) 52 | }) 53 | 54 | describe('min()', function() { 55 | it('has method', function() { 56 | User.min.should.be.a.Function() 57 | }) 58 | 59 | it('has the right internal variables', function() { 60 | var Chained = User.min('salary') 61 | Chained.getInternal('min').should.be.equal('salary') 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /test/rest/client/includes-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../lib/store') 2 | 3 | describe('REST Client: Include', function() { 4 | var store 5 | 6 | before(function() { 7 | store = new Store({ 8 | type: 'rest', 9 | url: 'http://localhost:8889', 10 | version: '~1.0' 11 | }) 12 | 13 | store.Model('User', function() { 14 | this.attribute('id', Number, { primary: true }) 15 | this.attribute('login', String) 16 | this.attribute('email', String) 17 | 18 | this.hasMany('posts') 19 | }) 20 | 21 | store.Model('Post', function() { 22 | this.attribute('id', Number, { primary: true }) 23 | this.attribute('message', String) 24 | this.attribute('user_id', Number) 25 | this.attribute('thread_id', Number) 26 | 27 | this.belongsTo('user') 28 | }) 29 | }) 30 | 31 | it('includes a hasMany relation', function() { 32 | return store.ready(function() { 33 | var User = store.Model('User') 34 | 35 | return User.include('posts').exec(function(results) { 36 | results.length.should.be.above(3) 37 | results[0].posts.length.should.be.equal(3) 38 | results[1].posts.length.should.be.equal(1) 39 | results[2].posts.length.should.be.equal(0) 40 | }) 41 | }) 42 | }) 43 | 44 | it('includes a belongsTo relation', function() { 45 | return store.ready(function() { 46 | var Post = store.Model('Post') 47 | 48 | return Post.include('user').exec(function(results) { 49 | results.length.should.be.equal(5) 50 | results[0]._user.id.should.be.equal(1) 51 | results[1]._user.id.should.be.equal(1) 52 | results[2]._user.id.should.be.equal(1) 53 | results[3]._user.id.should.be.equal(2) 54 | }) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/sql/postgres/uuid_key-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Store = require('../../../store/postgres') 3 | 4 | describe('Postgres: UUID Key', function() { 5 | var store 6 | var database = 'uuid_key_test' 7 | 8 | before(function(next) { 9 | this.timeout(5000) 10 | beforePG( 11 | database, 12 | [ 13 | 'CREATE EXTENSION IF NOT EXISTS \\"uuid-ossp\\"', 14 | 'CREATE TABLE uuid_tests (id uuid not null primary key default uuid_generate_v1(), another_column varchar(255))' 15 | ], 16 | next 17 | ) 18 | }) 19 | 20 | before(function() { 21 | store = new Store({ 22 | host: 'localhost', 23 | type: 'postgres', 24 | database: database, 25 | user: 'postgres', 26 | password: '' 27 | }) 28 | 29 | store.Model('UuidTest', function() {}) 30 | }) 31 | 32 | after(function(next) { 33 | afterPG(database, next) 34 | }) 35 | 36 | it('attribute id is type uuid', function() { 37 | return store.ready(function() { 38 | var UuidTest = store.Model('UuidTest') 39 | UuidTest.definition.attributes.id.type.name.should.be.equal('uuid') 40 | }) 41 | }) 42 | 43 | it('new() returns a null id (uuid)', function() { 44 | return store.ready(function() { 45 | var UuidTest = store.Model('UuidTest') 46 | var test = new UuidTest({ another_column: 'i am setting uuid' }) 47 | should.not.exist(test.id) 48 | }) 49 | }) 50 | 51 | it('create() returns a new id (uuid)', function() { 52 | return store.ready(function() { 53 | var UuidTest = store.Model('UuidTest') 54 | var test = new UuidTest({ another_column: 'i am setting uuid' }) 55 | 56 | return test.save().then(function() { 57 | should.exist(test.id) 58 | }) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ![OpenRecord](logo.png) 2 | 3 | ## A powerful but simple to use ORM for nodejs. 4 | The primary goal is to offer a clean and easy to use API to your datastore. No matter if you want to connect to Postgres, MySQL or an ActiveDirectory. The API should be similar, but supports all the important features of your database! 5 | 6 | As the name implies, it's open and very easy to extend. The whole package was built [that way](./plugins.md#plugins). 7 | 8 | Currently it supports the following databases/datastores: SQLite3, MySQL, Postgres, Oracle, REST and LDAP (+ ActiveDirectory) 9 | If you want to build a GraphQL endpoint for any of these databases, OPENRECORD has some [built in features](./graphql.md) to support you! 10 | 11 | [Get started](./install.md) 12 | 13 | # Why? 14 | 15 | There are a lot of different ORMs (or similar) in the nodejs ecosystem to choose from. So why OPENRECORD? 16 | This project started many years ago - when the current version of nodejs was < 1.0. At that time there were a handful of ORMs available. 17 | From my point of view, they all had a terrible API and were lacking some important features. Most of them tried to support many different databases and therefore important database specific features were missing. 18 | Supporting different databases if not a bad thing, but the ORM should embrace the differences, instead of unifying them all under a common denominator. 19 | 20 | OPENRECORD was inspired by [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html), but ultimately has some unique touches on certain areas. 21 | 22 | It's used in production since 2014 for many projects and even powers an application which manages an ActiveDirectory with approximately 10.000 computers and 30.000 - 40.000 users. 23 | Although most of the work was invested into supporting relational databases like Postgres or MySQL. -------------------------------------------------------------------------------- /test/graphql/context-test.js: -------------------------------------------------------------------------------- 1 | // var should = require('should') 2 | 3 | var types = ['auto', 'custom'] 4 | 5 | types.forEach(function(type) { 6 | describe('GraphQL: Context (' + type + ')', function() { 7 | var database = 'context' + type 8 | var query 9 | 10 | before(function(next) { 11 | beforeGraphQL(database, type, function(error, _query) { 12 | query = _query 13 | next(error) 14 | }) 15 | }) 16 | 17 | after(function(next) { 18 | afterGraphQL(database, next) 19 | }) 20 | 21 | it('get `me` via context', function() { 22 | return query( 23 | `{ 24 | me{ 25 | name 26 | email 27 | } 28 | }`, 29 | { id: 1 } 30 | ).then(function(result) { 31 | result.should.be.eql({ 32 | data: { 33 | me: { name: 'phil', email: 'phil@mail.com' } 34 | } 35 | }) 36 | }) 37 | }) 38 | 39 | it('get `me` via context and return additonal relations', function() { 40 | return query( 41 | `{ 42 | me{ 43 | name 44 | email 45 | recipes{ 46 | totalCount 47 | nodes{ 48 | title 49 | } 50 | } 51 | } 52 | }`, 53 | { id: 1 } 54 | ).then(function(result) { 55 | result.should.be.eql({ 56 | data: { 57 | me: { 58 | name: 'phil', 59 | email: 'phil@mail.com', 60 | recipes: { 61 | nodes: [ 62 | { title: 'Toast Hawaii' }, 63 | { title: 'scrambled eggs' }, 64 | { title: 'Steak' } 65 | ], 66 | totalCount: 3 67 | } 68 | } 69 | } 70 | }) 71 | }) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /test/rest/client/create-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Store = require('../../../lib/store') 3 | 4 | describe('REST Client: Create', function() { 5 | var store 6 | 7 | before(function() { 8 | store = new Store({ 9 | type: 'rest', 10 | url: 'http://localhost:8889', 11 | version: '~1.0', 12 | autoSave: true 13 | }) 14 | 15 | store.Model('User', function() { 16 | this.attribute('id', Number, { primary: true }) 17 | this.attribute('login', String) 18 | this.attribute('email', String) 19 | 20 | this.hasMany('posts') 21 | }) 22 | 23 | store.Model('Post', function() { 24 | this.attribute('id', Number, { primary: true }) 25 | this.attribute('message', String) 26 | this.attribute('user_id', Number) 27 | this.attribute('thread_id', Number) 28 | 29 | this.belongsTo('user') 30 | }) 31 | }) 32 | 33 | it('creates a new record (create)', function() { 34 | return store.ready(function() { 35 | var User = store.Model('User') 36 | 37 | return User.create({ login: 'max', email: 'max@mail.com' }).then(function( 38 | user 39 | ) { 40 | should.exist(user.id) 41 | }) 42 | }) 43 | }) 44 | 45 | it('creates nested records (create)', function() { 46 | return store.ready(function() { 47 | var User = store.Model('User') 48 | 49 | var user = User.new({ login: 'hugo', email: 'hugo@mail.com' }) 50 | 51 | user.posts.add({ 52 | message: 'hugo post', 53 | thread_id: 3 54 | }) 55 | 56 | return user.save().then(function(user) { 57 | return User.find(user.id) 58 | .include('posts') 59 | .exec(function(user) { 60 | user.posts.length.should.be.equal(1) 61 | user.posts[0].message.should.be.equal('hugo post') 62 | }) 63 | }) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /lib/base/relations/has.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | has: function(name, options) { 3 | const store = this.store 4 | options = options || {} 5 | options.type = 'has' 6 | 7 | options.preInitialize = options.preInitialize || function() {} 8 | options.initialize = options.initialize || function() {} 9 | 10 | options.loadFromConditions = options.loadFromConditions || function(){ 11 | return undefined 12 | } 13 | 14 | if(!options.loadFromRecords && !options.query) throw new Error('You need to implement a `query(store, parentRecords)` method!') 15 | 16 | options.loadFromRecords = options.loadFromRecords || function(parentCollection){ 17 | return options.query(store, parentCollection) 18 | .then(function(result) { 19 | // add the result to corresponding record 20 | parentCollection.forEach(function(record) { 21 | record.relations[name] = options.convert(record, result) 22 | }) 23 | 24 | return result 25 | }) 26 | } 27 | 28 | options.loadFromRecord = options.loadFromRecord || function(parentRecord){ 29 | return options.query(store, [parentRecord]) 30 | .then(function(result) { 31 | parentRecord.relations[name] = options.convert(parentRecord, result) 32 | return result 33 | }) 34 | } 35 | 36 | if(!options.filterCache && !options.convert) throw new Error('You need to implement a `convert(parent, results)` method!') 37 | 38 | options.filterCache = options.filterCache || function(parentRecord, records){ 39 | return options.convert(parentRecord, records) 40 | } 41 | 42 | options.collection = options.collection || function(parentRecord){ 43 | return options.convert(parentRecord, null) 44 | } 45 | 46 | options.getter = options.getter || function() { 47 | return this['_' + name] 48 | } 49 | 50 | 51 | return this.relation(name, options) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | If you use OPENRECORD in a web application or similar, you most probably want to have access to some request information (e.g. the user session). 4 | Instead of calling a [scope](./definition#scopes) with this information it's better to use `setContext(context)`. OPENRECORD will provide you with the context everywhere you need it. 5 | 6 | Here is a very basic example: 7 | ```js 8 | // ./models(Post) 9 | module.exports = function(){ 10 | this.beforeUpdate(function(){ 11 | // here we use the `this.context`. 12 | // `this` is the record 13 | // check if the user is an admin or the creator of that post 14 | if(!this.context || (this.context.role !== 'admin' && this.context.id !== this.creator_id)){ 15 | throw new Error('insufficient permissions') 16 | } 17 | }) 18 | 19 | this.beforeFind(function(){ 20 | // `this` is the model 21 | // `this.context` is also available 22 | }) 23 | } 24 | ``` 25 | 26 | ```js 27 | var myContext = {role: 'admin', id: 1} 28 | 29 | post = await Post.setContext(myContext).find(id) 30 | post.title = 'OPENRECORD!!' 31 | await post.save() 32 | ``` 33 | 34 | In the above example we call `setContext()` on the `Post` model, but the context object won't be set globally! 35 | It will instead only be available for all [hooks](./definition.md#hooks) involved in the initial find, and all corresponding records (the result)! 36 | So if you do multiple queries, you have to use `setContext` for every new query: 37 | 38 | ```js 39 | const [users, totalCount] = await Promise.all([ 40 | User.limit(10).setContext(myContext), 41 | User.count().setContext(myContext) 42 | ]) 43 | ``` 44 | 45 | If you are [reusing a query](./query.md#chaining) only one `setContext` is neccesary: 46 | ```js 47 | const query = User.setContext(myContext) 48 | const [users, totalCount] = await Promise.all([ 49 | query.limit(10), 50 | query.clone().totalCount() 51 | ]) 52 | ``` 53 | -------------------------------------------------------------------------------- /test/base/convert-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../store/base') 2 | 3 | describe('Convert', function() { 4 | describe('convertInput()', function() { 5 | var store = new Store() 6 | 7 | store.Model('User', function() { 8 | this.attribute('login', String) 9 | this.attribute('other', String) 10 | 11 | this.convertInput('login', function(value) { 12 | return '$' + value 13 | }) 14 | }) 15 | 16 | var User, user 17 | before(function() { 18 | return store.ready(function() { 19 | User = store.Model('User') 20 | user = new User({ login: 'admin', other: 'foo' }) 21 | }) 22 | }) 23 | 24 | it('converts the value accordingly', function() { 25 | user.login.should.be.equal('$admin') 26 | }) 27 | 28 | it('converts only the specified value', function() { 29 | user.other.should.be.equal('foo') 30 | }) 31 | }) 32 | 33 | describe('convertOutput()', function() { 34 | var store = new Store() 35 | 36 | store.Model('User', function() { 37 | this.attribute('login', String) 38 | this.attribute('other', String) 39 | 40 | this.convertOutput('login', function(value) { 41 | return '$' + value 42 | }) 43 | }) 44 | 45 | var User, user 46 | before(function() { 47 | return store.ready(function() { 48 | User = store.Model('User') 49 | user = new User({ login: 'admin', other: 'foo' }) 50 | }) 51 | }) 52 | 53 | it('converts the value accordingly', function(done) { 54 | user.toJson().login.should.be.equal('$admin') 55 | done() 56 | }) 57 | 58 | it('converts only the specified value', function(done) { 59 | user.toJson().other.should.be.equal('foo') 60 | done() 61 | }) 62 | 63 | it('does not change the original value', function(done) { 64 | user.attributes.login.should.be.equal('admin') 65 | done() 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /lib/base/every.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CHAIN 3 | */ 4 | exports.chain = { 5 | mixinCallback: function() { 6 | /** 7 | * You could call `.every` on every Collection of records to get a special Record (PseudoRecord). 8 | * This PseudoRecord has all the properties and methods a normal Record of this Model has, but it will behave different 9 | * E.g. getting an attributes value `Collection.every.id` will return an array of all record's ids. 10 | * The same will work with setting an attribute or calling a method. 11 | * 12 | * @class Collection 13 | * @name .every 14 | * 15 | * @return {PseudoRecord} 16 | */ 17 | 18 | // returns a pseudo record 19 | this.__defineGetter__('every', function() { 20 | var self = this 21 | var pseudo = new this.model() // eslint-disable-line 22 | 23 | for (var name in pseudo) { 24 | ;(function(name, value) { 25 | if (typeof value === 'function') { 26 | // replace methods 27 | pseudo[name] = function() { 28 | for (var i = 0; i < self.length; i++) { 29 | var record = self[i] 30 | record[name].apply(record, arguments) 31 | } 32 | } 33 | } else { 34 | // replace attribute 35 | pseudo.__defineGetter__(name, function() { 36 | var tmp = [] 37 | for (var i = 0; i < self.length; i++) { 38 | var record = self[i] 39 | tmp.push(record[name]) 40 | } 41 | return tmp 42 | }) 43 | 44 | pseudo.__defineSetter__(name, function(value) { 45 | for (var i = 0; i < self.length; i++) { 46 | var record = self[i] 47 | record[name] = value 48 | } 49 | }) 50 | } 51 | })(name, pseudo[name]) 52 | } 53 | 54 | return pseudo 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/stores/ldap/attributes.js: -------------------------------------------------------------------------------- 1 | const parseDN = require('ldapjs').parseDN 2 | 3 | /* 4 | * DEFINITION 5 | */ 6 | exports.definition = { 7 | mixinCallback: function() { 8 | var self = this 9 | 10 | this.attribute('dn', 'dn', { emit_events: true }) // Well... it's not a realy primary key, but it's used internaly for relations... 11 | this.attribute('parent_dn', 'dn', { persistent: false, emit_events: true }) 12 | this.attribute('objectClass', 'object_class', { default: self.objectClass }) 13 | self.use(function() { 14 | // delay! 15 | this.attribute(self.rdn_prefix, String, { persistent: false }) 16 | }) 17 | 18 | this.on('parent_dn_changed', function(record, oldValue, value) { 19 | if (value) { 20 | record.dn = self.dn(record) 21 | } 22 | }) 23 | 24 | this.on('dn_changed', function(record, oldValue, value) { 25 | if (value) { 26 | record.attributes.parent_dn = self.store.utils.normalizeDn( 27 | parseDN(value) 28 | .parent() 29 | .toString() 30 | ) // sets the parent_dn, but we don't want to fire a change event 31 | } 32 | }) 33 | 34 | // add all attribute names to the search attributes 35 | this.beforeFind(function(options) { 36 | options.attributes = Object.keys(this.definition.attributes) 37 | }, 90) 38 | } 39 | } 40 | 41 | exports.store = { 42 | getAllAvailableAttributes: function(binaryOnly) { 43 | var tmp = [] 44 | 45 | for (var i in this.models) { 46 | if (this.models[i].definition) { 47 | for (var name in this.models[i].definition.attributes) { 48 | if (this.models[i].definition.attributes.hasOwnProperty(name)) { 49 | if (!binaryOnly || (this.models[i].definition.attributes[name].type.binary)) { 50 | tmp.push(name) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | return this.utils.uniq(tmp) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/sql/__shared/plugins/paranoid-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Store = require('../../../../store') 3 | 4 | module.exports = function(title, beforeFn, afterFn, storeConf) { 5 | describe(title + ': Paranoid', function() { 6 | var store 7 | 8 | before(beforeFn) 9 | after(function(next) { 10 | afterFn(next, store) 11 | }) 12 | 13 | before(function() { 14 | store = new Store(storeConf) 15 | 16 | store.Model('User', function() { 17 | this.paranoid() 18 | }) 19 | }) 20 | 21 | it('returns only records with deleted_at IS NULL', function() { 22 | return store.ready(function() { 23 | var User = store.Model('User') 24 | return User.where({ login_not: 'hans' }).exec(function(records) { 25 | records.length.should.be.equal(2) 26 | }) 27 | }) 28 | }) 29 | 30 | it('returns all records with withDeleted() scope', function() { 31 | return store.ready(function() { 32 | var User = store.Model('User') 33 | return User.where({ login_not: 'hans' }) 34 | .withDeleted() 35 | .exec(function(records) { 36 | records.length.should.be.equal(4) 37 | }) 38 | }) 39 | }) 40 | 41 | it('"deletes" a record', function() { 42 | return store.ready(function() { 43 | var User = store.Model('User') 44 | return User.find(5).exec(function(hans) { 45 | return hans.destroy().then(function() { 46 | return User.find(5).exec(function(delHans) { 47 | should.not.exist(delHans) 48 | 49 | return User.find(5) 50 | .withDeleted() 51 | .exec(function(existingHans) { 52 | existingHans.login.should.be.equal('hans') 53 | should.exist(existingHans.deleted_at) 54 | }) 55 | }) 56 | }) 57 | }) 58 | }) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/data_types/guid.js: -------------------------------------------------------------------------------- 1 | const ldap = require('ldapjs') 2 | /* istanbul ignore next: unable to test via travis-ci */ 3 | exports.store = { 4 | mixinCallback: function() { 5 | this.addType( 6 | 'guid', 7 | { 8 | read: function(value) { 9 | const data = Buffer.from(value, 'binary') 10 | var template = 11 | '{3}{2}{1}{0}-{5}{4}-{7}{6}-{8}{9}-{10}{11}{12}{13}{14}{15}' 12 | 13 | for (var i = 0; i < data.length; i++) { 14 | var dataStr = data[i].toString(16) 15 | dataStr = data[i] >= 16 ? dataStr : '0' + dataStr 16 | template = template.replace( 17 | new RegExp('\\{' + i + '\\}', 'g'), 18 | dataStr 19 | ) 20 | } 21 | return template 22 | }, 23 | 24 | write: function(value) { 25 | if (!value) return null 26 | 27 | value = value.replace( 28 | /(.{2})(.{2})(.{2})(.{2})-(.{2})(.{2})-(.{2})(.{2})-(.{2})(.{2})-(.{12})/, 29 | '$4$3$2$1$6$5$8$7$9$10$11' 30 | ) 31 | 32 | if (Buffer.from) return Buffer.from(value, 'hex') 33 | return new Buffer(value, 'hex') // eslint-disable-line node/no-deprecated-api 34 | // return value; //not yet supported by ldapjs 35 | // return '\\' + value.toString('hex').match(/.{2}/g).join('\\') 36 | } 37 | }, 38 | { 39 | binary: true, 40 | operators: { 41 | defaults: ['eq'], 42 | eq: { 43 | defaultMethod: function(attr, value, options) { 44 | value = this.definition.cast(attr, value, 'write', this) 45 | options.filter.filters.push( 46 | new ldap.EqualityFilter({ attribute: attr, value: value }) 47 | ) 48 | }, 49 | 50 | on: { 51 | all: true 52 | } 53 | } 54 | } 55 | } 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/sql/__shared/plugins/total_count-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../../store') 2 | 3 | module.exports = function(title, beforeFn, afterFn, storeConf) { 4 | describe(title + ': totalCount', function() { 5 | var store 6 | 7 | before(beforeFn) 8 | after(function(next) { 9 | afterFn(next, store) 10 | }) 11 | 12 | before(function() { 13 | store = new Store(storeConf) 14 | 15 | store.Model('User', function() {}) 16 | }) 17 | 18 | it('returns the total count', function() { 19 | return store.ready(function() { 20 | var User = store.Model('User') 21 | return User.totalCount().then(function(count) { 22 | count.should.be.equal(5) 23 | }) 24 | }) 25 | }) 26 | 27 | it('returns the total count by field', function() { 28 | return store.ready(function() { 29 | var User = store.Model('User') 30 | return User.totalCount('deleted_at').then(function(count) { 31 | count.should.be.equal(2) 32 | }) 33 | }) 34 | }) 35 | 36 | it('ignores invalid field names', function() { 37 | return store.ready(function() { 38 | var User = store.Model('User') 39 | return User.totalCount({}).then(function(count) { 40 | count.should.be.equal(5) 41 | }) 42 | }) 43 | }) 44 | 45 | it('resets limit', function() { 46 | return store.ready(function() { 47 | var User = store.Model('User') 48 | return User.limit(2) 49 | .totalCount() 50 | .then(function(count) { 51 | count.should.be.equal(5) 52 | }) 53 | }) 54 | }) 55 | 56 | it('resets limit (called after)', function() { 57 | return store.ready(function() { 58 | var User = store.Model('User') 59 | return User.totalCount() 60 | .limit(2) 61 | .then(function(count) { 62 | count.should.be.equal(5) 63 | }) 64 | }) 65 | }) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /test/sql/interceptors-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../../store/sql') 4 | 5 | describe('SQL: Interceptors', function() { 6 | var store = new Store({ 7 | type: 'sql' 8 | }) 9 | 10 | store.Model('User', function() { 11 | var self = this 12 | 13 | it('has beforeCreate()', function() { 14 | should.exist(self.beforeCreate) 15 | self.beforeCreate.should.be.a.Function() 16 | }) 17 | 18 | it('has afterCreate()', function() { 19 | should.exist(self.afterCreate) 20 | self.afterCreate.should.be.a.Function() 21 | }) 22 | 23 | it('has beforeUpdate()', function() { 24 | should.exist(self.beforeUpdate) 25 | self.beforeUpdate.should.be.a.Function() 26 | }) 27 | 28 | it('has afterUpdate()', function() { 29 | should.exist(self.afterUpdate) 30 | self.afterUpdate.should.be.a.Function() 31 | }) 32 | 33 | it('has beforeDestroy()', function() { 34 | should.exist(self.beforeDestroy) 35 | self.beforeDestroy.should.be.a.Function() 36 | }) 37 | 38 | it('has afterDestroy()', function() { 39 | should.exist(self.afterDestroy) 40 | self.afterDestroy.should.be.a.Function() 41 | }) 42 | 43 | it('has beforeSave()', function() { 44 | should.exist(self.beforeSave) 45 | self.beforeSave.should.be.a.Function() 46 | }) 47 | 48 | it('has afterSave()', function() { 49 | should.exist(self.afterSave) 50 | self.afterSave.should.be.a.Function() 51 | }) 52 | 53 | it('has beforeFind()', function() { 54 | should.exist(self.beforeFind) 55 | self.beforeFind.should.be.a.Function() 56 | }) 57 | 58 | it('has afterFind()', function() { 59 | should.exist(self.afterFind) 60 | self.afterFind.should.be.a.Function() 61 | }) 62 | 63 | it('has beforeValidation()', function() { 64 | should.exist(self.beforeValidation) 65 | self.beforeValidation.should.be.a.Function() 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /lib/stores/sql/plugins/stampable.js: -------------------------------------------------------------------------------- 1 | exports.migration = { 2 | stampable: function () { 3 | this.timestamp() 4 | this.userstamp() 5 | }, 6 | 7 | timestamp: function () { 8 | this.datetime('created_at', { 9 | comment: 'Time of insert', 10 | default: 'NOW()' 11 | }) 12 | this.datetime('updated_at', { 13 | comment: 'Time of last update', 14 | default: 'NOW()' 15 | }) 16 | }, 17 | 18 | userstamp: function () { 19 | this.id('creator_id', { 20 | comment: 'Identifier of the creator ' 21 | }) 22 | this.id('updater_id', { 23 | comment: 'Identifier of the last modifier' 24 | }) 25 | } 26 | } 27 | 28 | exports.definition = { 29 | stampable: function () { 30 | var self = this 31 | this.beforeSave(function () { 32 | var now = new Date() 33 | 34 | var userId = null 35 | 36 | if (typeof self.store.getUserByFn === 'function') { 37 | userId = self.store.getUserByFn(this, self) 38 | } else { 39 | if (this.context && this.context.user) { 40 | userId = this.context.user.id 41 | } 42 | } 43 | 44 | if (!this.__exists) { 45 | if (self.attributes.created_at) { 46 | this.created_at = this.created_at || now 47 | } 48 | 49 | if (self.attributes.creator_id) { 50 | this.creator_id = this.creator_id || userId 51 | } 52 | } 53 | 54 | if (this.hasChanges()) { 55 | // only set updated_at or updater_id if there are any changes 56 | if (self.attributes.updated_at && !this.hasChanged('updated_at')) { 57 | this.updated_at = now 58 | } 59 | 60 | if (self.attributes.updater_id && !this.hasChanged('updater_id')) { 61 | this.updater_id = userId || this.updater_id 62 | } 63 | } 64 | 65 | return true 66 | }) 67 | 68 | return this 69 | } 70 | } 71 | 72 | exports.store = { 73 | getUserBy: function (callback) { 74 | this.getUserByFn = callback 75 | } 76 | } -------------------------------------------------------------------------------- /test/rest/client/exec-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../lib/store') 2 | 3 | describe('REST Client: Exec', function() { 4 | var store 5 | 6 | before(function() { 7 | store = new Store({ 8 | type: 'rest', 9 | url: 'http://localhost:8889', 10 | version: '~1.0' 11 | }) 12 | 13 | store.Model('User', function() { 14 | this.attribute('id', Number, { primary: true }) 15 | this.attribute('login', String) 16 | this.attribute('email', String) 17 | }) 18 | }) 19 | 20 | it('loads records from the rest server (index)', function() { 21 | return store.ready(function() { 22 | var User = store.Model('User') 23 | 24 | return User.exec(function(results) { 25 | results.length.should.be.above(2) 26 | }) 27 | }) 28 | }) 29 | 30 | it('loads one record from the rest server (show)', function() { 31 | return store.ready(function() { 32 | var User = store.Model('User') 33 | 34 | return User.find(2).exec(function(result) { 35 | result.id.should.be.equal(2) 36 | result.login.should.be.equal('michl') 37 | }) 38 | }) 39 | }) 40 | 41 | it('loads filtered records from the rest server (index)', function() { 42 | return store.ready(function() { 43 | var User = store.Model('User') 44 | 45 | return User.where({ login: 'michl' }).exec(function(result) { 46 | result.length.should.be.equal(1) 47 | result[0].id.should.be.equal(2) 48 | result[0].login.should.be.equal('michl') 49 | }) 50 | }) 51 | }) 52 | 53 | it('loads filtered records from the rest server via promise (index)', function() { 54 | return store.ready(function() { 55 | var User = store.Model('User') 56 | 57 | return User.where({ login: 'michl' }) 58 | .exec() 59 | .then(function(result) { 60 | result.length.should.be.equal(1) 61 | result[0].id.should.be.equal(2) 62 | result[0].login.should.be.equal('michl') 63 | }) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /test/rest/client/update-test.js: -------------------------------------------------------------------------------- 1 | var Store = require('../../../lib/store') 2 | 3 | describe('REST Client: Update', function() { 4 | var store 5 | 6 | before(function() { 7 | store = new Store({ 8 | type: 'rest', 9 | url: 'http://localhost:8889', 10 | version: '~1.0', 11 | autoSave: true 12 | }) 13 | 14 | store.Model('User', function() { 15 | this.attribute('id', Number, { primary: true }) 16 | this.attribute('login', String) 17 | this.attribute('email', String) 18 | 19 | this.hasMany('posts') 20 | }) 21 | 22 | store.Model('Post', function() { 23 | this.attribute('id', Number, { primary: true }) 24 | this.attribute('message', String) 25 | this.attribute('user_id', Number) 26 | this.attribute('thread_id', Number) 27 | 28 | this.belongsTo('user') 29 | }) 30 | }) 31 | 32 | it('updates a record (update)', function() { 33 | return store.ready(function() { 34 | var User = store.Model('User') 35 | return User.find(1).exec(function(record) { 36 | record.id.should.be.equal(1) 37 | record.login = 'philipp' 38 | return record.save() 39 | }) 40 | }) 41 | }) 42 | 43 | it('updates nested records (update)', function() { 44 | return store.ready(function() { 45 | var User = store.Model('User') 46 | return User.find(2) 47 | .include('posts') 48 | .exec(function(record) { 49 | record.login.should.be.equal('michl') 50 | record.posts.length.should.be.equal(1) 51 | 52 | record.login = 'michael' 53 | record.posts[0].message = 'michaels post' 54 | 55 | return record.save().then(function() { 56 | return User.find(2) 57 | .include('posts') 58 | .exec(function(record) { 59 | record.login.should.be.equal('michael') 60 | record.posts.length.should.be.equal(1) 61 | record.posts[0].message.should.be.equal('michaels post') 62 | }) 63 | }) 64 | }) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /lib/stores/activedirectory/data_types/user_account_control.js: -------------------------------------------------------------------------------- 1 | var UserAccountControlBitmask = { 2 | SCRIPT: 1, 3 | ACCOUNTDISABLED: 2, 4 | HOMEDIR_REQUIRED: 8, 5 | LOCKOUT: 16, 6 | PASSWD_NOTREQUIRED: 32, 7 | PASSWD_CANT_CHANGE: 64, 8 | ENCRYPTED_TEXT_PWD_ALLOWED: 128, 9 | TEMP_DUPLICATE_ACCOUNT: 256, 10 | NORMAL_ACCOUNT: 512, 11 | INTERDOMAIN_TRUST_ACCOUNT: 2048, 12 | WORKSTATION_TRUST_ACCOUNT: 4096, 13 | SERVER_TRUST_ACCOUNT: 8192, 14 | DONT_EXPIRE_PASSWORD: 65536, 15 | MNS_LOGON_ACCOUNT: 131072, 16 | SMARTCARD_REQUIRED: 262144, 17 | TRUSTED_FOR_DELEGATION: 524288, 18 | NOT_DELEGATED: 1048576, 19 | USE_DES_KEY_ONLY: 2097152, 20 | DONT_REQ_PREAUTH: 4194304, 21 | PASSWORD_EXPIRED: 8388608, 22 | TRUSTED_TO_AUTH_FOR_DELEGATION: 16777216, 23 | PARTIAL_SECRETS_ACCOUNT: 67108864 24 | } 25 | 26 | /* istanbul ignore next: unable to test via travis-ci */ 27 | exports.store = { 28 | mixinCallback: function() { 29 | this.addType( 30 | 'user_account_control', 31 | { 32 | read: function(value) { 33 | if (typeof value === 'string') value = parseInt(value, 10) 34 | 35 | var obj = {} 36 | for (var attrName in UserAccountControlBitmask) { 37 | obj[attrName] = 38 | (value & UserAccountControlBitmask[attrName]) === 39 | UserAccountControlBitmask[attrName] 40 | } 41 | 42 | return obj 43 | }, 44 | 45 | write: function(value) { 46 | if (typeof value === 'number') return value 47 | if (!value) value = {} 48 | 49 | var bitmask = 0 50 | for (var attrName in UserAccountControlBitmask) { 51 | if (value[attrName] === true) 52 | bitmask += UserAccountControlBitmask[attrName] 53 | } 54 | return bitmask 55 | } 56 | }, 57 | { 58 | binary: true, 59 | defaults: { 60 | track_object_changes: true 61 | }, 62 | operators: { 63 | default: 'eq', 64 | defaults: ['eq', 'not'] 65 | } 66 | } 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/stores/ldap/relations/has_many.js: -------------------------------------------------------------------------------- 1 | exports.definition = { 2 | hasMany: function(name, options) { 3 | options = options || {} 4 | if (!options.from) options.from = 'dn' 5 | 6 | const definition = this 7 | const utils = definition.store.utils 8 | 9 | options.setter = 10 | options.setter || 11 | function(records) { 12 | if (records === this.relations[name]) return 13 | 14 | var chain = this.relations[name] 15 | 16 | // if no relational data exists (e.g. Parent.new()) 17 | // create a new collection 18 | if (!chain) { 19 | chain = definition.model.chain({ polymorph: true }) 20 | chain.setInternal('relation', options) // add the parent relation of this collection 21 | chain.setInternal('relation_to', this) // + the parent record 22 | chain._resolved() 23 | } 24 | 25 | // lazy remove unused child records (triggered on save by the parent record!) 26 | const recordsToRemove = utils.distinctRecords(chain, records, ['dn']) 27 | 28 | chain.setInternal('__clear_only', recordsToRemove) 29 | chain.clear() 30 | chain.add(records) 31 | } 32 | 33 | options.clear = function(parent, collection) { 34 | const records = collection.getInternal('__clear_only') || collection 35 | if (!records || records.length === 0) return 36 | 37 | collection.clearInternal('__clear_only') 38 | 39 | parent.relations[name]._lazyOperation(function(transOptions) { 40 | if (options.dependent === 'destroy') { 41 | const jobs = [] 42 | records.forEach(function(record) { 43 | jobs.push(function() { 44 | return record.destroy(transOptions) 45 | }) 46 | }) 47 | return utils.parallel(jobs) 48 | } 49 | }) 50 | 51 | // remove record from collection 52 | records.forEach(function(record) { 53 | const index = collection.indexOf(record) 54 | collection.splice(index, 1) 55 | }) 56 | } 57 | 58 | return this.callParent(name, options) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/sql/postgres/enum_type-test.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | var Store = require('../../../store/postgres') 4 | 5 | describe('Postgres: ENUM Attribute', function() { 6 | var store 7 | var database = 'enum_attributes_test' 8 | 9 | before(function(next) { 10 | this.timeout(5000) 11 | beforePG(database, [], next) 12 | }) 13 | 14 | before(function() { 15 | store = new Store({ 16 | host: 'localhost', 17 | type: 'postgres', 18 | database: database, 19 | user: 'postgres', 20 | password: '', 21 | migrations: path.join(__dirname, 'fixtures', 'migrations', '*'), 22 | plugins: require('../../../lib/base/dynamic_loading') 23 | }) 24 | 25 | store.Model('EnumTest', function() {}) 26 | }) 27 | 28 | after(function(next) { 29 | afterPG(database, next) 30 | }) 31 | 32 | it('has enum_attribute', function() { 33 | return store.ready(function() { 34 | var EnumTest = store.Model('EnumTest') 35 | 36 | var attrs = EnumTest.definition.attributes 37 | 38 | attrs.should.have.property('enum_attribute') 39 | }) 40 | }) 41 | 42 | it('enum_attribute is a string', function() { 43 | return store.ready(function() { 44 | var EnumTest = store.Model('EnumTest') 45 | 46 | var attrs = EnumTest.definition.attributes 47 | 48 | attrs.enum_attribute.type.name.should.be.equal('string') 49 | }) 50 | }) 51 | 52 | it('does not allow to save any other value than defined in enum', function() { 53 | return store 54 | .ready(function() { 55 | var EnumTest = store.Model('EnumTest') 56 | 57 | return EnumTest.create({ 58 | enum_attribute: 'unknown' 59 | }) 60 | }) 61 | .should.be.rejectedWith(store.ValidationError, { 62 | errors: { enum_attribute: ['only allow one of [foo, bar]'] } 63 | }) 64 | }) 65 | 66 | it('saves valid enum value', function() { 67 | return store.ready(function() { 68 | var EnumTest = store.Model('EnumTest') 69 | 70 | return EnumTest.create({ 71 | enum_attribute: 'foo' 72 | }) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/ldap/client/exec-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | 3 | var Store = require('../../../lib/store') 4 | 5 | describe('LDAP Client: Exec', function() { 6 | var store 7 | 8 | before(function() { 9 | store = new Store({ 10 | type: 'ldap', 11 | url: 'ldap://0.0.0.0:1389', 12 | base: 'dc=test', 13 | user: 'cn=root', 14 | password: 'secret', 15 | autoSave: true 16 | }) 17 | 18 | store.Model('User', function() { 19 | this.attribute('username') 20 | }) 21 | 22 | store.Model('Ou', function() { 23 | this.rdnPrefix('ou') 24 | }) 25 | }) 26 | 27 | it('get all user objects of the root ou', function() { 28 | return store.ready(function() { 29 | var User = store.Model('User') 30 | return User.recursive(false).exec(function(users) { 31 | users.length.should.be.equal(2) 32 | }) 33 | }) 34 | }) 35 | 36 | it('user object has standard attributes', function() { 37 | return store.ready(function() { 38 | var User = store.Model('User') 39 | return User.recursive(false).exec(function(users) { 40 | var user = users[0] 41 | 42 | user.dn.should.endWith('dc=test') 43 | user.objectClass.should.be.eql(['user']) 44 | }) 45 | }) 46 | }) 47 | 48 | it('get all user objects!', function() { 49 | return store.ready(function() { 50 | var User = store.Model('User') 51 | return User.exec(function(users) { 52 | users.length.should.be.above(4) 53 | }) 54 | }) 55 | }) 56 | 57 | it('get all user objects of another ou', function() { 58 | return store.ready(function() { 59 | var User = store.Model('User') 60 | return User.searchRoot('ou=others, dc=test').exec(function(users) { 61 | users.length.should.be.equal(2) 62 | }) 63 | }) 64 | }) 65 | 66 | it('do a find on a not existing user object', function() { 67 | return store.ready(function() { 68 | var User = store.Model('User') 69 | return User.find('ou=others, dc=test').exec(function(user) { 70 | should.not.exist(user) 71 | }) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /lib/graphql/type.js: -------------------------------------------------------------------------------- 1 | exports.model = { 2 | /** 3 | * returns a string represing the model as a graphql type 4 | * @param {Object} options Optional options 5 | * @param {String} options.name Overwrite the type name (Default: Model name) 6 | * @param {String} options.description Set a description for the type 7 | * @param {Array} options.exclude Array of fields to exclude 8 | * @return {String} The graphql type 9 | */ 10 | toGraphQLType: function(options) { 11 | options = options || {} 12 | const definition = this.definition 13 | const graphqlHelper = definition.graphqlHelper || { fields: {} } 14 | const exclude = options.exclude || graphqlHelper.exclude || [] 15 | const result = [] 16 | const customDone = {} 17 | 18 | options.description = options.description || graphqlHelper.description 19 | 20 | if (options.description) result.push('# ' + options.description) 21 | result.push('type ' + (options.name || definition.modelName) + ' {') 22 | 23 | Object.keys(definition.attributes) 24 | .sort() 25 | .forEach(function(key) { 26 | var attribute = definition.attributes[key] 27 | if (attribute.hidden) return // hidden fields are for example auto generated `_ids` fields 28 | if (exclude.indexOf(key) !== -1) return 29 | if (!attribute.type.graphQLTypeName) return // auto generated graphql type via openrecord type 30 | if (graphqlHelper.fields[key]) { 31 | // field overwrite via this.graphQLField('field definition...') 32 | result.push(' ' + graphqlHelper.fields[key]) 33 | customDone[key] = true 34 | return 35 | } 36 | 37 | var attr = [' '] 38 | attr.push(key) 39 | attr.push(': ') 40 | attr.push(attribute.type.graphQLTypeName) 41 | if (attribute.notnull) attr.push('!') 42 | if (attribute.comment) attr.push(' # ' + attribute.comment) 43 | 44 | result.push(attr.join('')) 45 | }) 46 | 47 | Object.keys(graphqlHelper.fields).forEach(function(key) { 48 | if (customDone[key]) return 49 | result.push(' ' + graphqlHelper.fields[key]) 50 | }) 51 | 52 | result.push('}') 53 | 54 | return result.join('\n') 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/stores/sql/validations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DEFINITION 3 | */ 4 | exports.definition = { 5 | /** 6 | * This validator checks the uniqness of the given field`s value before save. 7 | * @class Definition 8 | * @method validatesUniquenessOf 9 | * @param {array} fields - The fields to validate 10 | * @or 11 | * @param {string} fields - The field to validate 12 | * @param {object} options - Optional: Options hash 13 | * 14 | * @options 15 | * @param {string} scope - Set a scope column 16 | * 17 | * @return {Definition} 18 | */ 19 | validatesUniquenessOf: function() { 20 | var args = this.store.utils.args(arguments) 21 | if (args.length > 1 && typeof args[1] === 'string') { 22 | return this.validateFieldsHelper(args, this.validatesUniquenessOf) 23 | } 24 | 25 | var field = args[0] 26 | var options = args[1] || {} 27 | var self = this 28 | 29 | if (Array.isArray(field)) { 30 | return this.validateFieldsHelper( 31 | field, 32 | [options], 33 | this.validatesUniquenessOf 34 | ) 35 | } 36 | 37 | return this.validates(field, function() { 38 | var record = this 39 | var primaryKeys = self.primaryKeys 40 | var condition = {} 41 | var i 42 | 43 | if (this[field] === null) { 44 | return 45 | } 46 | 47 | if (!this.hasChanged(field)) { 48 | return 49 | } 50 | 51 | condition[field] = self.cast(field, this[field], 'write', this) 52 | 53 | for (i = 0; i < primaryKeys.length; i++) { 54 | if (this[primaryKeys[i]]) { 55 | condition[primaryKeys[i] + '_not'] = this[primaryKeys[i]] 56 | } 57 | } 58 | 59 | if (options.scope) { 60 | if (!Array.isArray(options.scope)) options.scope = [options.scope] 61 | 62 | for (i = 0; i < options.scope.length; i++) { 63 | condition[options.scope[i]] = this[options.scope[i]] 64 | } 65 | } 66 | 67 | return self.model 68 | .count() 69 | .where(condition) 70 | .exec(function(result) { 71 | if (result > 0) { 72 | record.errors.add(field, 'not uniq') 73 | throw record.errors 74 | } 75 | }) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/stores/mysql/attributes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * STORE 3 | */ 4 | exports.store = { 5 | loadTableAttributes: function(name) { 6 | return this.connection 7 | .raw('SHOW FULL COLUMNS FROM ' + name) 8 | .then(function(response) { 9 | var result = response[0] 10 | var attributes = [] 11 | 12 | for (var i in result) { 13 | var attrDef = { 14 | name: result[i].Field, 15 | type: simplifiedType(result[i].Type), 16 | options: { 17 | description: result[i].Comment, 18 | persistent: true, 19 | primary: result[i].Key === 'PRI', 20 | notnull: result[i].Null !== 'YES', 21 | default: result[i].Default, 22 | length: getMaxLength(result[i].Type), 23 | writable: !( 24 | result[i].Key === 'PRI' && result[i].Extra === 'auto_increment' 25 | ) // set to false if primary and integer 26 | }, 27 | validations: [] 28 | } 29 | 30 | if (result[i].Null !== 'YES' && result[i].Key !== 'PRI') { 31 | attrDef.validations.push({ name: 'validatesPresenceOf', args: [] }) 32 | } 33 | 34 | attributes.push(attrDef) 35 | } 36 | 37 | return attributes 38 | }) 39 | } 40 | } 41 | 42 | function getMaxLength(type) { 43 | var len = type.match(/\((\d+)\)/) 44 | if (len) { 45 | len = parseInt(len[1]) 46 | 47 | if (len > 1) { 48 | return len 49 | } 50 | } 51 | return null 52 | } 53 | 54 | function simplifiedType(type) { 55 | if (type.toLowerCase() === 'tinyint(1)') type = 'boolean' 56 | type = type.replace(/\(.+\)/, '').toUpperCase() 57 | 58 | switch (type) { 59 | case 'BIGINT UNSIGNED': 60 | case 'INT UNSIGNED': 61 | case 'BIGINT': 62 | case 'TINYINT': 63 | case 'INT': 64 | return 'integer' 65 | 66 | case 'FLOAT': 67 | return 'float' 68 | 69 | case 'BOOLEAN': 70 | return 'boolean' 71 | 72 | case 'DATE': 73 | return 'date' 74 | 75 | case 'TIME': 76 | return 'time' 77 | 78 | case 'DATETIME': 79 | return 'datetime' 80 | 81 | case 'BLOB': 82 | return 'binary' 83 | 84 | default: 85 | return 'string' 86 | } 87 | } 88 | --------------------------------------------------------------------------------