├── .github ├── ISSUE_TEMPLATE │ ├── --bug-report.md │ ├── --documentation.md │ ├── --feature-request.md │ └── --question.md ├── config.yml └── stale.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── book.json ├── doc ├── .gitignore ├── API │ ├── entity-manager.md │ ├── entity-repository.md │ ├── mapping.md │ ├── migrator.md │ ├── query-builder.md │ ├── schema-builder.md │ ├── scope.md │ ├── store.md │ ├── unit-of-work.md │ └── wetland.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SUMMARY.md ├── Tutorial │ ├── bonus-cli.md │ ├── bonus-stores.md │ ├── dev-migrations.md │ ├── entities.md │ ├── entitymanager-scope-unitofwork.md │ ├── joins.md │ ├── lifecycle-callbacks.md │ ├── media │ │ ├── count.gif │ │ ├── joins.gif │ │ └── snapshots.gif │ ├── migrations.md │ ├── populator.md │ ├── query.md │ ├── querybuilder.md │ ├── relations.md │ ├── repository.md │ ├── setting-up.md │ └── snapshots.md ├── configuration.md ├── decorators.md ├── edge-cases.md ├── entity.md ├── installation.md ├── quick-start.md ├── seeding.md └── styles │ └── ebook.css ├── examples └── todo │ ├── README.md │ ├── entity │ ├── List.js │ └── Todo.js │ ├── todo.js │ └── wetland.js ├── gulpfile.js ├── package.json ├── src ├── ArrayCollection.ts ├── Cleaner.ts ├── Criteria │ ├── Criteria.ts │ ├── Having.ts │ ├── On.ts │ └── Where.ts ├── Entity.ts ├── EntityInterface.ts ├── EntityManager.ts ├── EntityProxy.ts ├── EntityRepository.ts ├── Hydrator.ts ├── IdentityMap.ts ├── Mapping.ts ├── MetaData.ts ├── Migrator │ ├── Migration.ts │ ├── MigrationFile.ts │ ├── MigrationTable.ts │ ├── Migrator.ts │ ├── MigratorConfigInterface.ts │ ├── Run.ts │ └── templates │ │ ├── migration.js.dist │ │ └── migration.ts.dist ├── Populate.ts ├── Query.ts ├── QueryBuilder.ts ├── Raw.ts ├── SchemaBuilder.ts ├── SchemaManager.ts ├── Scope.ts ├── Seeder.ts ├── SnapshotManager.ts ├── Store.ts ├── UnitOfWork.ts ├── Wetland.ts ├── decorators │ └── Mapping.ts └── index.ts ├── test ├── helper.ts ├── resource │ ├── Schema.ts │ ├── Seeder.ts │ ├── entity │ │ ├── Foo.ts │ │ ├── NoAutoIncrement.ts │ │ ├── Parent.ts │ │ ├── Simple.ts │ │ ├── SimpleDifferent.ts │ │ ├── ToUnderscore.ts │ │ ├── WithCustomRepository.ts │ │ ├── book │ │ │ ├── book.ts │ │ │ └── publisher.ts │ │ ├── eval │ │ │ ├── Address.ts │ │ │ ├── Delivery.ts │ │ │ ├── Order.ts │ │ │ ├── Tracker.ts │ │ │ └── User.ts │ │ ├── postal │ │ │ ├── Address.ts │ │ │ ├── Delivery.ts │ │ │ ├── Order.ts │ │ │ ├── Tracker.ts │ │ │ └── User.ts │ │ ├── shop │ │ │ ├── Profile.ts │ │ │ ├── category.ts │ │ │ ├── image.ts │ │ │ ├── product.ts │ │ │ ├── tag.ts │ │ │ └── user.ts │ │ ├── snapshot │ │ │ ├── new.ts │ │ │ ├── new │ │ │ │ ├── media.ts │ │ │ │ └── offer.ts │ │ │ ├── old.ts │ │ │ └── old │ │ │ │ ├── media.ts │ │ │ │ └── offer.ts │ │ └── todo │ │ │ ├── List.ts │ │ │ ├── Todo.ts │ │ │ └── User.ts │ ├── fixtures │ │ ├── lifecycle │ │ │ ├── Pet.csv │ │ │ ├── Post.json │ │ │ └── User.json │ │ └── nolifecycle │ │ │ ├── Pet.csv │ │ │ ├── Post.json │ │ │ └── User.json │ ├── migrations.ts │ ├── migrations │ │ ├── 20161004123411_baz.ts │ │ ├── 20161004123412_foo.ts │ │ └── 20161004123413_bar.ts │ ├── queries.ts │ ├── repository │ │ └── CustomRepository.ts │ ├── schemas.ts │ └── tmp │ │ └── .gitkeep └── unit │ ├── ArrayCollection.spec.ts │ ├── Cleaner.spec.ts │ ├── Criteria.spec.ts │ ├── EntityManager.spec.ts │ ├── EntityProxy.spec.ts │ ├── EntityRepository.spec.ts │ ├── Hydrator.spec.ts │ ├── Lifecyclehooks.spec.ts │ ├── Mapping.spec.ts │ ├── MetaData.spec.ts │ ├── Migrator │ ├── MigrationFile.spec.ts │ └── Migrator.spec.ts │ ├── Populator.spec.ts │ ├── QueryBuilder.spec.ts │ ├── SchemaBuilder.spec.ts │ ├── Scope.spec.ts │ ├── Seeder.spec.ts │ ├── SnapshotManager.spec.ts │ ├── Store.spec.ts │ ├── UnitOfWork.spec.ts │ └── Wetland.spec.ts ├── tsconfig.json ├── tslint.json ├── wallaby.js ├── wetland.src.svg ├── wetland.svg └── yarn.lock /.github/ISSUE_TEMPLATE/--bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41BBug report" 3 | about: Create a report to help us improve. 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | [Include code or an example repository that can easily be set up] 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Snapshots** 19 | If applicable, add snapshots to help explain your problem. 20 | 21 | **Environment (please complete the following information):** 22 | - Node.js version: [e.g. 8.11.3] 23 | - Adapter: [e.g. MySQL or PostgreSQL] 24 | - Version [e.g. 3.1.0] 25 | 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4D6Documentation" 3 | about: Suggest improvements or report missing/unclear documentation. 4 | 5 | --- 6 | 7 | **Pre-check** 8 | - [ ] I'm aware that I can [edit the docs](https://github.com/SpoonX/wetland/tree/master/doc) and submit a pull request 9 | 10 | **Describe the improvement** 11 | 12 | I'd like to report 13 | - [ ] Unclear documentation 14 | - [ ] A typo 15 | - [ ] Missing documentation 16 | - [ ] Other 17 | 18 | **If applicable add a link to the page in question** 19 | [e.g. https://wetland.spoonx.org/Tutorial/snapshots.html or https://github.com/SpoonX/wetland/blob/master/doc/Tutorial/snapshots.md] 20 | 21 | **Description of the report** 22 | A clear and concise description. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1Feature request" 3 | about: Suggest an idea for this project. 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4ACQuestion" 3 | about: Ask questions. 4 | 5 | --- 6 | 7 | **Describe your question with as much detail as possible** 8 | A clear and concise question that doesn't require too much conversation. Need more help? [Join us on Gitter](https://gitter.im/SpoonX/Dev) 9 | 10 | 11 | **If it's about a specific piece of code, try and include some of it to support your question.** 12 | [...] 13 | 14 | 15 | **Environment (please complete the following information):** 16 | - Node.js version: [e.g. 8.11.3] 17 | - Adapter: [e.g. MySQL or PostgreSQL] 18 | - Version [e.g. 3.1.0] 19 | 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for request-info - https://github.com/behaviorbot/request-info 2 | 3 | # *OPTIONAL* Comment to reply with 4 | # Can be either a string : 5 | requestInfoReplyComment: > 6 | We would appreciate it if you could provide us with more info about this issue/pr! 7 | 8 | # Or an array: 9 | requestInfoReplyComment: 10 | - Ah no! young blade! That was a trifle short! 11 | - Tell me more! 12 | - I am sure you can be more effusive 13 | 14 | 15 | # *OPTIONAL* default titles to check against for lack of descriptiveness 16 | # MUST BE ALL LOWERCASE 17 | requestInfoDefaultTitles: 18 | - update readme.md 19 | - updates 20 | 21 | # *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given 22 | requestInfoLabelToAdd: needs-more-info 23 | 24 | # *OPTIONAL* Only warn about insufficient information on these events type 25 | # Keys must be lowercase. Valid values are 'issue' and 'pullRequest' 26 | requestInfoOn: 27 | pullRequest: true 28 | issue: true 29 | 30 | todo: 31 | keyword: "@makeAnIssue" 32 | autoAssign: true 33 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | # Label to use when marking an issue as stale 11 | staleLabel: wontfix 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | rlib-cov 3 | *.seed 4 | *.log 5 | *.out 6 | *.pid 7 | npm-debug.log 8 | *~ 9 | *# 10 | .DS_STORE 11 | .netbeans 12 | nbproject 13 | node_modules 14 | .idea 15 | .node_history 16 | .nyc_output 17 | .vscode 18 | notes.md 19 | _book 20 | .data 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | rlib-cov 2 | *.seed 3 | *.log 4 | *.out 5 | *.pid 6 | npm-debug.log 7 | *~ 8 | *# 9 | .DS_STORE 10 | .netbeans 11 | nbproject 12 | node_modules 13 | .idea 14 | .node_history 15 | .vscode 16 | notes.md 17 | .travis.yml 18 | .gitignore 19 | wallaby.js 20 | yarn.lock 21 | .data 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "9" 6 | - "12" 7 | 8 | services: 9 | - mysql 10 | 11 | before_script: 12 | - mysql -e 'create database wetland_test;' 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love for you to contribute and to make this project even better! 4 | If this interests you, please begin by reading our [contributing guidelines](https://github.com/SpoonX/about/blob/master/CONTRIBUTING.md), which will provide you with all the information you need to get started. 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 SpoonX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Wetland](https://cdn.rawgit.com/SpoonX/wetland/391040eba795183550bfff01d7c0ca56d01b5530/wetland.svg) 2 | 3 | [![Build Status](https://travis-ci.org/SpoonX/wetland.svg?branch=master)](https://travis-ci.org/SpoonX/wetland) 4 | [![npm version](https://badge.fury.io/js/wetland.svg)](https://badge.fury.io/js/wetland) 5 | [![Slack Status](https://spoonx-slack.herokuapp.com/badge.svg)](https://spoonx-slack.herokuapp.com) 6 | 7 | Wetland is a modern object-relational mapper (ORM) for node.js based on the JPA-spec. 8 | It strikes a balance between ease and structure, allowing you to get started quickly, without losing flexibility or features. 9 | 10 | **New!** Take a look at our [wetland tutorial](https://wetland.spoonx.org/Tutorial/setting-up.html). 11 | 12 | **New!** Wetland CLI now has its [own repository](https://github.com/SpoonX/wetland-cli). `npm i -g wetland-cli`. 13 | 14 | **New!** Wetland has a nice entity generator. Let us do the heavy lifting. [Repository can be found here](https://github.com/SpoonX/wetland-generator-entity). 15 | 16 | ## Features 17 | 18 | Wetland is based on the [JPA-spec](http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html) and therefore has some similarities to Hibernate and Doctrine. While some aspects of the ORM have been adapted to perform better in the Node.js environment and don't follow the specification to the letter for that reason, the JPA specification is a stable and well written specification that makes wetland structured and performant. 19 | 20 | Some of the major features provided include: 21 | 22 | * Unit of work 23 | * Derived tables 24 | * Migrations 25 | * Transactions 26 | * Entity manager 27 | * Cascaded persists 28 | * Deep joins 29 | * Repositories 30 | * QueryBuilder 31 | * Entity mapping 32 | * Optimized state manager 33 | * Recipe based hydration 34 | * [More...](https://wetland.spoonx.org) 35 | 36 | ## Installation 37 | 38 | To install wetland run the following command: 39 | 40 | `npm i --save wetland` 41 | 42 | Typings are provided by default for TypeScript users. No additional typings need installing. 43 | 44 | ## Plugins / essentials 45 | 46 | - [Wetland CLI](https://github.com/SpoonX/wetland-cli) `npm i -g wetland-cli` 47 | - [Express middleware](https://github.com/SpoonX/express-wetland) 48 | - [Sails.js hook](https://github.com/SpoonX/sails-hook-wetland) 49 | - [Trailpack](https://github.com/SpoonX/trailpack-wetland) 50 | - [Entity generator](https://github.com/SpoonX/wetland-generator-entity) 51 | 52 | ## Compatibility 53 | 54 | * All operating systems 55 | * Node.js 8.0+ 56 | 57 | ## Gotchas 58 | 59 | - When using sqlite3, foreign keys are disabled (this is due to alter table not working for foreign keys with sqlite). 60 | 61 | ## Usage 62 | 63 | The following is a snippet to give you an idea what it's like to work with wetland. 64 | For a much more detailed explanation, [head to the documentation.](https://wetland.spoonx.org). 65 | 66 | ```js 67 | const Wetland = require('wetland').Wetland; 68 | const Foo = require('./entity/foo').Foo; 69 | const Bar = require('./entity/foo').Bar; 70 | const wetland = new Wetland({ 71 | stores: { 72 | simple: { 73 | client : 'mysql', 74 | connection: { 75 | user : 'root', 76 | database: 'testdatabase' 77 | } 78 | } 79 | }, 80 | entities: [Foo, Bar] 81 | }); 82 | 83 | // Create the tables. Async process, only here as example. 84 | // use .getSQL() (not async) in stead of apply (async) to get the queries. 85 | let migrator = wetland.getMigrator().create(); 86 | migrator.apply().then(() => {}); 87 | 88 | // Get a manager scope. Call this method for every context (e.g. requests). 89 | let manager = wetland.getManager(); 90 | 91 | // Get the repository for Foo 92 | let repository = manager.getRepository(Foo); 93 | 94 | // Get some results, and join. 95 | repository.find({name: 'cake'}, {joins: ['candles', 'baker', 'baker.address']}) 96 | .then(results => { 97 | // ... 98 | }); 99 | ``` 100 | 101 | ### Entity example 102 | 103 | #### Javascript 104 | 105 | ```js 106 | const { UserRepository } = require('../repository/UserRepository'); 107 | 108 | class User { 109 | static setMapping(mapping) { 110 | // Adds id, updatedAt and createdAt for your convenience. 111 | mapping.autoFields(); 112 | 113 | mapping.entity({ repository: UserRepository }) 114 | mapping.field('dateOfBirth', { type: 'datetime' }); 115 | } 116 | } 117 | 118 | module.exports.User = User; 119 | ``` 120 | 121 | #### Typescript 122 | 123 | ```js 124 | import { entity, autoFields, field } from 'wetland'; 125 | import { UserRepository } from '../repository/UserRepository'; 126 | 127 | @entity({ repository: UserRepository }) 128 | @autoFields() 129 | export class User { 130 | @field({ type: 'datetime' }) 131 | public dateOfBirth: Date; 132 | } 133 | ``` 134 | 135 | ## License 136 | 137 | MIT 138 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "edit-link", 4 | "github", 5 | "theme-api", 6 | "anchors", 7 | "katex" 8 | ], 9 | "pluginsConfig": { 10 | "edit-link": { 11 | "base": "https://github.com/SpoonX/wetland/edit/master/doc", 12 | "label": "Edit This Page" 13 | }, 14 | "github": { 15 | "url": "https://github.com/spoonx/wetland" 16 | }, 17 | "theme-api": { 18 | "languages": [ 19 | { 20 | "lang": "js", 21 | "name": "JavaScript", 22 | "default": true 23 | }, 24 | { 25 | "lang": "ts", 26 | "name": "TypeScript" 27 | } 28 | ], 29 | "split": true, 30 | "theme": "dark" 31 | } 32 | }, 33 | "root": "./doc" 34 | } -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | -------------------------------------------------------------------------------- /doc/API/entity-manager.md: -------------------------------------------------------------------------------- 1 | # Entity Manager 2 | To create an entity manager instance and use the methods bellow: 3 | 4 | ```js 5 | let manager = wetland.getManager(); 6 | ```` 7 | 8 | {% method %} 9 | ## .createScope() 10 | Creates a new entity manager scope. 11 | 12 | {% common %} 13 | ```js 14 | manager.createScope(); 15 | ``` 16 | {% endmethod %} 17 | 18 | {% method %} 19 | ## .getRepository() 20 | Returns an entity repository instance for provided entity. 21 | 22 | **Note:** If you're planning on doing work with the unit of work (`wetland.getManager()` / `EntityManager.createScope()`) you must use the scope to get the repository. 23 | 24 | Repositories fetched from the entity manager are a more performant way of querying the database, but they're never linked to a scope. 25 | If all you're planning on doing is fetching data or performing simple queries, this method is for you. 26 | 27 | The queries performed on a repository fetched from the EntityManager _do_ run in an internal scope, so they're safe to use in for example APIs. 28 | 29 | {% common %} 30 | ```js 31 | manager.getRepository(entity); 32 | ``` 33 | {% endmethod %} 34 | 35 | {% method %} 36 | ## .getConfig() 37 | Gets the config using `.getConfig()` from wetland. 38 | 39 | {% common %} 40 | ```js 41 | manager.getConfig(); 42 | ``` 43 | {% endmethod %} 44 | 45 | {% method %} 46 | ## .getEntities() 47 | Returns all registered entities as an object. 48 | 49 | {% common %} 50 | ```js 51 | manager.getEntities(); 52 | ``` 53 | {% endmethod %} 54 | 55 | {% method %} 56 | ## .getEntity() 57 | Get the reference to an entity constructor by its name. 58 | 59 | {% common %} 60 | ```js 61 | manager.getEntity('entityName'); 62 | ``` 63 | {% endmethod %} 64 | 65 | {% method %} 66 | ## .getMapping() 67 | Get the mapping for provided entity. Can be an instance, constructor or the name of the entity. 68 | 69 | {% common %} 70 | ```js 71 | manager.getMapping(entity); 72 | ``` 73 | {% endmethod %} 74 | 75 | {% method %} 76 | ## .registerEntities() 77 | Register multiple entities. 78 | 79 | {% common %} 80 | ```js 81 | manager.registerEntities([entity1, entity2]); 82 | ``` 83 | {% endmethod %} 84 | 85 | {% method %} 86 | ## .registerEntity() 87 | Register a single entity. 88 | 89 | {% common %} 90 | ```js 91 | manager.registerEntity(entity); 92 | ``` 93 | {% endmethod %} 94 | 95 | {% method %} 96 | ## .resolveEntityReference() 97 | Resolve provided value to an entity reference. The argument can be a name, the constructor or the entity itself. 98 | 99 | {% common %} 100 | ```js 101 | manager.resolveEntityReference(entity); 102 | ``` 103 | {% endmethod %} 104 | -------------------------------------------------------------------------------- /doc/API/entity-repository.md: -------------------------------------------------------------------------------- 1 | # Entity Repository 2 | To get the entity repository: 3 | 4 | ```js 5 | let entityRepository = wetland.getManager().getRepository(Entity); 6 | ``` 7 | 8 | {% method %} 9 | ## .find() 10 | Finds entities based on provided criteria. Use `null` if you don't wish to pass any criteria. 11 | Optionals [finding options](#find-options) can be used as a second argument. 12 | 13 | {% common %} 14 | ```js 15 | entityRepository.find({name: 'Wesley'} , {populate: ['project']}); 16 | ``` 17 | {% endmethod %} 18 | 19 | {% method %} 20 | ## .findOne() 21 | Finds a single entity. In this method, criteria can be either an object, a number, a string or `null`. 22 | 23 | {% common %} 24 | ```js 25 | entityRepository.findOne({'u.name': 'Wesley', 'project.name': 'wetland'}, {alias: 'u', populate: ['u.project']}); 26 | ``` 27 | {% endmethod %} 28 | 29 | {% method %} 30 | ## .getQueryBuilder() 31 | Gets a new query builder. Optionally you can create your alias by passing a string as an argument. 32 | 33 | {% common %} 34 | ```js 35 | entityRepository.getQueryBuilder('u'); 36 | ``` 37 | {% endmethod %} 38 | 39 | {% method %} 40 | ## .getDerivedQueryBuilder() 41 | Gets a new query builder which uses provided derived table. An optional second argument allows you to set the alias of the derived table. 42 | 43 | {% common %} 44 | ```js 45 | entityRepository.getDerivedQueryBuilder(otherQueryBuilder, 'my_alias_derived'); 46 | ``` 47 | {% endmethod %} 48 | 49 | #### Find options 50 | 51 | | Options | Type | 52 | |:----------|:-------------------------------------------:| 53 | | orderBy | any | 54 | | alias | string | 55 | | limit | number | 56 | | page | number | 57 | | offset | number | 58 | | debug | boolean | 59 | | populate | object, array of strings, array of objects | 60 | -------------------------------------------------------------------------------- /doc/API/migrator.md: -------------------------------------------------------------------------------- 1 | # Migrator 2 | To get the migrator: 3 | 4 | ```js 5 | let migrator = wetland.getMigrator(); 6 | ``` 7 | 8 | {% method %} 9 | ## .allMigrations() 10 | Gets all migrations from the directory on the config. 11 | 12 | {% common %} 13 | ```js 14 | migrator.allMigrations(); 15 | ``` 16 | {% endmethod %} 17 | 18 | {% method %} 19 | ## .appliedMigrations() 20 | Gets all applied migrations. 21 | 22 | {% common %} 23 | ```js 24 | migrator.appliedMigrations(); 25 | ``` 26 | {% endmethod %} 27 | 28 | {% method %} 29 | ## .create() 30 | Creates a new migration file. 31 | 32 | {% common %} 33 | ```js 34 | migrator.create('migrationName'); 35 | ``` 36 | {% endmethod %} 37 | 38 | {% method %} 39 | ## .createSchema() 40 | Creates your database schema. 41 | 42 | {% common %} 43 | ```js 44 | migrator.createSchema(); 45 | ``` 46 | {% endmethod %} 47 | 48 | {% method %} 49 | ## .down() 50 | Go down one version based on latest run migration timestamp. 51 | Pass `'run'` to run the migration or `'getSQL'` to get the queries. 52 | 53 | {% common %} 54 | ```js 55 | migrator.down('run'); 56 | ``` 57 | {% endmethod %} 58 | 59 | {% method %} 60 | ## .getConnection() 61 | Get the connection for the migrations tables. 62 | 63 | {% common %} 64 | ```js 65 | migrator.getConnection(); 66 | ``` 67 | {% endmethod %} 68 | 69 | {% method %} 70 | ## .latest() 71 | Go up to the latest migration. 72 | Pass `'run'` to run the migration or `'getSQL'` to get the queries. 73 | 74 | {% common %} 75 | ```js 76 | migrator.latest('run'); 77 | ``` 78 | {% endmethod %} 79 | 80 | {% method %} 81 | ## .revert() 82 | Revert the last run UP migration (or batch of UP migrations). 83 | Pass `'run'` to run the migration or `'getSQL'` to get the queries. 84 | 85 | {% common %} 86 | ```js 87 | migrator.revert('run'); 88 | ``` 89 | {% endmethod %} 90 | 91 | {% method %} 92 | ## .run() 93 | Run a specific migration. 94 | Although you can run your migration using this method, we recommend you to use `.up()`, `.down()`, 95 | `.latest()` and `.revert()` instead. 96 | 97 | {% common %} 98 | ```js 99 | migrator.run('up', 'getSQL', '20161004123412_foo'); 100 | ``` 101 | {% endmethod %} 102 | 103 | {% method %} 104 | ## .up() 105 | Go up one version based on latest run migration timestamp. 106 | 107 | {% common %} 108 | ```js 109 | migrator.up('run'); 110 | ``` 111 | {% endmethod %} 112 | -------------------------------------------------------------------------------- /doc/API/query-builder.md: -------------------------------------------------------------------------------- 1 | # Query Builder 2 | To get the query builder: 3 | 4 | ```js 5 | let queryBuilder = wetland.getManager().getRepository(Entity).getQueryBuilder(); 6 | ``` 7 | 8 | {% method %} 9 | ## .createAlias() 10 | Creates an alias for an entity. This method is used by `.join()` to create an alias for the join column. 11 | To create an alias we recommend you to pass it directly on the `.getQueryBuilder()` method. 12 | 13 | {% common %} 14 | ```js 15 | queryBuilder.createAlias('a'); 16 | ``` 17 | {% endmethod %} 18 | 19 | {% method %} 20 | ## .crossJoin() 21 | Performs a cross join. Takes the column name the fist argument and the target alias as the second argument. 22 | 23 | {% common %} 24 | ```js 25 | queryBuilder.crossJoin('columnName', 'a'); 26 | ``` 27 | {% endmethod %} 28 | 29 | {% method %} 30 | ## .fullOuterJoin() 31 | Performs a full outer join. 32 | 33 | {% common %} 34 | ```js 35 | queryBuilder.fullOuterJoin('columnName', 'a'); 36 | ``` 37 | {% endmethod %} 38 | 39 | {% method %} 40 | ## .getQuery() 41 | Gets an instance of the query class. 42 | 43 | {% common %} 44 | ```js 45 | queryBuilder.getQuery(); 46 | ``` 47 | {% endmethod %} 48 | 49 | {% method %} 50 | ## .groupBy() 51 | Sets the group by. 52 | 53 | {% common %} 54 | ```js 55 | queryBuilder.groupBy('name'); 56 | queryBuilder.groupBy(['name', 'age']); 57 | ``` 58 | {% endmethod %} 59 | 60 | {% method %} 61 | ## .having() 62 | Sets the having clause. 63 | 64 | {% common %} 65 | ```js 66 | queryBuilder.having({'name': {lte: 12}}); 67 | queryBuilder.having({'name': {gte: 10, lte: 50}}); 68 | ``` 69 | {% endmethod %} 70 | 71 | {% method %} 72 | ## .innerJoin() 73 | Performs an inner join. 74 | 75 | {% common %} 76 | ```js 77 | queryBuilder.innerJoin('columnName', 'a'); 78 | ``` 79 | {% endmethod %} 80 | 81 | {% method %} 82 | ## .insert() 83 | Signals an insert. 84 | 85 | {% common %} 86 | ```js 87 | queryBuilder.insert({name: 'Wesley'}); 88 | ``` 89 | {% endmethod %} 90 | 91 | {% method %} 92 | ## .makeJoin() 93 | Performs a join. This method is used by specific join methods in this class and we recommend you to use them instead. 94 | 95 | {% common %} 96 | ```js 97 | queryBuilder.makeJoin('innerJoin', 'columnName', 'a'); 98 | ``` 99 | {% endmethod %} 100 | 101 | {% method %} 102 | ## .leftJoin() 103 | Performs 104 | Performs a left join. 105 | 106 | {% common %} 107 | ```js 108 | queryBuilder.leftJoin('columnName', 'a'); 109 | ``` 110 | {% endmethod %} 111 | 112 | {% method %} 113 | ## .leftOuterJoin() 114 | Performs a left outer join. 115 | 116 | {% common %} 117 | ```js 118 | queryBuilder.leftOuterJoin('columnName', 'a'); 119 | ``` 120 | {% endmethod %} 121 | 122 | {% method %} 123 | ## .limit() 124 | Sets the limit. 125 | 126 | {% common %} 127 | ```js 128 | queryBuilder.limit(10); 129 | ``` 130 | {% endmethod %} 131 | 132 | {% method %} 133 | ## .offset() 134 | Sets the offset. 135 | 136 | {% common %} 137 | ```js 138 | queryBuilder.offset(10); 139 | ``` 140 | {% endmethod %} 141 | 142 | {% method %} 143 | ## .orderBy() 144 | Sets the order by. 145 | 146 | {% common %} 147 | ```js 148 | queryBuilder.orderBy('name'); 149 | queryBuilder.orderBy('name', 'desc'); 150 | queryBuilder.orderBy({name: 'desc'}); 151 | queryBuilder.orderBy(['name', {age: 'asc'}]); 152 | ``` 153 | {% endmethod %} 154 | 155 | {% method %} 156 | ## .outerJoin() 157 | Performs an outer join. 158 | 159 | {% common %} 160 | ```js 161 | queryBuilder.outerJoin('columnName', 'a'); 162 | ``` 163 | {% endmethod %} 164 | 165 | {% method %} 166 | ## .prepare() 167 | Makes sure all changes have been applied to the query. 168 | 169 | {% common %} 170 | ```js 171 | queryBuilder.prepare(); 172 | ``` 173 | {% endmethod %} 174 | 175 | {% method %} 176 | ## .remove() 177 | Signals a delete. 178 | 179 | {% common %} 180 | ```js 181 | queryBuilder.remove(); 182 | ``` 183 | {% endmethod %} 184 | 185 | {% method %} 186 | ## .rightJoin() 187 | Performs a right join. 188 | 189 | {% common %} 190 | ```js 191 | queryBuilder.rightJoin('columnName', 'a'); 192 | ``` 193 | {% endmethod %} 194 | 195 | {% method %} 196 | ## .from() 197 | Provide a derived table to select from. 198 | 199 | {% common %} 200 | ```js 201 | usersQueryBuilder.where(criteria).groupBy('foo'); 202 | 203 | queryBuilder.from(usersQueryBuilder, 'a_derived').select({count: '*'}); 204 | ``` 205 | {% endmethod %} 206 | 207 | {% method %} 208 | ## .rightOuterJoin() 209 | Performs a right outer join. 210 | 211 | {% common %} 212 | ```js 213 | queryBuilder.rightOuterJoin('columnName', 'a'); 214 | ``` 215 | {% endmethod %} 216 | 217 | {% method %} 218 | ## .select() 219 | Select columns. Giving it an alias or the column name equals to `SELECT *`. 220 | 221 | {% common %} 222 | ```js 223 | queryBuilder.select('a'); 224 | queryBuilder.select('a.age'); 225 | queryBuilder.select({sum: 'age'}); 226 | ``` 227 | {% endmethod %} 228 | 229 | {% method %} 230 | ## .update() 231 | Signals an update. 232 | 233 | {% common %} 234 | ```js 235 | queryBuilder.update({name: 'Wesley'}); 236 | ``` 237 | {% endmethod %} 238 | 239 | {% method %} 240 | ## .where() 241 | Sets the where clause. 242 | 243 | {% common %} 244 | ```js 245 | queryBuilder.where({name: 'Wesley'}); 246 | queryBuilder.where({name: ['Wesley', 'Raphaela']}); 247 | queryBuilder.where({name: 'Wesley', company: 'SpoonX', age: {gt: 25}}); 248 | ``` 249 | {% endmethod %} 250 | -------------------------------------------------------------------------------- /doc/API/schema-builder.md: -------------------------------------------------------------------------------- 1 | # Schema Builder 2 | The schema builder is used by the migrator class to create the entity's schema. 3 | 4 | {% method %} 5 | ## .apply() 6 | Persists the schema to the database, returns a promise. 7 | 8 | {% common %} 9 | ```js 10 | schemaBuilder.apply(); 11 | ``` 12 | {% endmethod %} 13 | 14 | {% method %} 15 | ## .bigInteger() 16 | Define a column type as `BIGINT`. Field object is defined by the [field options.](./mapping.md#field-options) 17 | 18 | {% common %} 19 | ```js 20 | schemaBuilder.bigInteger(table, field); 21 | ``` 22 | {% endmethod %} 23 | 24 | {% method %} 25 | ## .binary() 26 | Define a column type as `BINARY`. 27 | 28 | {% common %} 29 | ```js 30 | schemaBuilder.binary(table, field); 31 | ``` 32 | {% endmethod %} 33 | 34 | {% method %} 35 | ## .boolean() 36 | Define a column type as `BOOLEAN`. 37 | 38 | {% common %} 39 | ```js 40 | schemaBuilder.boolean() 41 | ``` 42 | {% endmethod %} 43 | 44 | {% method %} 45 | ## .create() 46 | Creates the schema. 47 | 48 | {% common %} 49 | ```js 50 | schemaBuilder.create(); 51 | ``` 52 | {% endmethod %} 53 | 54 | {% method %} 55 | ## .date() 56 | Define a column type as `DATE`. 57 | 58 | {% common %} 59 | ```js 60 | schemaBuilder.date(table, field); 61 | ``` 62 | {% endmethod %} 63 | 64 | {% method %} 65 | ## .dateTime() 66 | Define a column type as `DATETIME`. 67 | 68 | {% common %} 69 | ```js 70 | schemaBuilder.datetime(table, field); 71 | //or 72 | schemaBuilder.dateTime(table, field); 73 | ``` 74 | {% endmethod %} 75 | 76 | {% method %} 77 | ## .decimal() 78 | Define a column type as `DECIMAL`. 79 | If `precision` and `scale` are not defined on the field options, `precision` will be set to `8` and `scale` to `2`. 80 | 81 | {% common %} 82 | ```js 83 | schemaBuilder.decimal(table, field); 84 | ``` 85 | {% endmethod %} 86 | 87 | {% method %} 88 | ## .enumeration() 89 | Define a column type as `ENUM`. 90 | 91 | {% common %} 92 | ```js 93 | schemaBuilder.enumeration(table, field); 94 | ``` 95 | {% endmethod %} 96 | 97 | {% method %} 98 | ## .float() 99 | Define a column type as `FLOAT`. 100 | If `precision` and `scale` are not defined on the field options, `precision` will be set to `8` and `scale` to `2`. 101 | 102 | {% common %} 103 | ```js 104 | schemaBuilder(table, field); 105 | ``` 106 | {% endmethod %} 107 | 108 | {% method %} 109 | ## .getSQL() 110 | Gets the schema queries. 111 | 112 | {% common %} 113 | ```js 114 | schemaBuilder.getSQL(); 115 | ``` 116 | {% endmethod %} 117 | 118 | {% method %} 119 | ## .integer() 120 | Define a column type as `INT`. 121 | 122 | {% common %} 123 | ```js 124 | schemaBuilder.integer(table, field); 125 | ``` 126 | {% endmethod %} 127 | 128 | {% method %} 129 | ## .json() 130 | Define a column type as `JSON`. 131 | 132 | {% common %} 133 | ```js 134 | schemaBuilder.json(table, field); 135 | ``` 136 | {% endmethod %} 137 | 138 | {% method %} 139 | ## .jsonb() 140 | Define a column type as `JSONB`. 141 | 142 | {% common %} 143 | ```js 144 | schemaBuilder.jsonb(table, field); 145 | ``` 146 | {% endmethod %} 147 | 148 | {% method %} 149 | ## .string() 150 | Define a column type as `VARCHAR`. 151 | If `size` is not defined on the field options, it defaults to `255`. 152 | 153 | {% common %} 154 | ```js 155 | schemaBuilder.string(table, field); 156 | ``` 157 | {% endmethod %} 158 | 159 | {% method %} 160 | ## .text() 161 | Define a column type as `TEXT`. 162 | 163 | {% common %} 164 | ```js 165 | schemaBuilder.text(table, field); 166 | ``` 167 | {% endmethod %} 168 | 169 | {% method %} 170 | ## .time() 171 | Define a column type as `TIME`. 172 | 173 | {% common %} 174 | ```js 175 | schemaBuilder.time(table, field); 176 | ``` 177 | {% endmethod %} 178 | 179 | {% method %} 180 | ## .timestamp() 181 | Define a column type as `TIMESTAMP`. 182 | 183 | {% common %} 184 | ```js 185 | schemaBuilder.timestamp(table, field); 186 | ``` 187 | {% endmethod %} 188 | 189 | {% method %} 190 | ## .uuid() 191 | Define a column type as `CHAR(36)`. (`UUID` on postgresql) 192 | 193 | {% common %} 194 | ```js 195 | schemaBuilder.uuid(table, field); 196 | ``` 197 | {% endmethod %} 198 | -------------------------------------------------------------------------------- /doc/API/scope.md: -------------------------------------------------------------------------------- 1 | # Scope 2 | The scope class is instantiated by the entity manager. 3 | 4 | ```js 5 | let scope = wetland.getManager(); 6 | ``` 7 | 8 | {% method %} 9 | ## .attach() 10 | This method is used to proxy an entity with the entity proxy. 11 | 12 | {% common %} 13 | ```js 14 | scope.attach(entity); 15 | ``` 16 | {% endmethod %} 17 | 18 | {% method %} 19 | ## .clear() 20 | Clears the unit of work. 21 | 22 | {% common %} 23 | ```js 24 | scope.clear(); 25 | ``` 26 | {% endmethod %} 27 | 28 | {% method %} 29 | ## .detach() 30 | Removes proxy and clear this entity from the unit of work. 31 | 32 | {% common %} 33 | ```js 34 | scope.detach(entity); 35 | ``` 36 | {% endmethod %} 37 | 38 | {% method %} 39 | ## .flush() 40 | This method is responsible for persisting the unit of work. 41 | This means calculating changes to make, as well as the order to do so. 42 | One of the things involved in this is making the distinction between stores. 43 | 44 | {% common %} 45 | ```js 46 | // Skip cleaning (state) of entities. Saves on performance. 47 | // Only use if you're done with the scope. 48 | const skipClean = true; 49 | 50 | // Skips the lifecycle hooks on the entities. Useful in rare situations. 51 | const skipLifecycleHooks = true; 52 | 53 | // Some options to override the defaults for this specific flush. 54 | // Refresh is responsible for re-fetching the entity's data from the database. 55 | const config = { refreshCreated: true, refreshUpdated: true }; 56 | 57 | // Aaaand flush! 58 | scope.flush(skipClean, skipLifecycleHooks, config); 59 | ``` 60 | {% endmethod %} 61 | 62 | {% method %} 63 | ## .getConfig() 64 | Gets the wetland configuration. 65 | 66 | {% common %} 67 | ```js 68 | scope.getConfig(); 69 | ``` 70 | {% endmethod %} 71 | 72 | {% method %} 73 | ## .getEntities() 74 | Gets all registered entities with the entity manager. 75 | 76 | {% common %} 77 | ```js 78 | scope.getEntities(); 79 | ``` 80 | {% endmethod %} 81 | 82 | {% method %} 83 | ## .getEntity(); 84 | Gets a single entity with the entity manager. 85 | 86 | {% common %} 87 | ```js 88 | scope.getEntity('name'); 89 | ``` 90 | {% endmethod %} 91 | 92 | {% method %} 93 | ## .getReference() 94 | Gets a reference to a persisted row without actually loading it from the database, or returns a row found in the IdentityMap (if fetched earlier in the scope). 95 | Besides giving it the entity, you also need to specify the primary key value of the targeted row. 96 | 97 | {% common %} 98 | ```js 99 | scope.getReference(entity, 1); 100 | ``` 101 | {% endmethod %} 102 | 103 | {% method %} 104 | ## .getRepository() 105 | Returns an entity repository instance for provided entity. 106 | 107 | {% common %} 108 | ```js 109 | scope.getRepository(entity); 110 | ``` 111 | {% endmethod %} 112 | 113 | {% method %} 114 | ## .getStore() 115 | Gets the store for provided entity with wetland. 116 | 117 | {% common %} 118 | ```js 119 | scope.getStore(entity); 120 | ``` 121 | {% endmethod %} 122 | 123 | {% method %} 124 | ## .getUnityOfWork() 125 | Returns an instance of unit of work. The methods of the unit of work are listed and described [here.](./unit-of-work.md) 126 | 127 | {% common %} 128 | ```js 129 | scope.getUnitOfWork(); 130 | ``` 131 | {% endmethod %} 132 | 133 | {% method %} 134 | ## .persist() 135 | Mark provided entity as new with the unit of work. 136 | This method returns `.registerNew()` from the unit of work to register new entities that will be persisted when `.flush()` is called. 137 | 138 | {% common %} 139 | ```js 140 | scope.persist([entity1, entity2]); 141 | ``` 142 | {% endmethod %} 143 | 144 | {% method %} 145 | ## .refresh() 146 | This method refreshes provided entities, syncing with the database. Entities must be passed as an array. 147 | 148 | {% common %} 149 | ```js 150 | scope.refresh([entity1, entity2]); 151 | ``` 152 | {% endmethod %} 153 | 154 | {% method %} 155 | ## .remove() 156 | Marks an entity as deleted with the unit of work. 157 | This method returns `.registerDeleted()` from the unit of work to register the provided entity as deleted. 158 | While it is registered as deleted, the entity will be deleted from the database when `.flush()` is called. 159 | 160 | {% common %} 161 | ```js 162 | scope.remove(entity); 163 | ``` 164 | {% endmethod %} 165 | 166 | {% method %} 167 | ## .resolveEntityReference() 168 | Resolve provided value to an entity reference. The argument can be a name, a constructor or the entity itself. 169 | 170 | {% common %} 171 | ```js 172 | scope.resolveEntityReference(entity); 173 | ``` 174 | {% endmethod %} 175 | -------------------------------------------------------------------------------- /doc/API/store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | To get the store: 3 | 4 | ```js 5 | let store = wetland.getStore('store'); 6 | ``` 7 | 8 | {% method %} 9 | ## .getConnection() 10 | Gets a connection for `role`. Uses round robin. If no argument is passed, uses `'single'` as default role. 11 | 12 | {% common %} 13 | ```js 14 | store.getConnection('master'); 15 | ``` 16 | {% endmethod %} 17 | 18 | {% method %} 19 | ## .getConnections() 20 | Gets the connection registered on this store. 21 | 22 | {% common %} 23 | ```js 24 | store.getConnections(); 25 | ``` 26 | {% endmethod %} 27 | 28 | {% method %} 29 | ## .getName() 30 | Gets the name of the store. 31 | 32 | {% common %} 33 | ```js 34 | store.getName(); 35 | ``` 36 | {% endmethod %} 37 | 38 | {% method %} 39 | ## .register() 40 | Registers connections. Connection config is an object and its keys vary according to the type of connection. 41 | 42 | {% common %} 43 | ```js 44 | store.register({ 45 | client: 'mysql', 46 | connection: { 47 | host : '127.0.0.1', 48 | user : 'root' 49 | database: 'wetland_database' 50 | } 51 | }); 52 | ``` 53 | {% endmethod %} 54 | 55 | {% method %} 56 | ## .registerConnection() 57 | Register a connection. Stating role is optional. 58 | 59 | {% common %} 60 | ```js 61 | store.registerConnection({connection: {...}}, 'slave'); 62 | ``` 63 | {% endmethod %} 64 | 65 | {% method %} 66 | ## .registerPool() 67 | Registers a pool of connections. 68 | 69 | {% common %} 70 | ```js 71 | store.registerPool({ 72 | client : 'mysql', 73 | connections: [{...}, {...}] 74 | }); 75 | ``` 76 | {% endmethod %} 77 | 78 | {% method %} 79 | ## .registerReplication() 80 | Registers replication connections. 81 | 82 | {% common %} 83 | ```js 84 | store.registerReplication({ 85 | client : 'mysql', 86 | connections: { 87 | master: [{...}, {...}], 88 | slave : [{...}, {...}, {...}] 89 | } 90 | }); 91 | ``` 92 | {% endmethod %} 93 | -------------------------------------------------------------------------------- /doc/API/wetland.md: -------------------------------------------------------------------------------- 1 | # Wetland 2 | Through wetland we are able to connect with the database, 3 | register entities and access methods that belong to different classes throughout the ORM. 4 | Here we are going to describe all of its methods and how to use them. 5 | 6 | ```js 7 | let wetland = new Wetland(); 8 | ``` 9 | 10 | {% method %} 11 | ## .destroyConnections() 12 | This method destroys all active connections, returning a Promise. 13 | 14 | {% common %} 15 | ```js 16 | wetland.destroyConnections(); 17 | ``` 18 | {% endmethod %} 19 | 20 | {% method %} 21 | ## .getConfig() 22 | Gets the wetland config. 23 | 24 | {% common %} 25 | ```js 26 | wetland.getConfig(); 27 | ``` 28 | {% endmethod %} 29 | 30 | {% method %} 31 | ## .getManager() 32 | This method gets a scoped entity manager. 33 | 34 | 35 | {% common %} 36 | ```js 37 | wetland.getManager(); 38 | ``` 39 | {% endmethod %} 40 | 41 | {% method %} 42 | ## .getMigrator() 43 | This method returns an instance of migrator, that you can use to create and run your database migrations. 44 | 45 | {% common %} 46 | ```js 47 | wetland.getMigrator(); 48 | ``` 49 | {% endmethod %} 50 | 51 | {% method %} 52 | ## .getStore() 53 | Gets a store by name. If no name is given, this method will return the default store. 54 | 55 | {% common %} 56 | ```js 57 | wetland.getStore('store'); 58 | ``` 59 | {% endmethod %} 60 | 61 | {% method %} 62 | ## .registerEntities() 63 | This method register multiple entities with the entity manager. 64 | Using this method is another way to register your entities, if you decide not to register them upon creating a wetland instance. 65 | 66 | {% common %} 67 | ```js 68 | wetland.registerEntities([entity1, entity2]); 69 | ``` 70 | {% endmethod %} 71 | 72 | {% method %} 73 | ## .registerEntity() 74 | Just like `.registerEntities()`, but only registering one entity with the entity manager. 75 | 76 | {% common %} 77 | ```js 78 | wetland.registerEntity(entity); 79 | ``` 80 | {% endmethod %} 81 | 82 | {% method %} 83 | ## .registerStore() 84 | This method registers a store with wetland. 85 | The first registered store using this method will be set as the default store. 86 | The second argument is an object with your store configuration. 87 | 88 | {% common %} 89 | ```js 90 | wetland.registerStore('myStore', {config: {...}}); 91 | ``` 92 | {% endmethod %} 93 | 94 | {% method %} 95 | ##.registerStores() 96 | Registers multiple stores with wetland. Stores must be passed as a single object. 97 | 98 | {% common %} 99 | ```js 100 | let stores = { 101 | store1 : {...}, 102 | store2 : {...} 103 | }; 104 | 105 | wetland.registerStores(stores); 106 | ``` 107 | {% endmethod %} 108 | -------------------------------------------------------------------------------- /doc/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 SpoonX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # ![Wetland](https://cdn.rawgit.com/SpoonX/wetland/391040eba795183550bfff01d7c0ca56d01b5530/wetland.svg) 2 | 3 | Wetland is a modern object-relational mapper (ORM) for node.js. 4 | 5 | - [View on github](https://github.com/SpoonX/wetland) 6 | - [View the quick start](https://wetland.spoonx.org/quick-start.html) 7 | - [View the tutorial](https://github.com/SpoonX/swetland) 8 | 9 | ## Features 10 | 11 | Wetland is based on the [JPA-spec](http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html) and therefore has some similarities to Hibernate and Doctrine. While some aspects of the ORM have been adapted to perform better in the Node.js environment and don't follow the specification to the letter for that reason, the JPA specification is a stable and well written specification that makes wetland structured and performant. 12 | 13 | **New!** Take a look at our [wetland tutorial](https://wetland.spoonx.org/Tutorial/setting-up.html). 14 | 15 | **New!** Wetland CLI now has its [own repository](https://github.com/SpoonX/wetland-cli). `npm i -g wetland-cli`. 16 | 17 | **New!** Wetland has a nice entity generator. Let us do the heavy lifting. [Repository can be found here](https://github.com/SpoonX/wetland-generator-entity). 18 | 19 | The major features this ORM provides are listed below. 20 | Looking at the tests will provide more detailed information, pending full documentation. 21 | 22 | * Unit of work 23 | * Derived tables 24 | * Transactions 25 | * Entity manager 26 | * Manager scopes 27 | * Cascade persist 28 | * Deep joins 29 | * Repositories 30 | * QueryBuilder 31 | * Mapping 32 | * MetaData 33 | * Entity proxy 34 | * Collection proxy 35 | * Criteria parser 36 | * More... 37 | 38 | ## Installation 39 | 40 | To install wetland run the following command: 41 | 42 | `npm i --save wetland` 43 | 44 | Typings are provided by default for TypeScript users. No additional typings need installing. 45 | 46 | ## Plugins / essentials 47 | 48 | - [Wetland CLI](https://github.com/SpoonX/wetland-cli) `npm i -g wetland-cli` 49 | - [Express middleware](https://github.com/SpoonX/express-wetland) 50 | - [Sails.js hook](https://github.com/SpoonX/sails-hook-wetland) 51 | - [Trailpack](https://github.com/SpoonX/trailpack-wetland) 52 | - [Entity generator](https://github.com/SpoonX/wetland-generator-entity) 53 | 54 | ## Usage 55 | 56 | Simple implementation example: 57 | 58 | ```js 59 | const Wetland = require('wetland').Wetland; 60 | const Foo = require('./entity/foo').Foo; 61 | const Bar = require('./entity/foo').Bar; 62 | const wetland = new Wetland({ 63 | stores: { 64 | simple: { 65 | client : 'mysql', 66 | connection: { 67 | user : 'root', 68 | database: 'testdatabase' 69 | } 70 | } 71 | }, 72 | entities: [Foo, Bar] 73 | }); 74 | 75 | // Create the tables. Async process, only here as example. 76 | // use .getSQL() (not async) in stead of apply (async) to get the queries. 77 | let migrator = wetland.getMigrator().create(); 78 | migrator.apply().then(() => {}); 79 | 80 | // Get a manager scope. Call this method for every context (e.g. requests). 81 | let manager = wetland.getManager(); 82 | 83 | // Get the repository for Foo 84 | let repository = manager.getRepository(Foo); 85 | 86 | // Get some results, and join. 87 | repository.find({name: 'cake'}, {populate: ['candles', 'baker', 'baker.address']}) 88 | .then(results => { 89 | // ... 90 | }); 91 | ``` 92 | 93 | ## License 94 | 95 | MIT 96 | -------------------------------------------------------------------------------- /doc/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [Installation](installation.md) 5 | * [Configuration](configuration.md) 6 | * [Quick Start](quick-start.md) 7 | * [Edge cases](edge-cases.md) 8 | * [Entity](entity.md) 9 | * [Seeding](seeding.md) 10 | * [Changelog](CHANGELOG.md) 11 | * [License](LICENSE.md) 12 | 13 | ## Tutorial 14 | 15 | * [Setting up](Tutorial/setting-up.md) 16 | * [Entities](Tutorial/entities.md) 17 | * [Snapshots](Tutorial/snapshots.md) 18 | * [Dev migrations](Tutorial/dev-migrations.md) 19 | * [Migrations](Tutorial/migrations.md) 20 | * [Relations](Tutorial/relations.md) 21 | * [Lifecycle callbacks](Tutorial/lifecycle-callbacks.md) 22 | * [EntityManager, Scope and UnitOfWork](Tutorial/entitymanager-scope-unitofwork.md) 23 | * [Repository](Tutorial/repository.md) 24 | * [QueryBuilder](Tutorial/querybuilder.md) 25 | * [Query](Tutorial/query.md) 26 | * [Joins](Tutorial/joins.md) 27 | * [Populator](Tutorial/populator.md) 28 | * [Bonus: stores](Tutorial/bonus-stores.md) 29 | 30 | ## API documentation 31 | 32 | * [Entity Manager](API/entity-manager.md) 33 | * [Entity Repository](API/entity-repository.md) 34 | * [Decorators](decorators.md) 35 | * [Mapping](API/mapping.md) 36 | * [Migrator](API/migrator.md) 37 | * [Query Builder](API/query-builder.md) 38 | * [Schema Builder](API/schema-builder.md) 39 | * [Scope](API/scope.md) 40 | * [Store](API/store.md) 41 | * [Unit of Work](API/unit-of-work.md) 42 | * [Wetland](API/wetland.md) 43 | -------------------------------------------------------------------------------- /doc/Tutorial/bonus-cli.md: -------------------------------------------------------------------------------- 1 | # Bonus: CLI 2 | Coming soon. 3 | 4 | * Writing CLI plugins 5 | * Using the CLI 6 | * CLI configuration -------------------------------------------------------------------------------- /doc/Tutorial/bonus-stores.md: -------------------------------------------------------------------------------- 1 | # Bonus: Stores 2 | Coming soon in **stores** near you. No, I'm not apologizing. 3 | -------------------------------------------------------------------------------- /doc/Tutorial/dev-migrations.md: -------------------------------------------------------------------------------- 1 | # Dev migrations 2 | > To clone the finished code for this part of the tutorial, run the following command: 3 | > 4 | > `git clone git@github.com:SpoonX/wetland-tutorial.git -b 3-dev-migrations --single-branch` 5 | > 6 | > To clone the base code for this part of the tutorial, run the following command: 7 | > 8 | > `git clone git@github.com:SpoonX/wetland-tutorial.git -b 2-entities --single-branch` 9 | > 10 | > **Github:** [Diff for this part of the tutorial](https://github.com/SpoonX/wetland-tutorial/compare/2-entities...3-dev-migrations?diff=split) - [Full repository on github](https://github.com/SpoonX/wetland-tutorial) 11 | 12 | In this part of the tutorial we'll be looking at snapshots, dev-migrations and migrations. 13 | 14 | ## Preparing 15 | We'll be working with migrations, and dev migrations. Migrations allow you to update the schema of your database, and revert to an older state, without having to do so manually. This takes away any "human error" you might produce during the migrations themselves. 16 | 17 | ### Snapshot 18 | As we briefly touched in the previous part of this tutorial, wetland makes use of snapshots for migrations. 19 | 20 | Snapshots are basically a "save" of your mapping that can be used to diff against other snapshots, or your current mapping. You can read more about them, and the motivation behind building this system [in the docs](../snapshots.html). 21 | 22 | In this part, we'll be adding stock support to our product entity. To allow wetland to calculate the difference between our current mapping, and the mapping we'll have when we're done, let's start off by creating a snapshot. 23 | 24 | ``` 25 | $ wetland snapshot create stock-support 26 | 27 | Success: Snapshot 'stock_support' created successfully! 28 | ``` 29 | 30 | _**Note:** if you don't supply a name for your snapshot (last parameter) the name of your git branche gets used by default._ 31 | 32 | _**Another note:** We'll cover migrations in the next step of this tutorial. The snapshot is for preparation._ 33 | 34 | ### Adding stock 35 | Open up `app/entity/Product.js` and add the following field to your mapping: 36 | 37 | ```js 38 | class Product { 39 | static setMapping(mapping) { 40 | mapping.forProperty('id').primary().increments(); 41 | 42 | mapping.field('name', {type: 'string'}); 43 | 44 | // This is the new field! 45 | mapping.field('stock', {type: 'integer', defaultTo: 0}); 46 | } 47 | } 48 | 49 | module.exports = Product; 50 | ``` 51 | 52 | Sweet, we now have a stock that defaults to `0` (no stock). Let's update our schema! 53 | 54 | ### Running dev migration 55 | Because we created our schema using dev migrations in the previous part of this tutorial, wetland now has something called a "dev snapshot". This is identical to a regular snapshot. The only difference is that it gets stored separately, and is managed by wetland internally. 56 | 57 | Running a dev migration, is going to diff our current schema (with the added stock) to the previous run (when we created our schema). Let's start off by checking what wetland _would_ do before actually applying anything. 58 | 59 | Run the following command in your terminal: `wetland migrator dev -d` 60 | 61 | The result should look like this: 62 | 63 | ```sql 64 | -- Queries for dev migrations: 65 | alter table "product" add column "stock" integer not null default '0' 66 | ``` 67 | 68 | That makes sense, that's what we added. Now let's apply the dev migration to our database schema: 69 | 70 | ``` 71 | wetland migrator dev -r 72 | 73 | Success: Dev migrations applied! 74 | ``` 75 | 76 | Just to show you that dev migrations store a new snapshot, I'd like you to run the dump command again: 77 | 78 | `wetland migrator dev -d` 79 | 80 | This time, the output should look like this: 81 | 82 | ```sql 83 | -- Queries for dev migrations: 84 | -- Nothing to do. 85 | ``` 86 | 87 | ## Automated dev migrations 88 | Cool, so our dev migration was a success! 89 | 90 | Obviously it would be a bit annoying to have to run this command with every change we make, so let's automate that. 91 | 92 | Open up `app.js`, and change the app.listen to be the following: 93 | 94 | ```js 95 | // Update the database schema 96 | wetland.getMigrator().devMigrations().then(() => { 97 | // Start server 98 | app.listen(3000, () => console.log('Inventory manager ready! Available on http://127.0.0.1:3000')); 99 | }); 100 | ``` 101 | 102 | Now, every time we start our server, dev migrations will run and apply. 103 | 104 | ## Next step 105 | Great, we've created a snapshot, added stock to our product, updated our schema and made sure that dev migrations will run every time we start our server. 106 | 107 | [Go to the next part](migrations.md). 108 | -------------------------------------------------------------------------------- /doc/Tutorial/media/count.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpoonX/wetland/78abeb380dbf0dfc9c0d5236dbb02e63c8980aaf/doc/Tutorial/media/count.gif -------------------------------------------------------------------------------- /doc/Tutorial/media/joins.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpoonX/wetland/78abeb380dbf0dfc9c0d5236dbb02e63c8980aaf/doc/Tutorial/media/joins.gif -------------------------------------------------------------------------------- /doc/Tutorial/media/snapshots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpoonX/wetland/78abeb380dbf0dfc9c0d5236dbb02e63c8980aaf/doc/Tutorial/media/snapshots.gif -------------------------------------------------------------------------------- /doc/Tutorial/query.md: -------------------------------------------------------------------------------- 1 | # Query 2 | > To clone the finished code for this part of the tutorial, run the following command: 3 | > 4 | > `git clone git@github.com:SpoonX/wetland-tutorial.git -b 10-query --single-branch` 5 | > 6 | > To clone the base code for this part of the tutorial, run the following command: 7 | > 8 | > `git clone git@github.com:SpoonX/wetland-tutorial.git -b 9-querybuilder --single-branch` 9 | > 10 | > **Github:** [Diff for this part of the tutorial](https://github.com/SpoonX/wetland-tutorial/compare/9-querybuilder...10-query?diff=split) - [Full repository on github](https://github.com/SpoonX/wetland-tutorial) 11 | 12 | In this part of the tutorial, we'll take a look at the Query object we get from the QueryBuilder. 13 | 14 | ## Some theory 15 | The query object is the final layer that stands between you and knex. It's what makes populate work, and allows wetland to hydrate your results. It's also really convenient when you want to do slightly more customzied things, like fetching raw results or scalar values. 16 | 17 | ### Execute 18 | Execute simply runs the query. If a parent was registered, it will make sure it only returns records associated with the parent. This method simply executes the knex instance query builder, and resolves to the raw result. 19 | 20 | ```js 21 | query.execute().then(result => { 22 | // Got the raw results! 23 | }); 24 | ``` 25 | 26 | ### Get result 27 | The execute method is where the most powerful features of wetland get used when it comes to fetching results. 28 | It allows you to fetch data in Entity form, giving you access to all the features that come with it. 29 | 30 | The steps executed by getResult are, in order: 31 | 32 | 1. Executes the query using `.execute()` 33 | 2. Hydrates the result into entities using wetland's optimized hydrator for awesome performance 34 | 3. Wraps the entities in proxies to track changes.. for awesome performance 35 | 4. Registers the entities with the identityMap, and marks them as clean in the UnitOfWork 36 | 5. Performs child queries _([QueryBuilder.populate()](./querybuilder.md#populate))_ 37 | 6. Attaches the results to each other using an indexed map..... for awesome performance! 38 | 39 | ```js 40 | query.getResult().then(result => { 41 | // Got the results! 42 | }); 43 | ``` 44 | 45 | ### Scalar values 46 | When producing a count, a sum, or any aggregated value for that matter, there's a high chance you just want just that value. 47 | 48 | ```js 49 | queryBuilder 50 | .select({count: 'u.id'}) 51 | .getQuery() 52 | .getSingleScalarResult() 53 | .then(numberOfUsers => { 54 | // We have our number! 55 | }); 56 | ``` 57 | 58 | ## Using it 59 | In fact, we want to use the `.getSingleScalarResult()` method to get the number of abundant and depleted products. 60 | 61 | Open up `app/repository/ProductRepository.js` and add the following count methods: 62 | 63 | ```js 64 | const {EntityRepository} = require('wetland'); 65 | 66 | class ProductRepository extends EntityRepository { 67 | findDepleted() { 68 | return this.find({stock: 0}); 69 | } 70 | 71 | findAbundant() { 72 | return this.getQueryBuilder('p') 73 | .select('p') 74 | .where({'p.stock': {'>': 4}}) 75 | .getQuery() 76 | .getResult(); 77 | } 78 | 79 | findDepletedCount() { 80 | return this.getQueryBuilder('p') 81 | .select({count: 'p.id'}) 82 | .where({stock: 0}) 83 | .getQuery() 84 | .getSingleScalarResult(); 85 | } 86 | 87 | findAbundantCount() { 88 | return this.getQueryBuilder('p') 89 | .select({count: 'p.id'}) 90 | .where({'p.stock': {'>': 4}}) 91 | .getQuery() 92 | .getSingleScalarResult(); 93 | } 94 | } 95 | 96 | module.exports = ProductRepository; 97 | ``` 98 | 99 | At this point, none of this should look new to you. We get a query builder, tell it we want a count, specify the criteria and proceed to ask the query for a single scalar result. 100 | 101 | ### Making it testable 102 | In order to test these new methods, we have to create two new endpoints. 103 | 104 | Open up `app/resource/product.js` and add the following routes: 105 | 106 | ```js 107 | // Get abundant products count 108 | router.get('/abundant/count', (req, res) => { 109 | req.getRepository(Product) 110 | .findAbundantCount() 111 | .then(result => res.json({count: result})) 112 | .catch(error => res.status(500).json({error})); 113 | }); 114 | 115 | // Get depleted products count 116 | router.get('/depleted/count', (req, res) => { 117 | req.getRepository(Product) 118 | .findDepletedCount() 119 | .then(result => res.json({count: result})) 120 | .catch(error => res.status(500).json({error})); 121 | }); 122 | ``` 123 | 124 | Again, nothing new except the `{count: result}` format, which is just because a Number is not a valud JSON value. 125 | 126 | Now start your server (or restart it, if it's still running): `node app`. 127 | 128 | And click these links to test the results: 129 | 130 | **[http://127.0.0.1:3000/product/depleted/count](http://127.0.0.1:3000/product/depleted/count)** 131 | 132 | ```json 133 | { 134 | "count": 2 135 | } 136 | ``` 137 | 138 | **[http://127.0.0.1:3000/product/abundant/count](http://127.0.0.1:3000/product/abundant/count)** 139 | 140 | ```json 141 | { 142 | "count": 1 143 | } 144 | ``` 145 | 146 | ## Next step 147 | Well, we know how the Query instance behaves. Also, we can count now! 148 | 149 | ![](./media/count.gif) 150 | 151 | Excuse the pun. Let's move on to joins! 152 | 153 | [Go to the next part](joins.md). 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /doc/Tutorial/setting-up.md: -------------------------------------------------------------------------------- 1 | # Setting up 2 | > To clone the finished code for this part of the tutorial, run the following command: 3 | > 4 | > `git clone git@github.com:SpoonX/wetland-tutorial.git -b 1-setting-up --single-branch` 5 | > 6 | > Find the full repository on github [here](https://github.com/SpoonX/wetland-tutorial). 7 | 8 | Before we get started, we need to set up a base project structure in which we can start building an application. 9 | 10 | In this first part of the tutorial, we'll be setting up a project and configuring a server. 11 | 12 | ## Initializing 13 | First, we need to get a directory set up to start working in. 14 | 15 | - Create a project directory somewhere. `mkdir ~/projects/nodejs/wetland-tutorial` 16 | - `npm init -y` 17 | - `git init` 18 | 19 | We now have a directory that's ready to get started in. 20 | 21 | ### Directory structure 22 | To get started, we'll create a directory structure for our application. I'll explain what the directories are for. 23 | 24 | Run this command to create the project directories: 25 | 26 | `mkdir -p app/{entity,repository,resource}` 27 | 28 | #### Structure explained 29 | ``` 30 | . 31 | └── app 32 | ├── entity 33 | ├── repository 34 | └── resource 35 | ``` 36 | 37 | ##### app 38 | App is the home of all the files we'll be writing in this tutorial, split into three different directories. 39 | 40 | ##### app/entity 41 | This directory is going to hold our entities. Entities are simple classes that represent tables in our database. 42 | 43 | ##### app/repository 44 | In this directory, we'll be storing our repositories. 45 | A repository is the layer between the domain (your logic) and the data mapper (wetland). They're what allow you to create queries and fetch data in a logical, expressive and flexible format. 46 | 47 | ##### app/resource 48 | Resources will be where our endpoints go. Our routes will be defined here, as well as the actions that handle calls to them. 49 | 50 | **Note:** this is not a recommended structure. It's simply the easiest structure for the purpose of this tutorial. 51 | 52 | ### Dependencies 53 | For this tutorial, we'll be using a couple of dependencies to make the application we'll be building look like a real world application. 54 | 55 | Run the following command to install the dependencies: 56 | 57 | `npm install --save express body-parser wetland express-wetland sqlite3` 58 | 59 | We're using [express](http://expressjs.com/), [body-parser](https://github.com/expressjs/body-parser) and [express-wetland](https://github.com/SpoonX/express-wetland) to make it easy to set up a simple server. Our first application will run on sqlite. 60 | 61 | ## Configuring our server 62 | Now the structure of our project has been laid out, it's time to start writing some code. First off, we'll be configuring a server. Because this tutorial is about _wetland_ and not about _building an api_, I'll just be supplying you with the code required to set up the server. 63 | 64 | ### App.js 65 | First, create a file called `app.js` in the root of the project, with the following contents. 66 | 67 | ```js 68 | const express = require('express'); 69 | const bodyParser = require('body-parser'); 70 | const expressWetland = require('express-wetland'); 71 | const Wetland = require('wetland').Wetland; 72 | const app = express(); 73 | const wetland = new Wetland(require('./wetland')); 74 | 75 | // Makes json prettier to read for the purpose of this tutorial 76 | app.set('json spaces', 2); 77 | 78 | // Middleware 79 | app.use(bodyParser.json()); 80 | app.use(bodyParser.urlencoded({extended: true})); 81 | app.use(expressWetland(wetland)); 82 | 83 | // Resources 84 | app.use('/product', require('./app/resource/product')); 85 | app.use('/category', require('./app/resource/category')); 86 | 87 | // Start server 88 | app.listen(3000, () => console.log('Inventory manager ready! Available on http://127.0.0.1:3000')); 89 | ``` 90 | 91 | ### wetland.js 92 | Now create a file in the root of your project called `wetland.js` with the following contents. 93 | 94 | ```js 95 | const path = require('path'); 96 | 97 | module.exports = { 98 | entityPath: path.resolve(process.cwd(), 'app', 'entity') 99 | }; 100 | ``` 101 | 102 | ### resource/product.js 103 | It's time to start creating the resources. First create a file in `resource/product.js` with the following contents. 104 | 105 | ```js 106 | const express = require('express'); 107 | const router = express.Router(); 108 | 109 | router.get('/', (req, res) => res.json({hello: 'from product.js'})); 110 | 111 | module.exports = router; 112 | ``` 113 | 114 | ### resource/category.js 115 | Now create a file in `resource/category.js` with the following contents. 116 | 117 | ```js 118 | const express = require('express'); 119 | const router = express.Router(); 120 | 121 | router.get('/', (req, res) => res.json({hello: 'from category.js'})); 122 | 123 | module.exports = router; 124 | ``` 125 | 126 | ## All done 127 | 128 | ### Test setup 129 | You can test this setup with the following steps: 130 | 131 | 1. run `node app.js` 132 | 2. Visit these links: [http://127.0.0.1:3000/category](http://127.0.0.1:3000/category) and [http://127.0.0.1:3000/product](http://127.0.0.1:3000/product) 133 | 3. If all goes well, you should get a response similar to this: `{hello: "from category.js"}` 134 | 135 | ### Next step 136 | Now our project is all ready to start building something. In the next part of this tutorial, we'll be creating our first entity, and take a look at the CLI tool. 137 | 138 | [Go to the next part](entities.md). 139 | -------------------------------------------------------------------------------- /doc/Tutorial/snapshots.md: -------------------------------------------------------------------------------- 1 | # Snapshots 2 | > This step in the tutorial doesn't have any code. 3 | > 4 | > Find the full repository on github [here](https://github.com/SpoonX/wetland-tutorial). 5 | 6 | This is a code-less tutorial part, where we look at what snapshots are and why they exist. 7 | 8 | ## Motivation 9 | Managing migrations has always been a black or white situation. 10 | You could either have dev migrations, and have the schema update automatically in development but find a solution for production deploys, or manage migrations yourself completely and write every change. 11 | Both of these have upsides and downsides. 12 | 13 | ## Dev migrations 14 | With dev migrations, you can develop your code faster. 15 | You don't have to worry about your schema constantly and can just make progress. 16 | This is awesome, but when pushing to production you have to figure out what you changed, and write some queries to update your schema in production. 17 | That's difficult and unreliable, very prone to errors. 18 | 19 | ## Constant migrations 20 | Using constant migrations, development becomes more complicated. 21 | You have to constantly update your schema, or first write your migrations. 22 | Even more annoying, writing migrations manually is just as prone to human error as writing queries. 23 | On the positive side, this gives you easier deploys as you already have managed migrations. 24 | 25 | ## Gray area 26 | For wetland, we decided to try and find the gray area. 27 | We want dev migrations to move forward quickly, but we also want migrations to document changes and have secure ways of managing updates and reverts. 28 | A really cool bonus would be to have migrations auto generated, but that's too much to ask for... Or is it? 29 | 30 | ### Schema diffing 31 | When trying to figure out how to update a schema, you would traditionally check with your database schema and perform a comparison. 32 | This is a tedious task, especially if you switch database adapters a lot because you want to run benchmarks for example. 33 | ORMs like waterline fetch all your data, perform some minor checks, re-create your schema and insert your data back in the table again. 34 | When this fails, you'll get a dump of the data it tried to insert. 35 | Schema diffing is key to reliable dev migrations, so we decided to take a different approach. 36 | 37 | ### Enter snapshots 38 | Snapshots are what allows wetland to provide the best of both worlds. 39 | It supplies dev migrations, as well as actual migrations based on schema differences. 40 | As a bonus, it generates these migrations for you, so all you have to do is check if wetland guessed right and add commit them into your favorite version control system. 41 | 42 | ## How it works 43 | Snapshots capture the state of your mappings into a compressed file, which can then later be used for diffing with other snapshots, and restoring state. 44 | 45 | In other words, you can tell wetland what your schema looked like before you started developing. 46 | When you're done coding a new feature, or fixing a bug, you ask wetland to generate your migrations for you. 47 | It then compares your current (dev-migrations updated) schema against the snapshot and creates a migration containing the `.up()` and `.down()` methods for you. 48 | 49 | ### Using snapshots 50 | Creating a snapshot is really simple. 51 | First, make sure you have the CLI installed, and wetland configured to run with it ([example](https://github.com/SpoonX/swetland/blob/master/wetland.js)). 52 | 53 | Snapshots get named based on the branch you're on by default (convention), as do migrations. 54 | You can decide not to use this convention by passing in a name. 55 | 56 | Here's what a typical development flow might look like: 57 | 58 | 1. Create a new branch to start your work. (`git checkout -b feat/contact`) 59 | 2. Create a new snapshot to tell wetland you're about to build awesome stuff. (`wetland snapshot create`) 60 | 3. Then actual build the awesome stuff. While doing that, your database will update (when running dev migrations). 61 | 4. Once done, create a migration file. (`wetland migrator create`) 62 | 5. Repeat. 63 | 64 | ![](./media/snapshots.gif) 65 | 66 | ## Next step 67 | We've briefly looked at what snapshots are, why they exist and how they can be applied. 68 | Let's use them in the next steps of this tutorial. 69 | 70 | [Go to the next part](dev-migrations.md). 71 | -------------------------------------------------------------------------------- /doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | To get your application up and running, you need to set some things up first. 3 | In this section we are going to give you an example of how to create a simple todo application using wetland. 4 | We already made an example of a [todo application](https://github.com/SpoonX/wetland/tree/master/examples/todo), 5 | feel free to take a look or try it yourself. 6 | 7 | {% method %} 8 | ## Stores 9 | You can use multiple databases to store your entities. Database configurations are stored in `wetland.stores`. 10 | If no stores are given upon wetland's instantiation, it defaults to an empty object. 11 | In this quick start we are going to use a mysql database as our store. 12 | [Here](installation.md#your-database) is a list of currently supported databases. 13 | For examples of how to use multiple databases, please refer to our [Cookbook.]() 14 | 15 | {% sample lang="js" %} 16 | ```js 17 | const Wetland = require('wetland').Wetland; 18 | 19 | const wetland = new Wetland({ 20 | stores: { 21 | myStore: { 22 | client: 'mysql', 23 | connection: { 24 | host : 'localhost', 25 | user : 'root', 26 | database: 'database' 27 | } 28 | } 29 | } 30 | }); 31 | ``` 32 | 33 | {% sample lang="ts" %} 34 | ```js 35 | import {Wetland} from 'wetland'; 36 | 37 | const wetland = new Wetland({ 38 | stores: { 39 | myStore: { 40 | client: 'mysql', 41 | connection: { 42 | host : 'localhost', 43 | user : 'root', 44 | database: 'wetland_database' 45 | } 46 | } 47 | } 48 | }); 49 | ``` 50 | {% endmethod %} 51 | 52 | {% method %} 53 | ## Default Store 54 | To set one of your stores to be the default store, you can either change your store key to `defaultStore` on 55 | `wetland.stores` or use your custom name as a value on `wetland.defaultStore`. 56 | 57 | {% common %} 58 | ```js 59 | const wetland = new Wetland({ 60 | stores: { 61 | defaultStore: {...} 62 | } 63 | }); 64 | ``` 65 | 66 | {% common %} 67 | ```js 68 | const wetland = new Wetland({ 69 | stores: { 70 | myStore: {...} 71 | }, 72 | defaultStore: 'myStore' 73 | }); 74 | ``` 75 | {% endmethod %} 76 | 77 | {% method %} 78 | ## Entities 79 | Wetland holds your entities on `wetland.entities`. 80 | One way to register your entities is requiring each of your entities on your main file, and then passing them inside an array 81 | upon creating a new wetland instance. 82 | 83 | {% sample lang="js" %} 84 | ```js 85 | const Entity1 = require('./entities/Entity1'); 86 | const Entity2 = require('./entities/Entity2'); 87 | const wetland = new Wetland({ 88 | entities: [Entity1, Entity2] 89 | }); 90 | ``` 91 | 92 | {% sample lang="ts" %} 93 | ```js 94 | import {Entity1} from './entities/Entity1'; 95 | import {Entity2} from './entities/Entity2'; 96 | 97 | const wetland = new Wetland({ 98 | entities: [Entity1, Entity2] 99 | }); 100 | ``` 101 | {% endmethod %} 102 | 103 | {% method %} 104 | ## Mapping 105 | Using the mapping config key you're able to configure some of the mapping's behavior. 106 | 107 | {% common %} 108 | ```js 109 | const wetland = new Wetland({ 110 | mapping: { 111 | // Automatically convert camel-cased property names... 112 | // ... to underscored column-names. 113 | defaultNamesToUnderscore: false, 114 | 115 | // Default values for mappings. 116 | // Useful to set auto-persist (defaults to empty array). 117 | defaults: {cascades: ['persist']} 118 | } 119 | }); 120 | ``` 121 | 122 | {% endmethod %} 123 | 124 | {% method %} 125 | ## Entity Path 126 | If you have multiple entities on a single directory and you want to register them without having to require each one of them, 127 | you can simply write the path to your directory on `wetland.entityPath` to register all the entities in that directory. 128 | 129 | {% common %} 130 | ```js 131 | const wetland = new Wetland({ 132 | entityPath: __dirname + './entities' 133 | }); 134 | ``` 135 | {% endmethod %} 136 | 137 | {% method %} 138 | ## Default names to underscore 139 | By default, the name for your columns in the database is the name of your property. 140 | If you supply a `name` in your field definition, this will be used instead. 141 | 142 | However, it's also possible to tell Wetland to name your columns based on the property name, 143 | replacing camel casing by underscores. 144 | 145 | As an example, property `firstName` would create a column with the name `first_name`. 146 | 147 | {% common %} 148 | ```js 149 | const wetland = new Wetland({ 150 | mapping: { 151 | defaultNamesToUnderscore: true 152 | } 153 | }); 154 | ``` 155 | {% endmethod %} 156 | 157 | {% method %} 158 | ## Use foreign keys 159 | By default, wetland uses foreign keys for relations. It's possible to disable them. 160 | 161 | **Note:** foreign keys are disabled for SQLite due to alter restrictions. 162 | 163 | {% common %} 164 | ```js 165 | const wetland = new Wetland({ 166 | useForeignKeys: false 167 | }); 168 | ``` 169 | {% endmethod %} 170 | 171 | {% method %} 172 | ## Entity Paths 173 | If you have multiple entities in multiple directories, you can register all of them on `wetland.entityPaths`. 174 | It works the same way as `entityPath` but you pass all your paths inside of an array. 175 | Wetland will register all your entities in all given directories. 176 | 177 | {% common %} 178 | ```js 179 | const wetland = new Wetland({ 180 | entityPath: [__dirname + './entities', __dirname + './moreEntities'] 181 | }); 182 | ``` 183 | {% endmethod %} 184 | 185 | {% method %} 186 | ## Debug 187 | For debugging, simply set `wetland.debug` to true. 188 | 189 | {% common %} 190 | ```js 191 | const wetland = new Wetland({ 192 | debug: true 193 | }); 194 | ``` 195 | {% endmethod %} 196 | 197 | {% method %} 198 | ## Migrator 199 | To configure a migration, change the properties on `wetland.migrator`. The options for the migrator config 200 | can be found on the [Migrator API.](./API/migrator.md) 201 | 202 | {% common %} 203 | ```js 204 | const wetland = new Wetland({ 205 | migrator: {...} 206 | }); 207 | ``` 208 | {% endmethod %} 209 | -------------------------------------------------------------------------------- /doc/decorators.md: -------------------------------------------------------------------------------- 1 | # Decorator 2 | 3 | Decorators are a way to decorate your entities so that you don't need to use setMapping ES6 syntax. 4 | 5 | Eventually it looks like this : 6 | 7 | ```js 8 | @entity() 9 | class User { 10 | 11 | @increments() 12 | @primary() 13 | id; 14 | 15 | @field({type: 'string'}) 16 | username; 17 | 18 | @field({type: 'string'}) 19 | password; 20 | } 21 | ``` 22 | 23 | ## Warnings 24 | 25 | Entities only work using typescript or babel, you will need to enable the related flags. At the time of writing babel imposes user to set default value to each properties of the class so that those properties are writable. The entity just presented will look like this : 26 | 27 | ```js 28 | @entity() 29 | class User { 30 | 31 | @increments() 32 | @primary() 33 | id = null; 34 | 35 | @field({type: 'string'}) 36 | username = null; 37 | 38 | @field({type: 'string'}) 39 | password = null; 40 | } 41 | ``` 42 | 43 | ## The decorators 44 | 45 | ### Class decorators 46 | 47 | #### @autoFields 48 | 49 | Adds auto fields to your entity (id, createdAt, updatedAt). 50 | 51 | ```js 52 | @autoFields() 53 | class Foo {} 54 | ``` 55 | 56 | #### @autoUpdatedAt 57 | 58 | Adds an updatedAt field to your entity. 59 | 60 | **Note:** this only sets the initial value, and doesn't maintain the value between updates. Use the beforeUpdate hook for that. 61 | 62 | ```js 63 | @autoUpdatedAt() 64 | class Foo {} 65 | ``` 66 | 67 | #### @autoCreatedAt 68 | 69 | Adds an createdAt field to your entity. 70 | 71 | **Note:** this sets the initial value. 72 | 73 | ```js 74 | @autoCreatedAt() 75 | class Foo {} 76 | ``` 77 | 78 | #### @autoPK 79 | 80 | Adds an auto incremented primary key with the name `id` to your entity. 81 | 82 | ```js 83 | @autoPK() 84 | class Foo {} 85 | ``` 86 | 87 | #### @entity 88 | 89 | Declares a class as an entity. 90 | 91 | - Default name and repository 92 | 93 | ```js 94 | @entity() 95 | class Foo {} 96 | ``` 97 | 98 | - Custom name and repository 99 | 100 | ```js 101 | @entity({repository: MyRepository, name: 'custom'}) 102 | class Foo {} 103 | ``` 104 | 105 | #### @index 106 | 107 | Create an index on one of the field of the entity. 108 | 109 | - Compound 110 | 111 | ```js 112 | @index('idx_something', ['property1', 'property2']) 113 | class Foo {} 114 | ``` 115 | 116 | - Single 117 | 118 | ```js 119 | @index('idx_something', ['property']) 120 | @index('idx_something', 'property') 121 | class Foo {} 122 | ``` 123 | 124 | - Generated index name "idx_property" 125 | 126 | ```js 127 | @index('property') 128 | @index(['property1', 'property2']) 129 | class Foo {} 130 | ``` 131 | 132 | - Directly on a property 133 | 134 | ```js 135 | class Foo { 136 | @index() 137 | public name: string; 138 | } 139 | ``` 140 | 141 | - Directly on a property with a custom index name 142 | 143 | ```js 144 | class Foo { 145 | @index('idx_custom') 146 | public name: string; 147 | } 148 | ``` 149 | 150 | #### @uniqueConstraint 151 | 152 | Creates a unique constraint on one of the field of the entity. 153 | 154 | - Compound: 155 | 156 | ```js 157 | @uniqueConstraint('something_unique', ['property1', 'property2']) 158 | class Foo {} 159 | ``` 160 | 161 | - Single: 162 | 163 | ```js 164 | @uniqueConstraint('something_unique', ['property']) 165 | @uniqueConstraint('something_unique', 'property') 166 | class Foo {} 167 | ``` 168 | 169 | - Generated uniqueConstraint name: 170 | 171 | ```js 172 | @uniqueConstraint('property') 173 | @uniqueConstraint(['property1', 'property2']) 174 | class Foo {} 175 | ``` 176 | 177 | - Directly on a property 178 | 179 | ```js 180 | class Foo { 181 | @uniqueConstraint() 182 | public name: string; 183 | } 184 | ``` 185 | 186 | - Directly on a property with a custom constraint name 187 | 188 | ```js 189 | class Foo { 190 | @uniqueConstraint('custom_constraint_name') 191 | public name: string; 192 | } 193 | ``` 194 | 195 | ### Properties decorators 196 | 197 | #### @field 198 | 199 | Creates a field of a certain type with specific options. 200 | 201 | - Default (property) name 202 | 203 | ```js 204 | @entity() 205 | class Foo { 206 | @field({type:'string', length: 255}) 207 | username; 208 | } 209 | ``` 210 | 211 | - Custom name 212 | 213 | ```js 214 | @entity() 215 | class Foo { 216 | @field('password_hash', {type: 'string', length: 255}) 217 | password; 218 | } 219 | ``` 220 | 221 | #### @primary 222 | 223 | Mark a property as primary. 224 | 225 | ```js 226 | @entity() 227 | class Foo { 228 | @primary() 229 | id; 230 | } 231 | ``` 232 | 233 | 234 | #### @generatedValue 235 | 236 | Add a generated value directive for the property. 237 | 238 | ```js 239 | @entity() 240 | class Foo { 241 | @generatedValue('autoIncrement') 242 | id; 243 | } 244 | ``` 245 | 246 | #### @increments 247 | 248 | Make the property auto increment. 249 | 250 | ```js 251 | @entity() 252 | class Foo { 253 | @increments() 254 | id; 255 | } 256 | ``` 257 | 258 | #### @oneToOne 259 | 260 | Creates a one to one relationship with another entity which will be populated using the decorated property. The decorated property will be populated with a ([0, 1]) target entity. 261 | 262 | ```js 263 | @entity() 264 | class Foo { 265 | @oneToOne({targetEntity: 'Bar', mappedBy: 'foo'}) 266 | bar; 267 | } 268 | ``` 269 | 270 | #### @oneToMany 271 | 272 | Creates a one to many relationship. The decorated property will be populated with many (>= 0) target entities. 273 | 274 | ```js 275 | @entity() 276 | class Foo { 277 | @oneToMany({targetEntity: 'Bar', inversedBy: 'foo'}) 278 | bars = []; 279 | } 280 | ``` 281 | 282 | #### @manyToOne 283 | 284 | Create a many to one relationship. The decorated property will be populated with a ([0, 1]) target entity. 285 | 286 | ```js 287 | @entity() 288 | class Foo { 289 | @manyToOne({targetEntity: 'Bar', mappedBy: 'foo'}) 290 | bar; 291 | } 292 | ``` 293 | 294 | #### @manyToMany 295 | 296 | Create a many to many relationship. The decorated property will be populated with many (>= 0) target entities. 297 | 298 | ```js 299 | @entity() 300 | class Foo { 301 | @manyToMany({targetEntity: 'Bar', inversedBy: 'foo'}) 302 | bars = []; 303 | } 304 | ``` 305 | 306 | 307 | -------------------------------------------------------------------------------- /doc/edge-cases.md: -------------------------------------------------------------------------------- 1 | # Edge cases 2 | When straying away from best-practises you might find yourself struggling to understand what's happening. 3 | This document is an attempt to help you catch these edge-cases. 4 | 5 | ## Store and database changes 6 | 7 | Wetland can be a bit picky if you don't know its inner-working. 8 | One error you can rapidly encounter is this one : 9 | 10 | ```bash 11 | ER_BAD_TABLE_ERROR: Unknown table 'my_database.table-name' 12 | ``` 13 | 14 | The source of this error is most probably a change in your store settings (e.g: changing database adapter, changing database name...) or database (e.g: recreating your database). 15 | Technically what's happening is that wetland doesn't know that it needs to update the database schema because the way migrations work is by looking at your [snapshots](https://wetland.spoonx.org/snapshots.html) in your `.data` directory, and the last snapshot wetland knows about was before your change. 16 | 17 | Solution is simple type clear your `.data` directory : `rm -r .data`. 18 | 19 | ## Dev migrations 20 | Wetland migrations are pretty powerful, but have some minor limitations. 21 | 22 | Dev migrations can't always do what should be done. This includes renaming columns and changing column definitions. 23 | 24 | When you change the definition of a column, it gets dropped and created again. 25 | When you change the name of a column, it gets dropped and created again. 26 | When you change the name of a table, any relationships, unique constraints and indexes it has get dropped and recreated. 27 | 28 | ## .toObject() and references 29 | > **Best-practise:** 30 | > When manipulating the contents returned for JSON stringified entities, always place the toJSON 31 | > method on the target entity. __Never__ manipulate the properties of relations, unless absolutely needed. 32 | 33 | For performance reasons, when calling `Entity.toObject()` (for example from within a `.toJSON()` call), 34 | wetland **does not** clone relations. 35 | This means that any changes made to relations on the result of `Entity.toObject()` will be **by reference**. 36 | 37 | The reason for this behaviour is that cloning objects requires memory and CPU, and you almost never need it. 38 | For that reason, it's probably best to stick with the best practise. 39 | If you do need to stray from these guidelines, you can use the following work-around. 40 | 41 | {% method %} 42 | ### Work-around 43 | To work around this issue, you can call `Entity.toObject()` on the relation you wish to manipulate. 44 | If the relation has its own toJSON(), you can call that instead. 45 | 46 | {% sample lang="js" %} 47 | ```js 48 | const Entity = require('wetland').Entity; 49 | 50 | module.exports = class Post extends Entity { 51 | toJSON() { 52 | let object = this.toObject(); 53 | object.user = Entity.toObject(object.user); 54 | // Or object.user.toJSON() if that exists. 55 | 56 | object.user.username += ' (original author)'; 57 | 58 | return object; 59 | } 60 | }; 61 | ``` 62 | 63 | {% sample lang="ts" %} 64 | ```js 65 | import {Entity} from 'wetland'; 66 | 67 | export class Post extends Entity { 68 | toJSON() { 69 | let object = this.toObject(); 70 | object.user = Entity.toObject(object.user); 71 | // Or object.user.toJSON() if that exists. 72 | 73 | object.user.username += ' (original author)'; 74 | 75 | return object; 76 | } 77 | } 78 | ``` 79 | {% endmethod %} 80 | -------------------------------------------------------------------------------- /doc/entity.md: -------------------------------------------------------------------------------- 1 | # Entity 2 | 3 | Entities are objects with a conceptual identity assigned within your domain. They're regular classes that hold a state you describe. Entities are mapped when registered with the entity manager, allowing you to describe what the entity looks like on the persisted side (the database). 4 | 5 | In short, entities are simple classes that describe tables in your database, of which instances hold state in your application and map to specific rows in your database. 6 | 7 | ## Entity example 8 | 9 | Here's a user with an id in autoincrement mode, a username and a password. 10 | 11 | ```js 12 | class User { 13 | /** 14 | * @param {Mapping} mapping 15 | * 16 | * @see https://wetland.spoonx.org/API/mapping.html 17 | */ 18 | static setMapping(mapping) { 19 | // Primary key 20 | mapping.forProperty('id').increments().primary(); 21 | 22 | // Fields 23 | mapping.field('username', {type: 'string'}); 24 | mapping.forProperty('password').field({type: 'string'}); 25 | } 26 | } 27 | ``` 28 | 29 | ## Lifecycle callbacks 30 | 31 | You may want to do some stuff after or before a CREATE, UPDATE OR DELETE action on an entity, this is the way to do it. 32 | 33 | ```js 34 | class AnyEntity extends Entity { 35 | 36 | beforeCreate(entityManager) { 37 | // Will be executed before creation of the entity in the database. 38 | } 39 | 40 | afterCreate (entityManager) { 41 | // Will be executed after creation of the entity in the database. 42 | } 43 | 44 | beforeUpdate (newValues, entityManager) { 45 | // Will be executed before update of the entity. 46 | } 47 | 48 | afterUpdate(entityManager) { 49 | // Will be executed after update of the entity 50 | } 51 | 52 | beforeRemove (entityManager) { 53 | // Will be executed before removal of the entity 54 | } 55 | 56 | afterRemove (entityManager) { 57 | // Will be executed after removal of the entity 58 | } 59 | } 60 | ``` 61 | 62 | ### An example with validation and encryption 63 | 64 | In this example we create a validation schema with [Joi](https://github.com/hapijs/joi) and apply it before any CREATE action. 65 | If given data is validated the given password encrypted using [bcrypt](https://en.wikipedia.org/wiki/Bcrypt). 66 | 67 | ```js 68 | const bcrypt = require('bcrypt'); 69 | const Joi = require('joi'); 70 | const {Entity} = require('wetland'); 71 | 72 | const validationSchema = Joi.object().keys({ 73 | username: Joi.string().alphanum().min(3).max(30).required(), 74 | password: Joi.string().min(8).max(100).required() 75 | }); 76 | 77 | module.exports = class User extends Entity { 78 | 79 | constructor() { 80 | super(...arguments); 81 | 82 | // Default values for username and password 83 | this.username = null; 84 | this.password = null; 85 | // This constructor is fully optional 86 | } 87 | 88 | static setMapping(mapping) { 89 | // Primary key 90 | mapping.forProperty('id').increments().primary(); 91 | 92 | // Fields 93 | mapping.field('username', {type: 'string'}); 94 | mapping.field('password', {type: 'string'}); 95 | } 96 | 97 | beforeCreate() { 98 | return Joi.validate(this, validationSchema, error => { 99 | 100 | if (error) { 101 | throw (error); 102 | } 103 | 104 | return bcrypt.hash(this.password, 15) 105 | .then(hash => { 106 | this.password = hash; // Only the hash is stored that way, this is the way you want to do it. 107 | }); 108 | }); 109 | } 110 | 111 | }; 112 | ``` 113 | -------------------------------------------------------------------------------- /doc/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | To install wetland run the following command from project root: 3 | 4 | `$ npm install --save wetland` 5 | 6 | For the cli: 7 | 8 | `npm i -g wetland-cli` 9 | 10 | ## Prerequisites 11 | ### Node.js (v6.0.0+) 12 | Wetland was built on ES6, so your Node.js version must be v6.0.0 or higher to support all of its features. 13 | You can update your current node version by running: 14 | 15 | `$ nvm install node` 16 | 17 | If you are new to Node.js, you can download its latest version [here](https://nodejs.org/en/download/current/). You will need to complete this installation to run wetland. 18 | 19 | ### Your database 20 | Wetland offers support for all of the following: 21 | 22 | ##### PostgresSQL 23 | `$ npm install --save pg` 24 | 25 | ##### SQLite3 26 | `$ npm install --save sqlite3` 27 | 28 | ##### MySQL 29 | `$ npm install --save mysql` 30 | 31 | ##### MySQL2 32 | `$ npm install --save mysql2` 33 | 34 | ##### MariaDB 35 | `$ npm install --save mariasql` 36 | 37 | ##### Oracle 38 | `$ npm install --save strong-oracle` 39 | 40 | ##### MsSQL 41 | `$ npm install --save mssql` 42 | -------------------------------------------------------------------------------- /doc/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | Here we will show you how to use basic methods to apply changes and fetch items from your database. 3 | More detailed examples can be found in our Cookbook. 4 | 5 | ## Prerequisites 6 | For this guide, we will assume you have both [node](https://nodejs.org/en/download/current/) and 7 | [npm](https://www.npmjs.com/get-npm) installed. 8 | Your node version must be 6.0 or above to be compatible with all ES6 features. We will also assume you have one of the 9 | [databases](./installation.md#your-database) supported by wetland. 10 | 11 | {% method %} 12 | ## Creating an entity 13 | One way of creating your entities is creating a file for each of them on the same directory. 14 | In this example, we will store them in `./entities`. [Mapping methods](./API/mapping.md) are used to configure your 15 | entity's schema. Here is what an entity file looks like: 16 | 17 | {% sample lang="js" %} 18 | ```js 19 | class User { 20 | static setMapping(mapping) { 21 | mapping.forProperty('id').primary().increments(); 22 | mapping.field('name', {type: 'string', size: 40}); 23 | mapping.field('phone', {type: 'integer'}); 24 | } 25 | } 26 | 27 | module.exports.User = User; 28 | ``` 29 | {% sample lang="ts" %} 30 | ```js 31 | export class User { 32 | static setMapping(mapping) { 33 | mapping.forProperty('id').primary().increments(); 34 | mapping.field('name', {type: 'string', size: 40}); 35 | mapping.field('phone', {type: 'integer'}); 36 | } 37 | } 38 | ``` 39 | {% endmethod %} 40 | 41 | {% method %} 42 | ## Implementing wetland 43 | In this quick start, we will register both our entities and our default store upon wetland's instantiation. Wetland 44 | will fetch all entity files on the `wetland.entityPath` directory and register each one of them. 45 | There are many ways to register entities and stores, detailed examples can be found in our Cookbook. 46 | 47 | {% sample lang="js" %} 48 | ```js 49 | const Wetland = require('wetland').Wetland; 50 | const wetland = new Wetland({ 51 | stores: { 52 | defaultStore: { 53 | client : 'mysql', 54 | connection: { 55 | host : '127.0.0.1', 56 | username: 'root', 57 | database: 'my_database' 58 | } 59 | } 60 | }, 61 | entityPath: __dirname + './entities' 62 | }); 63 | 64 | ``` 65 | 66 | {% sample lang="ts" %} 67 | ```js 68 | import {Wetland} from 'wetland'; 69 | 70 | const wetland = new Wetland({ 71 | stores: { 72 | defaultStore: { 73 | client : 'mysql', 74 | connection: { 75 | host : '127.0.0.1', 76 | username: 'root', 77 | database: 'my_database' 78 | } 79 | } 80 | }, 81 | entityPath: __dirname + './entities' 82 | }); 83 | ``` 84 | {% endmethod %} 85 | 86 | {% method %} 87 | ## "I'd like to speak to the manager!" 88 | To instantiate the entity manager all you need to do is call `.getManager()` on your wetland instance. 89 | The entity manager distributes scopes and supplies some core methods to be used in your application. 90 | 91 | {% sample lang="js" %} 92 | ```js 93 | let manager = wetland.getManager(); 94 | ``` 95 | 96 | {% sample lang="ts" %} 97 | ```js 98 | let manager = wetland.getManager(); 99 | ``` 100 | {% endmethod %} 101 | 102 | {% method %} 103 | ## Creating new rows 104 | Creating a new row is very easy. 105 | All you have to do is create a new instance of your entity and add its properties like with any object. 106 | 107 | {% common %} 108 | ```js 109 | let newUser = new User(); 110 | newUser.name = 'Wesley'; 111 | ``` 112 | {% endmethod %} 113 | 114 | {% method %} 115 | ## Persisting changes 116 | To persist changes into your database, call `.persist()` to stage this entity to be persisted and `.flush()` to apply those changes. 117 | 118 | {% common %} 119 | ```js 120 | manager.persist(newUser).flush().then(() => console.log('New user created')); 121 | ``` 122 | {% endmethod %} 123 | 124 | {% method %} 125 | ## Fetching from the database 126 | To fetch rows from your database, call `.getRepository()` to specify which table you are fetching from and `.find()` to 127 | fetch based on your criteria. 128 | 129 | {% common %} 130 | ```js 131 | manager.getRepository(User).find({name: 'Wesley'}).then(); 132 | ``` 133 | {% endmethod %} 134 | -------------------------------------------------------------------------------- /doc/seeding.md: -------------------------------------------------------------------------------- 1 | # Seeding 2 | 3 | Seeding consist in populating the database from fixtures. 4 | 5 | ## Fixtures 6 | 7 | A `fixture` is file which represents entities belonging to a repository. 8 | In wetland fixtures can be written in `JSON` and `csv` (If you think we need to support more don't hesitate to create a PR). 9 | 10 | ### Types of file 11 | 12 | #### JSON 13 | 14 | For `json` entities are represented by object in an array. Each object in the array is an entity. 15 | 16 | ##### Example 17 | 18 | Post.json 19 | ```json 20 | [ 21 | { 22 | "title": "Main post", 23 | "content": "Content...." 24 | }, 25 | { 26 | "title": "Test", 27 | "content": "Content...." 28 | } 29 | ] 30 | ``` 31 | 32 | #### CSV 33 | 34 | For `csv` entities are represented by a line (column support is not here but an issue is opened !) expect for the first line which represents the field name. 35 | 36 | ##### Example 37 | 38 | Pet.csv 39 | ```csv 40 | id,name 41 | 9,Kyle 42 | 2,Jill 43 | 10,Jullia 44 | ``` 45 | 46 | ## Types of seeding 47 | 48 | Wetland supports $$2^2 = 4$$ modes of seeding. 49 | 50 | ## Safe or clean 51 | 52 | ### Safe 53 | 54 | Safe seeding refers to the concept of verifying if a record already exist before seeding it, there's little risk associated with it that's why we call it safe seeding... 55 | 56 | ### Clean 57 | Clean seeding refers to the concept of clearing the database before seeding. 58 | 59 | ## Lifecycle or no lifecycle 60 | 61 | ### Lifecycle 62 | 63 | The lifecycle mode means that features will go through the lifecycles before being inserted : that's the default mode. 64 | 65 | ## No lifecyle 66 | 67 | The no lifecycle mode means that feature will not go through the lifecyles before being inserted. 68 | 69 | 70 | ## Setup 71 | 72 | ### Fixtures directory and file type support 73 | 74 | ``` 75 | └── fixtures 76 | ├── User.json 77 | ├── Pet.csv 78 | └── Entity.extension 79 | ``` 80 | 81 | Everything must be in a single folder, subfolder are not supported for the moment. 82 | The name of the file **must** be the name of the entity, the extension **must** either be `csv` or `json`. 83 | 84 | ### Config 85 | 86 | The seeder has to be configured. 87 | 88 | ```js 89 | const config = { 90 | seed : { 91 | fixturesDirectory : 'fixtures', // Each filename is an entity 92 | bypassLifecyclehooks: true, 93 | clean : false 94 | }, 95 | entities : [] 96 | } 97 | ``` 98 | 99 | ## The code 100 | 101 | If you want to use the seeder you must ask wetland to give you one. 102 | 103 | ```js 104 | const seeder = wetland.getSeeder(); 105 | ``` 106 | 107 | ### Clean seeding 108 | 109 | If you do clean seeding you should use the seeder like this : 110 | 111 | ```js 112 | const migrator = wetland.getMigrator(); 113 | const seeder = wetland.getSeeder(); 114 | const cleaner = wetland.getCleaner(); 115 | 116 | cleaner.clean() // Will clean the database, NO MAGICAL GOING BACK 117 | .then(() => migrator.devMigrations(false)) // Will actually do the migrations : needed here because the clean method wipes the database entirely 118 | .then(() => seeder.seed()) // Will seed accordingly to the configuration you gave wetland 119 | ``` 120 | 121 | ## Safe seeding 122 | 123 | If you want to do safe seeding you should use the seeder like this : 124 | 125 | ```js 126 | const migrator = wetland.getMigrator(); 127 | const seeder = wetland.getSeeder(); 128 | 129 | migrator.devMigrations(false) // Will migrate the database 130 | .then(() => seeder.seed()) // Will seed accordingly to the configuration you gave wetland 131 | ``` 132 | 133 | All of the above assume you are using the seeder in the dev environment : most likely the most common use case would be tests and dev setup (seeding your database some data for development). But you could chose to use it for production but then most likely you want to stay safe and not use dev migrations effectively just doing this : 134 | 135 | ```js 136 | const seeder = wetland.getSeeder(); 137 | 138 | seeder.seed() 139 | ``` 140 | -------------------------------------------------------------------------------- /doc/styles/ebook.css: -------------------------------------------------------------------------------- 1 | /* CSS for ebook */ 2 | -------------------------------------------------------------------------------- /examples/todo/README.md: -------------------------------------------------------------------------------- 1 | # Example todo 2 | This is a small example utilizing wetland to create a todo application. 3 | 4 | It shows you how to create a schema, run the migrator, query the database, apply cascaded persists and more. 5 | 6 | ## Setting up 7 | 8 | Create a project directory, and navigate to it. 9 | 10 | - `npm init -y` 11 | - `npm i sqlite3 wetland --save` 12 | - `cp -r node_modules/wetland/examples/todo/* .` 13 | 14 | The project is all set up now. 15 | 16 | ## Mappings 17 | 18 | *The mappings for List* 19 | ```js 20 | class List { 21 | static setMapping(mapping) { 22 | mapping.forProperty('id').primary().increments(); 23 | mapping.field('name', {type: 'string', size: 24}); 24 | mapping.forProperty('todos') 25 | .oneToMany({targetEntity: Todo, mappedBy: 'list'}) 26 | .cascade(['persist']); 27 | } 28 | } 29 | ``` 30 | 31 | *The mappings for Todo* 32 | ```js 33 | class Todo { 34 | static setMapping(mapping) { 35 | mapping.forProperty('id').primary().increments(); 36 | mapping.field('task', {type: 'string'}); 37 | mapping.field('done', {type: 'boolean', defaultTo: false}); 38 | mapping.forProperty('list') 39 | .manyToOne({targetEntity: 'List', inversedBy: 'todos'}) 40 | .joinColumn({onDelete: 'cascade'}); 41 | } 42 | } 43 | ``` 44 | 45 | ## Running 46 | To run the examples, follow the following commands. 47 | Bear in mind that both `.flush()` and `.execute()` return Promises that need to be handled. 48 | 49 | **NOTE:** Run the `setup` command first. Otherwise there won't be a schema to query against. 50 | 51 | ### setup 52 | Run using `node todo setup`. 53 | 54 | This sets up the schema based on your entity mappings. 55 | 56 | ```js 57 | return wetland.getMigrator().devMigrations() 58 | .then(() => console.log('Tables created')); 59 | ``` 60 | 61 | ### create-list 62 | Run using `node todo create-list `. 63 | 64 | This will create and persist the new list entity into the database. 65 | 66 | ```js 67 | let newList = new List; 68 | newList.name = list; 69 | 70 | return manager.persist(newList).flush(); 71 | ``` 72 | 73 | ### create-full 74 | Run using `node todo create-full [todo, todo, ...]`. 75 | 76 | This will create and persist both the list and the todo(s) entities. 77 | 78 | ```js 79 | let todos = parameters.splice(4); 80 | let newList = new List; 81 | let newTodos = []; 82 | 83 | todos.forEach(todo => { 84 | let newTodo = new Todo; 85 | newTodo.task = todo; 86 | 87 | newTodos.push(newTodo); 88 | }); 89 | 90 | newList.name = list; 91 | newList.todos = newTodos; 92 | 93 | return manager.persist(newList).flush(); 94 | ``` 95 | 96 | ### show-all 97 | Run using `node todo show-all`. 98 | 99 | This will fetch all lists and its respective todos. 100 | 101 | ```js 102 | return manager.getRepository(List).find(null, {join: ['todos']}) 103 | .then(all => console.log(util.inspect(all, { depth: 8 }))); 104 | ``` 105 | 106 | ### add-todo 107 | Run using `node todo add-todo `. 108 | 109 | This will add a todo on the referred list. 110 | 111 | ```js 112 | manager.getRepository(List).findOne({name: list}).then(list => { 113 | let newTodo = new Todo; 114 | newTodo.task = todo; 115 | 116 | list.todos.add(newTodo); 117 | 118 | return manager.flush(); 119 | }); 120 | ``` 121 | 122 | ### done 123 | Run using `node todo done `. 124 | 125 | This will update the referred todo setting its property `done` to `true`. 126 | 127 | ```js 128 | return manager.getRepository(Todo) 129 | .find({'t.task': todo}, {alias: 't', populate: 't.list'}) 130 | .then(all => { 131 | let rowToUpdate = all.filter(row => row.list.name === list)[0]; 132 | 133 | rowToUpdate.done = true; 134 | 135 | return manager.flush(); 136 | }) 137 | .then(console.log(`Todo '${todo}' from list '${list}' was set as done.`)); 138 | ``` 139 | 140 | ### remove-todo 141 | Run using `node todo remove-todo `. 142 | 143 | This will remove the todo from the referred list. 144 | 145 | ```js 146 | return manager.getRepository(Todo) 147 | .findOne({'t.task': todo, 'list.name': list}, {alias: 't', join: ['t.list']}) 148 | .then(todo => manager.remove(todo).flush()); 149 | ``` 150 | 151 | ### delete-list 152 | Run using `node todo delete-list `. 153 | 154 | This will delete the list and all its todos. 155 | 156 | ```js 157 | return manager.getRepository(List).findOne({name: list}) 158 | .then(list => manager.remove(list).flush()); 159 | ``` 160 | -------------------------------------------------------------------------------- /examples/todo/entity/List.js: -------------------------------------------------------------------------------- 1 | let Todo = require('./Todo'); 2 | 3 | class List { 4 | static setMapping(mapping) { 5 | mapping.forProperty('id').primary().increments(); 6 | mapping.field('name', {type: 'string', size: 24}); 7 | mapping.forProperty('todos') 8 | .oneToMany({targetEntity: Todo, mappedBy: 'list'}) 9 | .cascade(['persist']); 10 | } 11 | } 12 | 13 | module.exports = List; 14 | -------------------------------------------------------------------------------- /examples/todo/entity/Todo.js: -------------------------------------------------------------------------------- 1 | let List = require('./List'); 2 | 3 | class Todo { 4 | static setMapping(mapping) { 5 | mapping.forProperty('id').primary().increments(); 6 | mapping.field('task', {type: 'string'}); 7 | mapping.field('done', {type: 'boolean', defaultTo: false}); 8 | mapping.forProperty('list') 9 | .manyToOne({targetEntity: 'List', inversedBy: 'todos'}) 10 | .joinColumn({onDelete: 'cascade'}); 11 | } 12 | } 13 | 14 | module.exports = Todo; 15 | -------------------------------------------------------------------------------- /examples/todo/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const Wetland = require('wetland').Wetland; 5 | const List = require('./entity/List'); 6 | const Todo = require('./entity/Todo'); 7 | const wetland = new Wetland(require('./wetland.js')); 8 | 9 | function todo (parameters) { 10 | let action = parameters[2]; 11 | let list = parameters[3]; 12 | let todo = parameters[4]; 13 | 14 | let manager = wetland.getManager(); 15 | 16 | if (action === 'setup') { 17 | return wetland.getMigrator().devMigrations() 18 | .then(() => console.log('Tables created.')); 19 | } 20 | 21 | if (action === 'create-list') { 22 | let newList = new List; 23 | newList.name = list; 24 | 25 | return manager.persist(newList).flush() 26 | .then(() => console.log(`New list '${list}' created.`)); 27 | } 28 | 29 | if (action === 'create-full') { 30 | let todos = parameters.splice(4); 31 | let newList = new List; 32 | let newTodos = []; 33 | 34 | todos.forEach(todo => { 35 | let newTodo = new Todo; 36 | newTodo.task = todo; 37 | 38 | newTodos.push(newTodo); 39 | }); 40 | 41 | newList.name = list; 42 | newList.todos = newTodos; 43 | 44 | return manager.persist(newList).flush().then(() => console.log('List and todos created')); 45 | } 46 | 47 | if (action === 'show-all') { 48 | return manager.getRepository(List).find(null, {join: ['todos']}) 49 | .then(all => console.log(util.inspect(all, {depth: 8}))); 50 | } 51 | 52 | if (action === 'add-todo') { 53 | return manager.getRepository(List).findOne({name: list}) 54 | .then(list => { 55 | let newTodo = new Todo; 56 | newTodo.task = todo; 57 | 58 | list.todos.add(newTodo); 59 | 60 | return manager.flush(); 61 | }) 62 | .then(() => console.log(`Todo '${todo}' added to the list '${list}'.`)); 63 | } 64 | 65 | if (action === 'done') { 66 | return manager.getRepository(Todo).getQueryBuilder('t') 67 | .select('t') 68 | .innerJoin('t.list', 'l') 69 | .where({'t.task': todo, 'l.name': list}) 70 | .getQuery() 71 | .getResult() 72 | .then(row => { 73 | row[0].done = true; 74 | 75 | return manager.flush(); 76 | }) 77 | .then(() => console.log(`Todo '${todo}' from list '${list}' was set as done.`)); 78 | } 79 | 80 | if (action === 'remove-todo') { 81 | return manager.getRepository(Todo).findOne({'t.task': todo, 'list.name': list}, {alias: 't', join: ['t.list']}) 82 | .then(todo => manager.remove(todo).flush()) 83 | .then(() => console.log(`Todo '${todo}' removed.`)); 84 | } 85 | 86 | if (action === 'delete-list') { 87 | return manager.getRepository(List).findOne({name: list}) 88 | .then(list => manager.remove(list).flush()) 89 | .then(() => console.log('List deleted.')); 90 | } 91 | } 92 | 93 | // Connections get closed on exit. 94 | todo(process.argv).then(() => process.exit()); 95 | -------------------------------------------------------------------------------- /examples/todo/wetland.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entityPath: __dirname + '/entity', 3 | stores : { 4 | defaultStore: { 5 | client : 'sqlite3', 6 | useNullAsDefault: true, 7 | connection : {filename: `./todo.sqlite`} 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | gulp.task('build', ['copyFiles']); 4 | gulp.task('copyFiles', () => { 5 | gulp.src(['./test/resource/fixtures/**/**/*']).pipe(gulp.dest('dist/test/resource/fixtures/')); 6 | gulp.src(['./src/**/*', '!./**/*.ts']).pipe(gulp.dest('dist/src')); 7 | }); 8 | 9 | gulp.task('watch', ['build'], () => gulp.watch(['./src/**/*', '!./**/*.ts', './test/resource/fixtures/**/**/*'], ['build'])); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wetland", 3 | "version": "5.1.2", 4 | "description": "A modern object-relational mapper (ORM) for node.js.", 5 | "main": "./dist/src/index.js", 6 | "typings": "./dist/src/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "test": "mocha dist/test/helper dist/test/unit/{*.spec.js,**/*.spec.js} --timeout 15000", 12 | "dtest": "npm run build && npm run test", 13 | "build": "rm -rf ./dist && tsc && gulp build", 14 | "version": "conventional-changelog -p angular -i doc/CHANGELOG.md -s && git add -A doc/CHANGELOG.md", 15 | "prepublish": "npm run build", 16 | "postpublish": "git push upstream master && git push upstream --tags" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/SpoonX/wetland.git" 21 | }, 22 | "author": "RWOverdijk ", 23 | "keywords": [ 24 | "orm", 25 | "knex", 26 | "db", 27 | "mysql", 28 | "sqlite", 29 | "postgres", 30 | "node" 31 | ], 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/SpoonX/wetland/issues" 35 | }, 36 | "homepage": "https://wetland.spoonx.org", 37 | "dependencies": { 38 | "bluebird": "^3.5.3", 39 | "csv-parse": "^1.2.0", 40 | "del": "^3.0.0", 41 | "homefront": "^3.0.0", 42 | "knex": "^0.19.5", 43 | "mkdirp": "^0.5.1", 44 | "stream-replace": "^1.0.0" 45 | }, 46 | "devDependencies": { 47 | "@types/chai": "^3.4.30", 48 | "@types/knex": "^0.15.2", 49 | "@types/mkdirp": "^0.3.29", 50 | "@types/mocha": "^2.2.29", 51 | "@types/node": "^6.0.41", 52 | "chai": "^3.5.0", 53 | "gulp": "^3.9.1", 54 | "gulp-conventional-changelog": "^1.1.3", 55 | "mocha": "^3.0.1", 56 | "mysql": "^2.11.1", 57 | "reflect-metadata": "^0.1.3", 58 | "rimraf": "^2.6.1", 59 | "sqlite3": "^4.0.2", 60 | "ts-node": "^5.0.1", 61 | "tslint": "^5.11.0", 62 | "tslint-config-prettier": "^1.15.0", 63 | "tslint-eslint-rules": "^5.4.0", 64 | "typescript": "^3.0.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ArrayCollection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents Array collections. 3 | */ 4 | export class ArrayCollection extends Array { 5 | 6 | /** 7 | * Add items to the collection when not already in the collection. 8 | * 9 | * @param {...*} items 10 | * 11 | * @returns this Fluent interface 12 | */ 13 | add(...items: Array): ArrayCollection { 14 | items.forEach(item => { 15 | if (!this.includes(item)) { 16 | this.push(item); 17 | } 18 | }); 19 | 20 | return this; 21 | } 22 | 23 | /** 24 | * Loop over each item in the collection, without worrying about index changes. 25 | * 26 | * @param {Function} callback 27 | * 28 | * @returns {ArrayCollection} 29 | */ 30 | each(callback: (target: any) => void): ArrayCollection { 31 | let target; 32 | 33 | while (target = this.pop()) { 34 | callback(target); 35 | } 36 | 37 | return this; 38 | } 39 | 40 | /** 41 | * Remove items from the collection when part of the collection. 42 | * 43 | * @param {...*} items 44 | * 45 | * @returns this Fluent interface 46 | */ 47 | remove(...items: Array): ArrayCollection { 48 | items.forEach(item => { 49 | const itemIndex = this.indexOf(item); 50 | 51 | if (itemIndex > -1) { 52 | this.splice(itemIndex, 1); 53 | } 54 | }); 55 | 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Cleaner.ts: -------------------------------------------------------------------------------- 1 | import { Wetland } from './Wetland'; 2 | import { SnapshotManager } from './SnapshotManager'; 3 | import * as rm from 'del'; 4 | import * as path from 'path'; 5 | import { SchemaBuilder } from './SchemaBuilder'; 6 | 7 | export class Cleaner { 8 | 9 | wetland: Wetland; 10 | 11 | /** 12 | * Construct a cleaner instance. 13 | * 14 | * @param {Wetland} wetland 15 | */ 16 | constructor(wetland: Wetland) { 17 | this.wetland = wetland; 18 | } 19 | 20 | /** 21 | * Clean wetland's related tables and wetland's dev snapshots'. 22 | * 23 | * @return {Promise} 24 | */ 25 | public clean(): Promise { 26 | return this.dropTables() 27 | .then(() => this.cleanDataDirectory()); 28 | } 29 | 30 | /** 31 | * Clean the dev snapshots in the data directory. 32 | * 33 | * @return {Promise} 34 | */ 35 | private cleanDataDirectory(): Promise { 36 | return rm(path.join(this.wetland.getConfig().fetch('dataDirectory'), SnapshotManager.DEV_SNAPSHOTS_PATH, '*')) 37 | .catch(error => { 38 | if (error.code === 'ENOENT') { 39 | return Promise.resolve(); 40 | } 41 | 42 | return Promise.reject(error); 43 | }); 44 | } 45 | 46 | /** 47 | * Drop all tables' entities. 48 | * 49 | * @return {Promise} 50 | */ 51 | private dropTables(): any { 52 | const manager = this.wetland.getManager(); 53 | const snapshotManager = this.wetland.getSnapshotManager(); 54 | const schemaBuilder = new SchemaBuilder(manager); 55 | 56 | return snapshotManager 57 | .fetch() 58 | .then(previous => { 59 | const instructions = snapshotManager.diff(previous, {}); 60 | 61 | return schemaBuilder.process(instructions).apply(); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Criteria/Having.ts: -------------------------------------------------------------------------------- 1 | import { Criteria } from './Criteria'; 2 | 3 | export class Having extends Criteria { 4 | protected conditions: { and: string, or: string } = { and: 'having', or: 'orHaving' }; 5 | } 6 | -------------------------------------------------------------------------------- /src/Criteria/On.ts: -------------------------------------------------------------------------------- 1 | import { Criteria } from './Criteria'; 2 | 3 | export class On extends Criteria { 4 | protected conditions: { and: string, or: string } = { and: 'on', or: 'orOn' }; 5 | } 6 | -------------------------------------------------------------------------------- /src/Criteria/Where.ts: -------------------------------------------------------------------------------- 1 | import { Criteria } from './Criteria'; 2 | 3 | export class Where extends Criteria { 4 | } 5 | -------------------------------------------------------------------------------- /src/Entity.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from './Mapping'; 2 | 3 | export class Entity { 4 | public static toObject(source: T): Partial | Partial[] { 5 | if (Array.isArray(source)) { 6 | return source.map(target => Entity.toObject(target)) as Partial[]; 7 | } 8 | 9 | const mapping = Mapping.forEntity(source); 10 | 11 | if (!mapping) { 12 | return source; 13 | } 14 | 15 | const object = mapping.getFieldNames().reduce((asObject, fieldName) => { 16 | asObject[fieldName] = source[fieldName]; 17 | 18 | return asObject; 19 | }, {}); 20 | 21 | const relations = mapping.getRelations(); 22 | 23 | if (relations) { 24 | Reflect.ownKeys(relations).forEach(fieldName => { 25 | if (typeof source[fieldName] !== 'undefined') { 26 | object[fieldName] = source[fieldName]; 27 | } 28 | }); 29 | } 30 | 31 | return object; 32 | } 33 | 34 | public toObject(): Partial { 35 | return Entity.toObject(this) as Partial; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/EntityInterface.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from './Mapping'; 2 | import { Scope } from './Scope'; 3 | 4 | export interface EntityInterface { 5 | /** 6 | * Optional mapping (not required when using decorators). 7 | * 8 | * @param mapping 9 | */ 10 | setMapping?(mapping: Mapping): void; 11 | 12 | beforeCreate?(entityManager: Scope): Promise | void; 13 | 14 | afterCreate?(entityManager: Scope): Promise | void; 15 | 16 | beforeUpdate?(values: Object, entityManager: Scope): Promise | void; 17 | 18 | afterUpdate?(entityManager: Scope): Promise | void; 19 | 20 | beforeRemove?(entityManager: Scope): Promise | void; 21 | 22 | afterRemove?(entityManager: Scope): Promise | void; 23 | 24 | [key: string]: any; 25 | } 26 | 27 | export interface ProxyInterface extends EntityInterface { 28 | isEntityProxy?: boolean; 29 | 30 | activateProxying?(): ProxyInterface; 31 | 32 | deactivateProxying?(): ProxyInterface; 33 | 34 | getTarget?(): EntityInterface; 35 | 36 | isProxyingActive?(): boolean; 37 | 38 | [key: string]: any; 39 | } 40 | 41 | export type EntityCtor = new (...args: any[]) => T; 42 | -------------------------------------------------------------------------------- /src/EntityManager.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from './Mapping'; 2 | import { Wetland } from './Wetland'; 3 | import { Entity, Scope } from './Scope'; 4 | import { EntityCtor, EntityInterface } from './EntityInterface'; 5 | import { Homefront } from 'homefront'; 6 | import { EntityRepository } from './EntityRepository'; 7 | import { Store } from './Store'; 8 | 9 | /** 10 | * The main entity manager for wetland. 11 | * This distributes scopes and supplies some core methods. 12 | */ 13 | export class EntityManager { 14 | 15 | /** 16 | * The wetland instance this entity manager belongs to. 17 | * 18 | * @type { Wetland } 19 | */ 20 | private readonly wetland: Wetland = null; 21 | 22 | /** 23 | * Holds the entities registered with the entity manager indexed on name. 24 | * 25 | * @type {{}} 26 | */ 27 | private entities: { [key: string]: { entity: EntityCtor, mapping: Mapping } } = {}; 28 | 29 | /** 30 | * Holds instances of repositories that have been instantiated before, as a cache. 31 | * 32 | * @type { Map } 33 | */ 34 | private repositories: Map, EntityRepository> = new Map(); 35 | 36 | /** 37 | * Construct a new core entity manager. 38 | * @constructor 39 | * 40 | * @param {Wetland} wetland 41 | */ 42 | public constructor(wetland: Wetland) { 43 | this.wetland = wetland; 44 | } 45 | 46 | /** 47 | * Get the wetland config. 48 | * 49 | * @returns {Homefront} 50 | */ 51 | public getConfig(): Homefront { 52 | return this.wetland.getConfig(); 53 | } 54 | 55 | /** 56 | * Create a new entity manager scope. 57 | * 58 | * @returns {Scope} 59 | */ 60 | public createScope(): Scope { 61 | return new Scope(this, this.wetland); 62 | } 63 | 64 | /** 65 | * Get the reference to an entity constructor by name. 66 | * 67 | * @param {string} name 68 | * 69 | * @returns {Function} 70 | */ 71 | public getEntity(name: string): EntityCtor { 72 | const entity = this.entities[name]; 73 | 74 | if (!entity) { 75 | throw new Error(`No entity found for "${name}".`); 76 | } 77 | 78 | return entity.entity; 79 | } 80 | 81 | /** 82 | * Get a repository instance for the provided Entity reference. 83 | * 84 | * @param {string|Entity} entity 85 | * @param {Scope} scope 86 | * 87 | * @returns {EntityRepository} 88 | */ 89 | public getRepository(entity: string | EntityCtor, scope?: Scope): EntityRepository { 90 | const entityReference = this.resolveEntityReference(entity) as EntityCtor; 91 | 92 | if (!this.repositories.has(entityReference) || scope) { 93 | const entityMapping = Mapping.forEntity(entityReference); 94 | const Repository = entityMapping.getRepository(); 95 | 96 | if (!Repository) { 97 | throw new Error([ 98 | `Unable to find Repository for entity "${entityMapping.getEntityName() || entityReference.name}".`, 99 | `Did you forget to register your entity or set entityPath(s)?`, 100 | ].join(' ')); 101 | } 102 | 103 | if (scope) { 104 | return new Repository(scope, entityReference); 105 | } 106 | 107 | this.repositories.set(entityReference, new Repository(this, entityReference)); 108 | } 109 | 110 | return this.repositories.get(entityReference) as EntityRepository; 111 | } 112 | 113 | /** 114 | * Get store for provided entity. 115 | * 116 | * @param {EntityInterface} entity 117 | * 118 | * @returns {Store} 119 | */ 120 | public getStore(entity?: EntityInterface | string): Store { 121 | return this.wetland.getStore(this.getStoreName(entity)); 122 | } 123 | 124 | /** 125 | * Get all registered entities. 126 | * 127 | * @returns {{}} 128 | */ 129 | public getEntities(): { [key: string]: { entity: EntityCtor, mapping: Mapping } } { 130 | return this.entities; 131 | } 132 | 133 | /** 134 | * Register an entity with the entity manager. 135 | * 136 | * @param {EntityInterface} entity 137 | * 138 | * @returns {EntityManager} 139 | */ 140 | public registerEntity(entity: EntityCtor & EntityInterface): EntityManager { 141 | const mapping = this.getMapping(entity).setEntityManager(this); 142 | 143 | if (typeof entity.setMapping === 'function') { 144 | entity.setMapping(mapping); 145 | } 146 | 147 | this.entities[mapping.getEntityName()] = { entity, mapping }; 148 | 149 | return this; 150 | } 151 | 152 | /** 153 | * Get the mapping for provided entity. Can be an instance, constructor or the name of the entity. 154 | * 155 | * @param {EntityInterface|string|{}} entity 156 | * 157 | * @returns {Mapping} 158 | */ 159 | public getMapping(entity: T): Mapping { 160 | return Mapping.forEntity(this.resolveEntityReference(entity)) as Mapping; 161 | } 162 | 163 | /** 164 | * Register multiple entities with the entity manager. 165 | * 166 | * @param {EntityInterface[]} entities 167 | * 168 | * @returns {EntityManager} 169 | */ 170 | public registerEntities(entities: Array>): EntityManager { 171 | entities.forEach(entity => { 172 | this.registerEntity(entity); 173 | }); 174 | 175 | return this; 176 | } 177 | 178 | /** 179 | * Resolve provided value to an entity reference. 180 | * 181 | * @param {EntityInterface|string|{}} hint 182 | * 183 | * @returns {EntityInterface|null} 184 | */ 185 | public resolveEntityReference(hint: Entity): EntityCtor { 186 | if (typeof hint === 'string') { 187 | return this.getEntity(hint); 188 | } 189 | 190 | if (typeof hint === 'object') { 191 | return hint as EntityCtor; 192 | } 193 | 194 | return typeof hint === 'function' ? hint as EntityCtor : null; 195 | } 196 | 197 | private getStoreName(entity?: EntityInterface | string): string { 198 | if (typeof entity === 'string') { 199 | return entity; 200 | } 201 | 202 | if (entity) { 203 | return this.getMapping(entity).getStoreName(); 204 | } 205 | 206 | return null; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/IdentityMap.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from './Mapping'; 2 | import { EntityInterface, ProxyInterface } from './EntityInterface'; 3 | 4 | export class IdentityMap { 5 | /** 6 | * Map entities to objects. 7 | * 8 | * @type {WeakMap} 9 | */ 10 | private map: WeakMap = new WeakMap; 11 | 12 | /** 13 | * Reset the map. 14 | */ 15 | public reset() { 16 | this.map = new WeakMap; 17 | } 18 | 19 | /** 20 | * Get the PK map for entity. 21 | * 22 | * @param {Function | EntityInterface} entity 23 | * 24 | * @returns {Object} 25 | */ 26 | public getMapForEntity(entity: Function | EntityInterface): Object { 27 | const entityReference = (typeof entity === 'function' ? entity : entity.constructor) as Function; 28 | const map = this.map.get(entityReference); 29 | 30 | if (!map) { 31 | this.map.set(entityReference, {}); 32 | } 33 | 34 | return this.map.get(entityReference); 35 | } 36 | 37 | /** 38 | * Register an entity with the map. 39 | * 40 | * @param {EntityInterface} entity 41 | * @param {ProxyInterface} proxy 42 | * 43 | * @returns {IdentityMap} 44 | */ 45 | public register(entity: EntityInterface, proxy: ProxyInterface): IdentityMap { 46 | this.getMapForEntity(entity)[entity[Mapping.forEntity(entity).getPrimaryKey()]] = proxy; 47 | 48 | return this; 49 | } 50 | 51 | /** 52 | * Fetch an entity from the map. 53 | * 54 | * @param {EntityInterface|Function} entity 55 | * @param {*} primaryKey 56 | * 57 | * @returns {EntityInterface|null} 58 | */ 59 | public fetch(entity: EntityInterface | Function, primaryKey: any): EntityInterface | ProxyInterface | null { 60 | return this.getMapForEntity(entity)[primaryKey] || null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/MetaData.ts: -------------------------------------------------------------------------------- 1 | import { Homefront } from 'homefront'; 2 | import { ProxyInterface } from './EntityInterface'; 3 | 4 | export class MetaData { 5 | /** 6 | * Static weakmap of objects their metadata. 7 | * 8 | * @type {WeakMap} 9 | */ 10 | private static metaMap: WeakMap = new WeakMap(); 11 | 12 | /** 13 | * Get metadata for provided target (uses constructor). 14 | * 15 | * @param {function|{}} target 16 | * 17 | * @returns {Homefront} 18 | */ 19 | static forTarget(target: Function | Object): Homefront { 20 | return MetaData.ensure(MetaData.getConstructor(target)); 21 | } 22 | 23 | /** 24 | * Ensure metadata for provided target. 25 | * 26 | * @param {*} target 27 | * 28 | * @returns {Homefront} 29 | */ 30 | static ensure(target: any): Homefront { 31 | if (!MetaData.metaMap.has(target)) { 32 | MetaData.metaMap.set(target, new Homefront()); 33 | } 34 | 35 | return MetaData.metaMap.get(target); 36 | } 37 | 38 | /** 39 | * Clear the MetaData for provided targets. 40 | * 41 | * @param {*} targets 42 | */ 43 | static clear(...targets: any[]): void { 44 | targets.forEach(target => MetaData.metaMap.delete(MetaData.getConstructor(target))); 45 | } 46 | 47 | /** 48 | * Get metadata for provided target (accepts instance). 49 | * 50 | * @param {ProxyInterface} instance 51 | * 52 | * @returns {Homefront} 53 | */ 54 | static forInstance(instance: ProxyInterface): Homefront { 55 | if (typeof instance !== 'object') { 56 | throw new Error('Can\'t get metadata, provided instance isn\'t of type Object.'); 57 | } 58 | 59 | return MetaData.ensure(instance.isEntityProxy ? instance.getTarget() : instance); 60 | } 61 | 62 | /** 63 | * Get the constructor for provided target. 64 | * 65 | * @param {function|{}} target 66 | * 67 | * @returns {Function} 68 | */ 69 | public static getConstructor(target: ProxyInterface): Function { 70 | return (typeof target === 'function' ? target : target.constructor) as Function; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Migrator/Migration.ts: -------------------------------------------------------------------------------- 1 | import { Scope } from '../Scope'; 2 | import { Store } from '../Store'; 3 | import { Run } from './Run'; 4 | import * as Knex from 'knex'; 5 | import * as Bluebird from 'bluebird'; 6 | 7 | export class Migration { 8 | 9 | /** 10 | * @type {Scope} 11 | */ 12 | private entityManager: Scope; 13 | 14 | /** 15 | * @type {{}[]} 16 | */ 17 | private builders: Array<{ store: string, schemaBuilder: Knex.SchemaBuilder, knex: Knex }> = []; 18 | 19 | /** 20 | * @type {Function} 21 | */ 22 | private migration: Function; 23 | 24 | /** 25 | * @type {Run} 26 | */ 27 | private migrationRun: Run; 28 | 29 | /** 30 | * Holds whether or not this Migration is a promise. 31 | * 32 | * @type {boolean} 33 | */ 34 | private promise: boolean = false; 35 | 36 | /** 37 | * Construct a new Migration. 38 | * 39 | * @param {Function} migration 40 | * @param {Run} run 41 | */ 42 | public constructor(migration: Function, run: Run) { 43 | this.migration = migration; 44 | this.entityManager = run.getEntityManager(); 45 | this.migrationRun = run; 46 | 47 | this.prepare(); 48 | } 49 | 50 | /** 51 | * Get a schemabuilder to work with. 52 | * 53 | * @param {string} store 54 | * 55 | * @returns {Knex.SchemaBuilder} 56 | */ 57 | public getSchemaBuilder(store?: string): Knex.SchemaBuilder { 58 | return this.getBuilder(store).schema; 59 | } 60 | 61 | /** 62 | * Get a (reusable) transaction for `storeName` 63 | * 64 | * @param {string} storeName 65 | * 66 | * @returns {Bluebird} 67 | */ 68 | public getTransaction(storeName?: string): Bluebird { 69 | return this.migrationRun.getTransaction(storeName); 70 | } 71 | 72 | /** 73 | * Get a builder. This includes the knex instance. 74 | * 75 | * @param {string} store 76 | * 77 | * @returns {{schema: Knex.SchemaBuilder, knex: Knex}} 78 | */ 79 | public getBuilder(store?: string): { schema: Knex.SchemaBuilder, knex: Knex } { 80 | const connection = this.getConnection(store); 81 | const schemaBuilder = connection.schema; 82 | 83 | this.builders.push({ store, schemaBuilder, knex: connection }); 84 | 85 | return { schema: schemaBuilder, knex: connection }; 86 | } 87 | 88 | /** 89 | * Get the SQL for current builders. 90 | * 91 | * @returns {string} 92 | */ 93 | public getSQL(): string { 94 | if (this.promise) { 95 | throw new Error('It\'s not possible to get SQL for a promise based migration.'); 96 | } 97 | 98 | return this.builders.map(builder => builder.schemaBuilder.toString()).join('\n'); 99 | } 100 | 101 | /** 102 | * Run the migration. 103 | * 104 | * @returns {Bluebird} 105 | */ 106 | public run(): Bluebird { 107 | return Bluebird.each(this.builders, builder => { 108 | return this.getTransaction(builder.store).then(transaction => { 109 | return builder.schemaBuilder['transacting'](transaction).then(); 110 | }); 111 | }); 112 | } 113 | 114 | /** 115 | * Prepare the migration by running it. 116 | */ 117 | private prepare(): void { 118 | const prepared = this.migration(this); 119 | 120 | if (prepared && 'then' in prepared) { 121 | this.promise = true; 122 | } 123 | } 124 | 125 | /** 126 | * Get connection for store. 127 | * 128 | * @param {string} store 129 | * 130 | * @returns {knex} 131 | */ 132 | private getConnection(store?: string): Knex { 133 | return this.entityManager.getStore(store).getConnection(Store.ROLE_MASTER); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Migrator/MigrationFile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as mkdirp from 'mkdirp'; 4 | import * as Promise from 'bluebird'; 5 | import { MigratorConfigInterface } from './MigratorConfigInterface'; 6 | 7 | const replace = require('stream-replace'); 8 | 9 | export class MigrationFile { 10 | /** 11 | * @type {MigratorConfigInterface} 12 | */ 13 | private config: MigratorConfigInterface; 14 | 15 | /** 16 | * @param {MigratorConfigInterface} config 17 | */ 18 | public constructor(config: MigratorConfigInterface) { 19 | this.config = config; 20 | 21 | this.ensureMigrationDirectory(); 22 | } 23 | 24 | /** 25 | * Get the config. 26 | * 27 | * @returns {MigratorConfigInterface} 28 | */ 29 | public getConfig(): MigratorConfigInterface { 30 | return this.config; 31 | } 32 | 33 | /** 34 | * Create a new migration file. 35 | * 36 | * @param {string} name 37 | * @param {{}} [code] 38 | * 39 | * @returns {Bluebird} 40 | */ 41 | public create(name: string, code?: { up: string, down: string }): Promise { 42 | const sourceFile = `${__dirname}/templates/migration.${this.config.extension}.dist`; 43 | const migrationName = `${this.makeMigrationName(name)}.${this.config.extension}`; 44 | const targetFile = path.join(this.config.directory, migrationName); 45 | const readStream = fs.createReadStream(sourceFile); 46 | const writeStream = fs.createWriteStream(targetFile); 47 | code = code || { up: null, down: null }; 48 | 49 | if (!code.up) { 50 | code.up = ' // @todo Implement'; 51 | } 52 | 53 | if (!code.down) { 54 | code.down = ' // @todo Implement'; 55 | } 56 | 57 | return new Promise((resolve, reject) => { 58 | readStream 59 | .pipe(replace(/\{\{ up }}/, code.up)) 60 | .pipe(replace(/\{\{ down }}/, code.down)) 61 | .pipe(writeStream); 62 | 63 | readStream.on('error', reject); 64 | writeStream.on('error', reject); 65 | writeStream.on('close', () => resolve(migrationName)); 66 | }); 67 | } 68 | 69 | /** 70 | * Get all migrations from the directory. 71 | * 72 | * @returns {Bluebird} 73 | */ 74 | public getMigrations(): Promise> { 75 | return Promise.promisify(fs.readdir)(this.config.directory).then(contents => { 76 | const regexp = new RegExp(`\.${this.config.extension}$`); 77 | 78 | return contents 79 | .filter(migration => migration.search(regexp) > -1) 80 | .map(migration => migration.replace(regexp, '')) 81 | .sort(); 82 | }); 83 | } 84 | 85 | /** 86 | * Make sure the migration directory exists. 87 | */ 88 | private ensureMigrationDirectory() { 89 | try { 90 | fs.statSync(this.config.directory); 91 | } catch (error) { 92 | mkdirp.sync(this.config.directory); 93 | } 94 | } 95 | 96 | /** 97 | * Make migration name. 98 | * 99 | * @param {string} name 100 | * 101 | * @returns {string} 102 | */ 103 | private makeMigrationName(name): string { 104 | const date = new Date(); 105 | const pad = (source) => { 106 | source = source.toString(); 107 | 108 | return source[1] ? source : `0${source}`; 109 | }; 110 | 111 | return date.getFullYear().toString() + 112 | pad(date.getMonth() + 1) + 113 | pad(date.getDate()) + 114 | pad(date.getHours()) + 115 | pad(date.getMinutes()) + 116 | pad(date.getSeconds()) + `_${name}`; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Migrator/MigrationTable.ts: -------------------------------------------------------------------------------- 1 | import * as Knex from 'knex'; 2 | import { Migrator } from './Migrator'; 3 | 4 | export class MigrationTable { 5 | /** 6 | * @type {Knex} 7 | */ 8 | private connection: Knex; 9 | 10 | /** 11 | * @type {string} 12 | */ 13 | private tableName: string; 14 | 15 | /** 16 | * @type {string} 17 | */ 18 | private lockTableName: string; 19 | 20 | /** 21 | * Construct migrationTable. 22 | * 23 | * @param {Knex} connection 24 | * @param {string} tableName 25 | * @param {string} lockTableName 26 | */ 27 | public constructor(connection: Knex, tableName: string, lockTableName: string) { 28 | this.connection = connection; 29 | this.tableName = tableName; 30 | this.lockTableName = lockTableName; 31 | } 32 | 33 | /** 34 | * Obtain a lock. 35 | * 36 | * @returns {Promise} 37 | */ 38 | public getLock(): Promise { 39 | return this.ensureMigrationTables().then(() => { 40 | return this.connection.transaction(transaction => { 41 | return this.isLocked(transaction) 42 | .then(isLocked => { 43 | if (isLocked) { 44 | throw new Error('Migration table is already locked'); 45 | } 46 | }) 47 | .then(() => this.lockMigrations(transaction)); 48 | }); 49 | }); 50 | } 51 | 52 | /** 53 | * Free a lock. 54 | * 55 | * @returns {QueryBuilder} 56 | */ 57 | public freeLock(): Promise { 58 | return Promise.resolve(this.connection(this.lockTableName).update({ locked: 0 })); 59 | } 60 | 61 | /** 62 | * Get the ID of the last run. 63 | * 64 | * @returns {Promise} 65 | */ 66 | public getLastRunId(): Promise { 67 | const lastRunId = this.connection(this.tableName) 68 | .select('run') 69 | .limit(1) 70 | .orderBy('run', 'desc') 71 | .then(result => result[0] ? result[0].run : null); 72 | 73 | return Promise.resolve(lastRunId); 74 | } 75 | 76 | /** 77 | * Get the name of the last run migration. 78 | * 79 | * @returns {Promise} 80 | */ 81 | public getLastMigrationName(): Promise { 82 | return this.ensureMigrationTables().then(() => { 83 | return this.connection(this.tableName) 84 | .select('name') 85 | .limit(1) 86 | .orderBy('id', 'desc') 87 | .then(result => result[0] ? result[0].name : null); 88 | }); 89 | } 90 | 91 | /** 92 | * Get the names of the migrations that were part of the last run. 93 | * 94 | * @returns {Promise|null>} 95 | */ 96 | public getLastRun(): Promise | null> { 97 | return this.getLastRunId().then(lastRun => { 98 | if (lastRun === null) { 99 | return null; 100 | } 101 | 102 | const connection = this.connection(this.tableName) 103 | .select('name') 104 | .where('run', lastRun) 105 | .orderBy('id', 'desc'); 106 | 107 | return connection.then(results => results.map(result => result.name)); 108 | }); 109 | } 110 | 111 | /** 112 | * Get the names of the migrations that were run. 113 | * 114 | * @returns {Promise|null>} 115 | */ 116 | public getAllRun(): Promise | null> { 117 | return this.ensureMigrationTables().then(() => { 118 | return this.connection(this.tableName) 119 | .orderBy('id', 'desc'); 120 | }); 121 | } 122 | 123 | /** 124 | * Save the last run. 125 | * 126 | * @param {string} direction 127 | * @param {string[]} migrations 128 | * 129 | * @returns {Promise} 130 | */ 131 | public saveRun(direction: string, migrations: Array): Promise { 132 | if (direction === Migrator.DIRECTION_DOWN) { 133 | return Promise.resolve(this.connection(this.tableName).whereIn('name', migrations).del()); 134 | } 135 | 136 | return this.getLastRunId().then(lastRun => { 137 | return this.connection(this.tableName).insert(migrations.map(name => { 138 | return { name, run: (lastRun + 1) }; 139 | })); 140 | }); 141 | } 142 | 143 | /** 144 | * Check if migrations is locked. 145 | * 146 | * @param {Knex.Transaction} transaction 147 | * 148 | * @returns {Promise} 149 | */ 150 | private isLocked(transaction: Knex.Transaction): Promise { 151 | const isLocked = this.connection(this.lockTableName) 152 | .transacting(transaction) 153 | .forUpdate() 154 | .select('*') 155 | .then(data => !!data[0] && !!data[0].locked); 156 | 157 | return Promise.resolve(isLocked); 158 | } 159 | 160 | /** 161 | * Lock migrations. 162 | * 163 | * @param {Knex.Transaction} transaction 164 | * 165 | * @returns {QueryBuilder} 166 | */ 167 | private lockMigrations(transaction): Promise { 168 | return Promise.resolve(this.connection(this.lockTableName).transacting(transaction).update({ locked: 1 })); 169 | } 170 | 171 | /** 172 | * Ensure the migration tables exist. 173 | * 174 | * @returns {Promise} 175 | */ 176 | private async ensureMigrationTables(): Promise { 177 | const connection = this.connection; 178 | const migrationTableExists = await connection.schema.hasTable(this.tableName); 179 | 180 | if (migrationTableExists) { 181 | return; 182 | } 183 | 184 | await connection.schema.createTable(this.tableName, t => { 185 | t.increments(); 186 | t.string('name'); 187 | t.integer('run'); 188 | t.timestamp('migration_time').defaultTo(connection.fn.now()); 189 | t.index([ 'run' ]); 190 | t.index([ 'migration_time' ]); 191 | }); 192 | 193 | const lockTableExists = await connection.schema.hasTable(this.lockTableName); 194 | 195 | if (lockTableExists) { 196 | return; 197 | } 198 | 199 | await connection.schema.createTable(this.lockTableName, t => t.boolean('locked')); 200 | 201 | return connection.schema; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Migrator/MigratorConfigInterface.ts: -------------------------------------------------------------------------------- 1 | export interface MigratorConfigInterface { 2 | store?: string; 3 | tableName?: string; 4 | lockTableName?: string; 5 | directory?: string; 6 | extension?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/Migrator/Run.ts: -------------------------------------------------------------------------------- 1 | import { Migrator } from './Migrator'; 2 | import { Scope } from '../Scope'; 3 | import { Store } from '../Store'; 4 | import { Migration } from './Migration'; 5 | import * as Bluebird from 'bluebird'; 6 | import * as path from 'path'; 7 | import * as Knex from 'knex'; 8 | 9 | /** 10 | * A single migration run. Multiple migrations can be run in one run. 11 | * Each migration in the run gets the same run id. 12 | */ 13 | export class Run { 14 | 15 | /** 16 | * @type {Scope} 17 | */ 18 | private entityManager: Scope; 19 | 20 | /** 21 | * @type {string} 22 | */ 23 | private direction: string; 24 | 25 | /** 26 | * @type {string} 27 | */ 28 | private directory: string; 29 | 30 | /** 31 | * @type {Migration[]} 32 | */ 33 | private migrations: Array; 34 | 35 | /** 36 | * @type {{}}} 37 | */ 38 | private transactions: { [key: string]: Knex.Transaction | Bluebird } = {}; 39 | 40 | /** 41 | * Construct a runner. 42 | * 43 | * @param {string} direction 44 | * @param {Scope} entityManager 45 | * @param {string[]} migrations 46 | * @param {string} directory 47 | */ 48 | public constructor(direction: string, entityManager: Scope, migrations: Array, directory: string) { 49 | this.direction = direction; 50 | this.directory = directory; 51 | this.entityManager = entityManager; 52 | 53 | this.loadMigrations(migrations); 54 | } 55 | 56 | /** 57 | * Run the migrations. 58 | * 59 | * @returns {Promise} 60 | */ 61 | public run(): Bluebird { 62 | return Bluebird.each(this.migrations, migration => migration.run()) 63 | .then(() => { 64 | return Bluebird.map(Reflect.ownKeys(this.transactions) as any, (transaction: string) => { 65 | return (this.transactions[transaction] as Knex.Transaction).commit(); 66 | }); 67 | }) 68 | .catch(error => { 69 | return Bluebird.map(Reflect.ownKeys(this.transactions) as any, (transaction: string) => { 70 | return (this.transactions[transaction] as Knex.Transaction).rollback(); 71 | }).then(() => Bluebird.reject(error)); 72 | }); 73 | } 74 | 75 | /** 76 | * Get the SQL for migrations. 77 | * 78 | * @returns {Bluebird} 79 | */ 80 | public getSQL(): Bluebird { 81 | return Bluebird.mapSeries(this.migrations, migration => migration.getSQL()).then(result => result.join('\n')); 82 | } 83 | 84 | /** 85 | * Get the transaction for this unit of work, and provided target entity. 86 | * 87 | * @param {string} storeName 88 | * 89 | * @returns {Bluebird} 90 | */ 91 | public getTransaction(storeName?: string): Bluebird { 92 | const store = this.entityManager.getStore(storeName); 93 | 94 | if (this.transactions[storeName]) { 95 | if (this.transactions[storeName] instanceof Bluebird) { 96 | return this.transactions[storeName] as Bluebird; 97 | } 98 | 99 | return Bluebird.resolve(this.transactions[storeName]); 100 | } 101 | 102 | return this.transactions[storeName] = new Bluebird(resolve => { 103 | const connection = store.getConnection(Store.ROLE_MASTER); 104 | 105 | connection.transaction(transaction => { 106 | this.transactions[storeName] = transaction; 107 | 108 | resolve(this.transactions[storeName]); 109 | }); 110 | }); 111 | } 112 | 113 | /** 114 | * Get the entity manager scope. 115 | * 116 | * @returns {Scope} 117 | */ 118 | public getEntityManager(): Scope { 119 | return this.entityManager; 120 | } 121 | 122 | /** 123 | * Load migrations provided. 124 | * 125 | * @param {string[]} migrations 126 | * 127 | * @returns {Run} 128 | */ 129 | private loadMigrations(migrations: Array): this { 130 | if (migrations.length === 0) { 131 | return this; 132 | } 133 | 134 | this.migrations = migrations.map(migration => { 135 | if (!migration) { 136 | throw new Error('Invalid migration name supplied. Expected string.'); 137 | } 138 | 139 | let migrationClass = require(path.join(this.directory, migration)); 140 | migrationClass = typeof migrationClass === 'function' ? migrationClass : migrationClass.Migration; 141 | 142 | this.validateMigration(migrationClass); 143 | 144 | return new Migration(migrationClass[this.direction], this); 145 | }); 146 | 147 | return this; 148 | } 149 | 150 | /** 151 | * Validate provided migrations 152 | * 153 | * @param {Function|{}} migration 154 | */ 155 | private validateMigration(migration: Function | Object): void { 156 | if (typeof migration !== 'function' && typeof migration !== 'object') { 157 | throw new Error(`Migration '${migration}' of type '${typeof migration}' is not of type Function or Object.`); 158 | } 159 | 160 | if (!Reflect.has(migration, Migrator.DIRECTION_DOWN) || typeof migration[Migrator.DIRECTION_DOWN] !== 'function') { 161 | throw new Error(`Migration is missing a '${Migrator.DIRECTION_DOWN}' method.`); 162 | } 163 | 164 | if (!Reflect.has(migration, Migrator.DIRECTION_UP) || typeof migration[Migrator.DIRECTION_UP] !== 'function') { 165 | throw new Error(`Migration is missing a '${Migrator.DIRECTION_UP}' method.`); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Migrator/templates/migration.js.dist: -------------------------------------------------------------------------------- 1 | class Migration { 2 | static up(migration) { 3 | {{ up }} 4 | } 5 | 6 | static down(migration) { 7 | {{ down }} 8 | } 9 | } 10 | 11 | module.exports.Migration = Migration; 12 | -------------------------------------------------------------------------------- /src/Migrator/templates/migration.ts.dist: -------------------------------------------------------------------------------- 1 | import {Migration as WlMigration} from 'wetland'; 2 | 3 | export class Migration { 4 | public static up(migration: WlMigration) { 5 | {{ up }} 6 | } 7 | 8 | public static down(migration: WlMigration) { 9 | {{ down }} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Populate.ts: -------------------------------------------------------------------------------- 1 | import { Scope } from './Scope'; 2 | import { EntityCtor } from './EntityInterface'; 3 | import { ArrayCollection as Collection } from './ArrayCollection'; 4 | import { Mapping } from './Mapping'; 5 | import { UnitOfWork } from './UnitOfWork'; 6 | 7 | export class Populate { 8 | 9 | /** 10 | * @type {Scope} 11 | */ 12 | private entityManager: Scope; 13 | 14 | /** 15 | * @type {UnitOfWork} 16 | */ 17 | private unitOfWork: UnitOfWork; 18 | 19 | constructor(entityManager: Scope) { 20 | this.entityManager = entityManager; 21 | this.unitOfWork = entityManager.getUnitOfWork(); 22 | } 23 | 24 | /** 25 | * Find records for update based on provided data. 26 | * 27 | * @param {number|string} primaryKey 28 | * @param {EntityCtor} Entity 29 | * @param {{}} data 30 | * 31 | * @returns {Promise<{new()}>} 32 | */ 33 | public findDataForUpdate(primaryKey: string | number, Entity: EntityCtor<{ new() }>, data: Object): Promise { 34 | const repository = this.entityManager.getRepository(Entity); 35 | const mapping = this.entityManager.getMapping(Entity); 36 | const options = { populate: new Collection(), alias: mapping.getTableName() }; 37 | const relations = mapping.getRelations(); 38 | 39 | Reflect.ownKeys(data).forEach((property: string) => { 40 | if (!relations || !relations[property]) { 41 | return; 42 | } 43 | 44 | const relation = relations[property]; 45 | const reference = this.entityManager.resolveEntityReference(relation.targetEntity); 46 | const type = relation.type; 47 | 48 | if (type === Mapping.RELATION_ONE_TO_MANY || type === Mapping.RELATION_MANY_TO_MANY) { 49 | return options.populate.add({ [property]: property }); 50 | } 51 | 52 | if (typeof data[property] !== 'object' || data[property] === null) { 53 | return; 54 | } 55 | 56 | if (!data[property][Mapping.forEntity(reference).getPrimaryKey()]) { 57 | return; 58 | } 59 | 60 | options.populate.add({ [property]: property }); 61 | }); 62 | 63 | return repository.findOne(primaryKey, options); 64 | } 65 | 66 | /** 67 | * Assign data to base. Create new if not provided. 68 | * 69 | * @param {EntityCtor} Entity 70 | * @param {{}} data 71 | * @param {{}} [base] 72 | * @param {boolean|number} [recursive] 73 | * 74 | * @returns {T} 75 | */ 76 | public assign(Entity: EntityCtor, data: Object, base?: T | Collection, recursive: boolean | number = 1): T { 77 | const mapping = this.entityManager.getMapping(Entity); 78 | const fields = mapping.getFields(); 79 | const primary = mapping.getPrimaryKey(); 80 | 81 | // Ensure base. 82 | if (!(base instanceof Entity)) { 83 | if (typeof data === 'string' || typeof data === 'number') { 84 | // Convenience, allow the primary key value. 85 | return this.entityManager.getReference(Entity, data, false) as T; 86 | } 87 | 88 | if (data && data[primary]) { 89 | // Get the reference (from identity map or mocked) 90 | base = this.entityManager.getReference(Entity, data[primary]) as T; 91 | 92 | base['activateProxying'](); 93 | } else { 94 | // Create a new instance and persist. 95 | base = new Entity(); 96 | 97 | this.entityManager.persist(base); 98 | } 99 | } 100 | 101 | Reflect.ownKeys(data).forEach((property: string) => { 102 | const field = fields[property]; 103 | 104 | // Only allow mapped fields to be assigned. 105 | if (!field) { 106 | return; 107 | } 108 | 109 | // Only relationships require special treatment. This isn't one, so just assign and move on. 110 | if (!field.relationship) { 111 | if ([ 'date', 'dateTime', 'datetime', 'time' ].indexOf(field.type) > -1 && !(data[property] instanceof Date)) { 112 | data[property] = new Date(data[property]); 113 | } 114 | 115 | base[property] = data[property]; 116 | 117 | return; 118 | } 119 | 120 | if (!data[property]) { 121 | if (base[property]) { 122 | delete base[property]; 123 | } 124 | 125 | return; 126 | } 127 | 128 | if (!recursive) { 129 | return; 130 | } 131 | 132 | if (Array.isArray(data[property]) && !data[property].length && (!base[property] || !base[property].length)) { 133 | return; 134 | } 135 | 136 | let level = recursive; 137 | 138 | if (typeof level === 'number') { 139 | level--; 140 | } 141 | 142 | const targetConstructor = this.entityManager.resolveEntityReference(field.relationship.targetEntity); 143 | 144 | if (Array.isArray(data[property])) { 145 | base[property] = this.assignCollection(targetConstructor, data[property], base[property], level); 146 | } else if (data[property]) { 147 | base[property] = this.assign(targetConstructor, data[property], base[property], level); 148 | } 149 | }); 150 | 151 | return base; 152 | } 153 | 154 | /** 155 | * Assign data based on a collection. 156 | * 157 | * @param {EntityCtor} Entity 158 | * @param {{}} data 159 | * @param {{}} [base] 160 | * @param {boolean|number} [recursive] 161 | * 162 | * @returns {Collection} 163 | */ 164 | private assignCollection(Entity: EntityCtor, data: Array, base?: Collection, recursive: boolean | number = 1): Array { 165 | base = base || new Collection; 166 | 167 | base.splice(0); 168 | 169 | data.forEach(rowData => { 170 | base.push(this.assign(Entity, rowData, null, recursive)); 171 | }); 172 | 173 | return base; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Query.ts: -------------------------------------------------------------------------------- 1 | import * as knex from 'knex'; 2 | import { Hydrator } from './Hydrator'; 3 | import { QueryBuilder } from './QueryBuilder'; 4 | 5 | export class Query { 6 | 7 | /** 8 | * @type {Hydrator} 9 | */ 10 | private hydrator: Hydrator; 11 | 12 | /** 13 | * @type {{}} 14 | */ 15 | private statement: knex.QueryBuilder; 16 | 17 | /** 18 | * The parent of this Query. 19 | * 20 | * @type {{}} 21 | */ 22 | private parent: { column: string, primaries: Array }; 23 | 24 | /** 25 | * The child queries. 26 | * 27 | * @type {Array} 28 | */ 29 | private children: Array> = []; 30 | 31 | /** 32 | * Construct a new Query. 33 | * 34 | * @param {knex.QueryBuilder} statement 35 | * @param {Hydrator} hydrator 36 | * @param {[]} children 37 | */ 38 | public constructor(statement: knex.QueryBuilder, hydrator: Hydrator, children: Array> = []) { 39 | this.statement = statement; 40 | this.hydrator = hydrator; 41 | this.children = children; 42 | } 43 | 44 | /** 45 | * Set the parent for this query. 46 | * 47 | * @param parent 48 | * @returns {Query} 49 | */ 50 | public setParent(parent: { column: string, primaries: Array }): this { 51 | this.parent = parent; 52 | 53 | return this; 54 | } 55 | 56 | /** 57 | * Execute the query. 58 | * 59 | * @returns {Promise<[]>} 60 | */ 61 | public execute(): Promise> { 62 | const query = this.restrictToParent(); 63 | 64 | if (process.env.LOG_QUERIES) { 65 | console.log('Executing query:', query.toString()); 66 | } 67 | 68 | return Promise.resolve(query.then()); 69 | } 70 | 71 | /** 72 | * Get a single scalar result (for instance for count, sum or max). 73 | * 74 | * @returns {Promise} 75 | */ 76 | public getSingleScalarResult(): Promise { 77 | return this.execute().then(result => { 78 | if (!result || typeof result[0] !== 'object') { 79 | return null; 80 | } 81 | 82 | return result[0][Object.keys(result[0])[0]]; 83 | }); 84 | } 85 | 86 | /** 87 | * Get the result for the query. 88 | * 89 | * @returns {Promise<{}[]>} 90 | */ 91 | public getResult(): Promise { 92 | return this.execute().then(result => { 93 | if (!result || !result.length) { 94 | return null; 95 | } 96 | 97 | const hydrated = this.hydrator.hydrateAll(result); 98 | 99 | return Promise.all(this.children.map(child => { 100 | return child.getQuery().getResult(); 101 | })).then(() => hydrated); 102 | }); 103 | } 104 | 105 | /** 106 | * Get the SQL query for current query. 107 | * 108 | * @returns {string} 109 | */ 110 | public getSQL(): string { 111 | return this.statement.toString(); 112 | } 113 | 114 | /** 115 | * Get the statement for this query. 116 | * 117 | * @returns {knex.QueryBuilder} 118 | */ 119 | public getStatement(): knex.QueryBuilder { 120 | return this.statement; 121 | } 122 | 123 | /** 124 | * Restrict this query to parents. 125 | * 126 | * @returns {any} 127 | */ 128 | private restrictToParent(): knex.QueryBuilder { 129 | const statement = this.statement; 130 | 131 | if (!this.parent || !this.parent.primaries.length) { 132 | return statement; 133 | } 134 | 135 | const parent = this.parent; 136 | 137 | if (parent.primaries.length === 1) { 138 | statement.where(parent.column, parent.primaries[0]); 139 | 140 | return statement; 141 | } 142 | 143 | const client = statement['client']; 144 | const unionized = client.queryBuilder(); 145 | 146 | parent.primaries.forEach(primary => { 147 | const toUnion = statement.clone().where(parent.column, primary); 148 | 149 | if (client.config.client === 'sqlite3' || client.config.client === 'sqlite') { 150 | unionized.union(client.queryBuilder().select('*').from(client.raw(toUnion).wrap('(', ')'))); 151 | 152 | return unionized; 153 | } 154 | 155 | unionized.union(toUnion, true); 156 | }); 157 | 158 | return unionized; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Raw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A raw query 3 | */ 4 | export class Raw { 5 | /** 6 | * @type {string} 7 | */ 8 | private query: string; 9 | 10 | /** 11 | * @param {string} query 12 | */ 13 | public constructor(query) { 14 | this.setQuery(query); 15 | } 16 | 17 | /** 18 | * @returns {string} 19 | */ 20 | getQuery(): string { 21 | return this.query; 22 | } 23 | 24 | /** 25 | * @param {string} value 26 | */ 27 | setQuery(value: string) { 28 | this.query = value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SchemaManager.ts: -------------------------------------------------------------------------------- 1 | import { Wetland } from './Wetland'; 2 | import { SchemaBuilder } from './SchemaBuilder'; 3 | 4 | export class SchemaManager { 5 | /** 6 | * @type {Wetland} 7 | */ 8 | private wetland: Wetland; 9 | 10 | /** 11 | * @type {SchemaBuilder} 12 | */ 13 | private schemaBuilder: SchemaBuilder; 14 | 15 | /** 16 | * @param {Wetland} wetland 17 | */ 18 | constructor(wetland: Wetland) { 19 | this.wetland = wetland; 20 | this.schemaBuilder = new SchemaBuilder(wetland.getManager()); 21 | } 22 | 23 | /** 24 | * Get the sql for schema. 25 | * 26 | * @param {{}} [previous] Optional starting point to diff against. 27 | * @param {boolean} [revert] 28 | * 29 | * @returns {string} 30 | */ 31 | public getSQL(previous: Object = {}, revert: boolean = false): string { 32 | return this.prepare(previous, revert).getSQL(); 33 | } 34 | 35 | /** 36 | * Get the code for schema. 37 | * 38 | * @param {{}} [previous] Optional starting point to diff against. 39 | * @param {boolean} [revert] 40 | * 41 | * @returns {string} 42 | */ 43 | public getCode(previous: Object = {}, revert: boolean = false): string { 44 | return this.prepare(previous, revert).getCode(); 45 | } 46 | 47 | /** 48 | * Create the schema (alias for `.apply({})`) 49 | * 50 | * @returns {Promise} 51 | */ 52 | public create(): Promise { 53 | return this.apply({}); 54 | } 55 | 56 | /** 57 | * Diff and execute. 58 | * 59 | * @param {{}} [previous] Optional starting point to diff against. 60 | * @param {boolean} [revert] 61 | * 62 | * @returns {Promise} 63 | */ 64 | public apply(previous: Object = {}, revert: boolean = false): Promise { 65 | return this.prepare(previous, revert).apply(); 66 | } 67 | 68 | /** 69 | * Prepare (diff) instructions. 70 | * 71 | * @param {{}} previous 72 | * @param {boolean} [revert] 73 | * 74 | * @returns {SchemaBuilder} 75 | */ 76 | private prepare(previous: Object, revert: boolean = false) { 77 | const snapshot = this.wetland.getSnapshotManager(); 78 | const serializable = snapshot.getSerializable(); 79 | let instructions; 80 | 81 | if (revert) { 82 | instructions = snapshot.diff(serializable, previous); 83 | } else { 84 | instructions = snapshot.diff(previous, serializable); 85 | } 86 | 87 | return this.schemaBuilder.process(instructions); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SnapshotManager } from './SnapshotManager'; 2 | export { SchemaBuilder } from './SchemaBuilder'; 3 | export { Migrator } from './Migrator/Migrator'; 4 | export { Run } from './Migrator/Run'; 5 | export { Migration } from './Migrator/Migration'; 6 | export { Hydrator } from './Hydrator'; 7 | export { IdentityMap } from './IdentityMap'; 8 | export { EntityInterface, ProxyInterface, EntityCtor } from './EntityInterface'; 9 | export { EntityProxy } from './EntityProxy'; 10 | export { MetaData } from './MetaData'; 11 | export { Query } from './Query'; 12 | export { Store } from './Store'; 13 | export { UnitOfWork } from './UnitOfWork'; 14 | export { ArrayCollection } from './ArrayCollection'; 15 | export { EntityRepository, FindOptions } from './EntityRepository'; 16 | export { Wetland } from './Wetland'; 17 | export { Mapping, Field, FieldOptions, JoinColumn, JoinTable, Relationship } from './Mapping'; 18 | export { QueryBuilder } from './QueryBuilder'; 19 | export { EntityManager } from './EntityManager'; 20 | export { Scope } from './Scope'; 21 | export { Populate } from './Populate'; 22 | export { Criteria } from './Criteria/Criteria'; 23 | export { Where } from './Criteria/Where'; 24 | export { Having } from './Criteria/Having'; 25 | export { Entity } from './Entity'; 26 | export * from './decorators/Mapping'; 27 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | process.setMaxListeners(0); 5 | 6 | let tmpTestDir = path.join(__dirname, '.tmp'); 7 | let clearTmpDir = () => { 8 | try { 9 | fs.readdirSync(tmpTestDir).forEach(file => { 10 | fs.unlinkSync(path.join(tmpTestDir, file)); 11 | }); 12 | 13 | fs.rmdirSync(tmpTestDir); 14 | } catch (e) { 15 | } 16 | }; 17 | 18 | before(done => { 19 | clearTmpDir(); 20 | fs.mkdir(tmpTestDir, done); 21 | }); 22 | 23 | after(() => { 24 | clearTmpDir(); 25 | }); 26 | -------------------------------------------------------------------------------- /test/resource/Schema.ts: -------------------------------------------------------------------------------- 1 | import * as knex from 'knex'; 2 | 3 | export class Schema { 4 | static getColumns(connection, table?) { 5 | return Schema.getData(connection, false, [ 6 | 'column_default', 7 | 'table_name', 8 | 'column_name', 9 | 'data_type', 10 | 'extra', 11 | 'column_key', 12 | 'column_type', 13 | 'is_nullable', 14 | ], 'columns', 'column_name', table); 15 | } 16 | 17 | static getReferentialConstraints(connection, table?) { 18 | return Schema.getData(connection, true, [ 19 | 'constraint_name', 20 | 'unique_constraint_schema', 21 | 'unique_constraint_name', 22 | 'update_rule', 23 | 'delete_rule', 24 | 'table_name', 25 | 'referenced_table_name', 26 | ], 'referential_constraints', 'constraint_name', table); 27 | } 28 | 29 | static getConstraints(connection, table?) { 30 | return Schema.getData(connection, true, [ 31 | 'table_name', 32 | 'column_name', 33 | 'constraint_name', 34 | 'referenced_table_name', 35 | 'referenced_column_name', 36 | ], 'key_column_usage', 'column_name', table); 37 | } 38 | 39 | static getAllInfo(connection, table?) { 40 | return Promise.all([ 41 | Schema.getColumns(connection, table), 42 | Schema.getConstraints(connection, table), 43 | Schema.getReferentialConstraints(connection, table), 44 | ]).then(results => { 45 | return { 46 | columns : results[0], 47 | constraints : results[1], 48 | referentialConstraints: results[2], 49 | }; 50 | }); 51 | } 52 | 53 | static getData(connection, constraint, select, from, orderBy, table?) { 54 | let query = connection 55 | .select(select) 56 | .from('information_schema.' + from) 57 | .where({ [constraint ? 'constraint_schema' : 'table_schema']: 'wetland_test' }) 58 | .orderBy('table_name', 'asc'); 59 | 60 | if (orderBy) { 61 | query.orderBy(orderBy); 62 | } 63 | 64 | if (table) { 65 | query.andWhere({ referenced_table_name: table }); 66 | } 67 | 68 | return query.then(); 69 | } 70 | 71 | static resetDatabase(done) { 72 | let connection = knex({ 73 | client : 'mysql', 74 | connection: { 75 | user : 'root', 76 | host : '127.0.0.1', 77 | database: 'wetland_test', 78 | }, 79 | }); 80 | 81 | connection.raw('drop database wetland_test').then(function () { 82 | return connection.raw('create database wetland_test').then(() => { 83 | connection.destroy().then(() => { 84 | done(); 85 | }); 86 | }); 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/resource/Seeder.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Bluebird from 'bluebird'; 3 | import * as rimraf from 'rimraf'; 4 | 5 | export const tmpTestDir = path.join(__dirname, '../.tmp'); 6 | export const dataDir = `${tmpTestDir}/.data`; 7 | export const fixturesDir = path.join(__dirname, '../resource/fixtures/'); 8 | 9 | export class User { 10 | static setMapping(mapping) { 11 | mapping.forProperty('id').increments().primary(); 12 | mapping.forProperty('username').field({ type: 'string' }); 13 | mapping.forProperty('password').field({ type: 'string' }); 14 | mapping.forProperty('posts').oneToMany({ targetEntity: Post, mappedBy: 'author' }); 15 | } 16 | } 17 | 18 | export class Post { 19 | static setMapping(mapping) { 20 | mapping.forProperty('id').increments().primary(); 21 | mapping.forProperty('title').field({ type: 'string' }); 22 | mapping.forProperty('content').field({ type: 'text' }); 23 | mapping.forProperty('author').manyToOne({ targetEntity: User, inversedBy: 'posts' }); 24 | } 25 | } 26 | 27 | export class Pet { 28 | static setMapping(mapping) { 29 | mapping.forProperty('id').increments().primary(); 30 | mapping.forProperty('name').field({ type: 'string' }); 31 | } 32 | } 33 | 34 | export function getType (bypassLifecyclehooks: boolean): string { 35 | return bypassLifecyclehooks ? 'nolifecycle' : 'lifecycle'; 36 | } 37 | 38 | export function rmDataDir (): Promise { 39 | const rmDir: any = Bluebird.promisify(rimraf); 40 | 41 | return rmDir(dataDir); 42 | } 43 | -------------------------------------------------------------------------------- /test/resource/entity/Foo.ts: -------------------------------------------------------------------------------- 1 | export class FooEntity { 2 | static setMapping(mapping) { 3 | mapping.field('camelCase', { type: 'integer' }); 4 | mapping.field('PascalCase', { type: 'integer' }); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/resource/entity/NoAutoIncrement.ts: -------------------------------------------------------------------------------- 1 | export class NoAutoIncrement { 2 | public name: string; 3 | 4 | public dateOfBirth: Date; 5 | 6 | public static setMapping(mapping) { 7 | mapping.field('id', { type: 'integer' }).primary('id'); 8 | mapping.field('foo', { type: 'string' }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/resource/entity/Parent.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../../src/ArrayCollection'; 2 | import { Simple } from './Simple'; 3 | 4 | export class Parent { 5 | public name: string; 6 | 7 | public simples: ArrayCollection = new ArrayCollection(); 8 | 9 | public others: ArrayCollection = new ArrayCollection(); 10 | 11 | public single: Simple; 12 | 13 | public static setMapping(mapping) { 14 | mapping.oneToMany('simples', { targetEntity: Simple, mappedBy: 'parent' }); 15 | mapping.oneToOne('single', { targetEntity: Simple }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/resource/entity/Simple.ts: -------------------------------------------------------------------------------- 1 | export class Simple { 2 | public name: string; 3 | 4 | public dateOfBirth: Date; 5 | 6 | public static setMapping(mapping) { 7 | mapping.field('dateOfBirth', { type: 'datetime' }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/resource/entity/SimpleDifferent.ts: -------------------------------------------------------------------------------- 1 | export class SimpleDifferent { 2 | public name: string; 3 | public static setMapping(mapping) { 4 | 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/resource/entity/ToUnderscore.ts: -------------------------------------------------------------------------------- 1 | export class ToUnderscore { 2 | static setMapping(mapping) { 3 | mapping.forProperty('id').primary().increments().field({ name: 'underscore_id' }); 4 | mapping.field('camelCaseToUnderscore', { type: 'string', size: 20 }); 5 | mapping.field('PascalToUnderscore', { type: 'integer' }); 6 | mapping.field('already_underscore', { type: 'boolean' }); 7 | mapping.field('customName', { type: 'string', name: 'customColumnName' }); 8 | mapping.field('camelCaseAnd_underscore', { type: 'boolean' }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/resource/entity/WithCustomRepository.ts: -------------------------------------------------------------------------------- 1 | import { CustomRepository } from '../repository/CustomRepository'; 2 | 3 | export class WithCustomRepository { 4 | public static setMapping(mapping) { 5 | mapping.entity({ repository: CustomRepository }); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/resource/entity/book/book.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 2 | import { Mapping } from '../../../../src/Mapping'; 3 | import { Publisher } from './publisher'; 4 | 5 | export class Book { 6 | 7 | public id: Number; 8 | 9 | public publisher: ArrayCollection; 10 | 11 | public name: string; 12 | 13 | static setMapping(mapping: Mapping) { 14 | mapping.field('id', { type: 'integer' }).primary('id').generatedValue('id', 'autoIncrement'); 15 | mapping.field('name', { type: 'string', size: 24 }); 16 | 17 | mapping 18 | .manyToOne('publisher', { targetEntity: 'Publisher', inversedBy: 'books' }) 19 | .joinColumn('publisher', { nullable: false }); 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/resource/entity/book/publisher.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 2 | import { Mapping } from '../../../../src/Mapping'; 3 | import { Book } from './book'; 4 | 5 | export class Publisher { 6 | public id: number; 7 | 8 | public name: string; 9 | 10 | public books: ArrayCollection; 11 | 12 | static setMapping(mapping: Mapping) { 13 | mapping.forProperty('id') 14 | .field({ type: 'integer' }) 15 | .generatedValue('autoIncrement') 16 | .primary(); 17 | 18 | mapping.field('name', { type: 'string', size: 24 }); 19 | 20 | mapping.oneToMany('books', { targetEntity: 'Book', mappedBy: 'publisher' }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/resource/entity/eval/Address.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { Delivery } from './Delivery'; 3 | 4 | export class Address { 5 | public static setMapping(mapping: Mapping
) { 6 | mapping.forProperty('id').primary().increments(); 7 | 8 | mapping.field('street', { type: 'string' }); 9 | mapping.field('houseNumber', { type: 'integer', name: 'house_number' }); 10 | mapping.field('postcode', { type: 'string' }); 11 | mapping.field('country', { type: 'string' }); 12 | 13 | mapping.oneToMany('deliveries', { targetEntity: Delivery, mappedBy: 'address' }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/resource/entity/eval/Delivery.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { Address } from './Address'; 3 | import { Order } from './Order'; 4 | 5 | export class Delivery { 6 | public static setMapping(mapping: Mapping) { 7 | mapping.forProperty('id').primary().increments(); 8 | 9 | mapping.forProperty('created').field({ type: 'timestamp', defaultTo: mapping.now() }); 10 | 11 | mapping.manyToOne('address', { targetEntity: Address, inversedBy: 'deliveries' }); 12 | 13 | mapping.forProperty('order') 14 | .joinColumn({ onDelete: 'cascade' }) 15 | .oneToOne({ targetEntity: Order }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/resource/entity/eval/Order.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | 3 | export class Order { 4 | public static setMapping(mapping: Mapping) { 5 | mapping.forProperty('id').primary().increments(); 6 | mapping.field('name', { type: 'string' }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/resource/entity/eval/Tracker.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { User } from './User'; 3 | 4 | export class Tracker { 5 | public static setMapping(mapping: Mapping) { 6 | mapping.forProperty('id').primary().increments(); 7 | mapping.field('status', { type: 'integer' }); 8 | mapping.forProperty('observers').manyToMany({ targetEntity: User, inversedBy: 'trackers' }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/resource/entity/eval/User.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { Tracker } from './Tracker'; 3 | 4 | export class User { 5 | public static setMapping(mapping: Mapping) { 6 | mapping.forProperty('id').primary().increments(); 7 | mapping.field('name', { type: 'string' }); 8 | mapping.forProperty('trackers').manyToMany({ targetEntity: Tracker, mappedBy: 'observers' }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/resource/entity/postal/Address.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { Delivery } from './Delivery'; 3 | 4 | export class Address { 5 | public static setMapping(mapping: Mapping
) { 6 | mapping.forProperty('id').primary().increments(); 7 | 8 | mapping.field('street', { type: 'string' }); 9 | mapping.field('houseNumber', { type: 'integer', name: 'house_number' }); 10 | mapping.field('postcode', { type: 'string' }); 11 | mapping.field('country', { type: 'string' }); 12 | 13 | mapping.oneToMany('deliveries', { targetEntity: Delivery, mappedBy: 'address' }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/resource/entity/postal/Delivery.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { Address } from './Address'; 3 | import { Order } from './Order'; 4 | 5 | export class Delivery { 6 | public static setMapping(mapping: Mapping) { 7 | mapping.autoPK(); 8 | mapping.forProperty('created').field({ type: 'timestamp', defaultTo: mapping.now() }); 9 | 10 | mapping.manyToOne('address', { targetEntity: Address, inversedBy: 'deliveries' }); 11 | 12 | mapping.forProperty('order') 13 | .joinColumn({ onDelete: 'cascade' }) 14 | .oneToOne({ targetEntity: Order }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/resource/entity/postal/Order.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | 3 | export class Order { 4 | public static setMapping(mapping: Mapping) { 5 | mapping.forProperty('id').primary().increments(); 6 | mapping.field('name', { type: 'string' }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/resource/entity/postal/Tracker.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 3 | import { User } from './User'; 4 | 5 | export class Tracker { 6 | public id: number; 7 | 8 | public status: number; 9 | 10 | public observers: ArrayCollection = new ArrayCollection(); 11 | 12 | public static setMapping(mapping: Mapping) { 13 | mapping.forProperty('id').primary().increments(); 14 | mapping.field('status', { type: 'integer' }); 15 | mapping.forProperty('observers').cascade([ 'persist' ]).manyToMany({ targetEntity: User, inversedBy: 'trackers' }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/resource/entity/postal/User.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 3 | import { Tracker } from './Tracker'; 4 | 5 | export class User { 6 | public id: number; 7 | 8 | public name: string; 9 | 10 | public trackers: ArrayCollection = new ArrayCollection(); 11 | 12 | public static setMapping(mapping: Mapping) { 13 | mapping.forProperty('id').primary().increments(); 14 | mapping.field('name', { type: 'string' }); 15 | mapping.forProperty('trackers').cascade([ 'persist' ]).manyToMany({ targetEntity: Tracker, mappedBy: 'observers' }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/resource/entity/shop/Profile.ts: -------------------------------------------------------------------------------- 1 | export class Profile { 2 | public slogan: string; 3 | 4 | static setMapping(mapping) { 5 | mapping.field('id', { type: 'integer' }).primary('id').generatedValue('id', 'autoIncrement'); 6 | mapping.field('slogan', { type: 'string', size: 24 }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/resource/entity/shop/category.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 2 | import { Tag } from './tag'; 3 | import { Mapping } from '../../../../src/Mapping'; 4 | 5 | export class Category { 6 | public id: number; 7 | 8 | public name: string; 9 | 10 | public tags: ArrayCollection; 11 | 12 | static setMapping(mapping: Mapping) { 13 | mapping.forProperty('id') 14 | .field({ type: 'integer' }) 15 | .generatedValue('autoIncrement') 16 | .primary(); 17 | 18 | mapping.field('name', { type: 'string', size: 24 }); 19 | 20 | mapping.manyToMany('products', { targetEntity: 'Product', mappedBy: 'categories' }); 21 | 22 | mapping.forProperty('tags') 23 | .cascade([ 'persist' ]) 24 | .manyToMany({ targetEntity: 'Tag', inversedBy: 'categories' }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/resource/entity/shop/image.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 2 | import { Tag } from './tag'; 3 | 4 | export class Image { 5 | public name: string; 6 | 7 | public tags: ArrayCollection; 8 | 9 | static setMapping(mapping) { 10 | mapping.field('id', { type: 'integer' }).primary('id').generatedValue('id', 'autoIncrement'); 11 | mapping.field('name', { type: 'string', size: 24 }); 12 | mapping.field('type', { type: 'string', size: 24, nullable: true }); 13 | mapping.field('location', { type: 'string', size: 24, nullable: true }); 14 | 15 | mapping 16 | .manyToOne('author', { targetEntity: 'User', inversedBy: 'products' }) 17 | .joinColumn('author', { name: 'author_id', referencedColumnName: 'id' }); 18 | 19 | mapping 20 | .cascade('tags', [ 'persist' ]) 21 | .manyToMany('tags', { targetEntity: 'Tag', inversedBy: 'images' }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/resource/entity/shop/product.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../../../src/ArrayCollection'; 2 | import { Category } from './category'; 3 | import { User } from './user'; 4 | import { Image } from './image'; 5 | 6 | export class Product { 7 | public categories: ArrayCollection; 8 | 9 | public name: string; 10 | 11 | public image: Image; 12 | 13 | public author: User; 14 | 15 | static setMapping(mapping) { 16 | mapping.entity({ charset: 'utf8mb4', collate: 'utf8mb4_bin' }); 17 | 18 | mapping.field('id', { type: 'integer' }).primary('id').generatedValue('id', 'autoIncrement'); 19 | mapping.field('name', { type: 'string', size: 24 }); 20 | 21 | mapping 22 | .cascade('image', [ 'persist' ]) 23 | .oneToOne('image', { targetEntity: 'Image' }) 24 | .joinColumn('image', { name: 'image_id', referencedColumnName: 'id' }); 25 | 26 | mapping 27 | .manyToOne('author', { targetEntity: User, inversedBy: 'products' }) 28 | .joinColumn('author', { name: 'author_id', referencedColumnName: 'id' }); 29 | 30 | mapping 31 | .cascade('categories', [ 'persist' ]) 32 | .manyToMany('categories', { targetEntity: Category, inversedBy: 'products' }) 33 | .joinTable('categories', { 34 | name : 'product_custom_join_category', 35 | joinColumns : [ { referencedColumnName: 'id', name: 'product_id' } ], 36 | inverseJoinColumns: [ { referencedColumnName: 'id', name: 'category_id' } ], 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/resource/entity/shop/tag.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | 3 | export class Tag { 4 | public id: number; 5 | 6 | public name: string; 7 | 8 | public creator: User; 9 | 10 | static setMapping(mapping) { 11 | mapping.field('id', { type: 'integer' }).primary('id').generatedValue('id', 'autoIncrement'); 12 | mapping.field('name', { type: 'string', size: 24 }); 13 | 14 | mapping.manyToMany('images', { targetEntity: 'Image', mappedBy: 'tags' }); 15 | 16 | mapping.manyToMany('categories', { targetEntity: 'Category', mappedBy: 'tags' }); 17 | 18 | mapping 19 | .cascade('creator', [ 'persist' ]) 20 | .manyToOne('creator', { targetEntity: 'User', inversedBy: 'tags' }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/resource/entity/shop/user.ts: -------------------------------------------------------------------------------- 1 | import { Profile } from './Profile'; 2 | import { Mapping } from '../../../../src/Mapping'; 3 | 4 | export class User { 5 | public name: string; 6 | 7 | public profile: Profile; 8 | 9 | static setMapping(mapping: Mapping) { 10 | mapping.uniqueConstraint('lonely_name', 'name'); 11 | 12 | mapping.forProperty('id').primary().generatedValue('autoIncrement'); 13 | 14 | mapping.field('name', { type: 'string', size: 24, name: 'custom' }); 15 | 16 | mapping 17 | .oneToMany('products', { targetEntity: 'Product', mappedBy: 'author' }) 18 | .cascade('products', [ 'persist' ]); 19 | 20 | mapping.oneToMany('tags', { targetEntity: 'Tag', mappedBy: 'creator' }); 21 | 22 | mapping.cascade('profile', [ 'persist', 'delete' ]).oneToOne('profile', { targetEntity: 'Profile' }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/resource/entity/snapshot/new.ts: -------------------------------------------------------------------------------- 1 | import { Media } from './new/media'; 2 | import { Offer } from './new/offer'; 3 | 4 | export default [ 5 | Media, 6 | Offer, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/resource/entity/snapshot/new/media.ts: -------------------------------------------------------------------------------- 1 | export class Media { 2 | static setMapping(mapping) { 3 | // Pk 4 | mapping.forProperty('id').increments().primary(); 5 | 6 | // // Fields 7 | mapping.forProperty('url').field({ type: 'string' }); 8 | 9 | // Relations 10 | mapping.forProperty('offer') 11 | .manyToOne({ targetEntity: 'Offer', inversedBy: 'pictures' }) 12 | .joinColumn({ onDelete: 'cascade' }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/resource/entity/snapshot/new/offer.ts: -------------------------------------------------------------------------------- 1 | export class Offer { 2 | static setMapping(mapping) { 3 | // Pk 4 | mapping.forProperty('id').increments().primary(); 5 | 6 | // Fields 7 | mapping.forProperty('name').field({ type: 'string' }); 8 | 9 | // Relations 10 | mapping.forProperty('pictures') 11 | .oneToMany({ targetEntity: 'Picture', mappedBy: 'offer' }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/resource/entity/snapshot/old.ts: -------------------------------------------------------------------------------- 1 | import { Media } from './old/media'; 2 | import { Offer } from './old/offer'; 3 | 4 | export default [ 5 | Media, 6 | Offer, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/resource/entity/snapshot/old/media.ts: -------------------------------------------------------------------------------- 1 | export class Media { 2 | static setMapping(mapping) { 3 | // Pk 4 | mapping.forProperty('id').increments().primary(); 5 | 6 | // Fields 7 | mapping.forProperty('url').field({ type: 'string' }); 8 | 9 | // Relations 10 | mapping.forProperty('offer') 11 | .manyToOne({ targetEntity: 'Offer', inversedBy: 'pictures' }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/resource/entity/snapshot/old/offer.ts: -------------------------------------------------------------------------------- 1 | export class Offer { 2 | static setMapping(mapping) { 3 | // Pk 4 | mapping.forProperty('id').increments().primary(); 5 | 6 | // Fields 7 | mapping.forProperty('name').field({ type: 'string' }); 8 | 9 | // Relations 10 | mapping.forProperty('pictures') 11 | .oneToMany({ targetEntity: 'Picture', mappedBy: 'offer' }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/resource/entity/todo/List.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from './Todo'; 2 | 3 | export class List { 4 | static setMapping(mapping) { 5 | mapping.forProperty('id').primary().increments(); 6 | 7 | mapping.field('name', { type: 'string' }); 8 | mapping.field('done', { type: 'boolean', nullable: true }); 9 | 10 | mapping.forProperty('todos') 11 | .oneToMany({ targetEntity: Todo, mappedBy: 'list' }) 12 | .cascade([ 'persist', 'remove' ]); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/resource/entity/todo/Todo.ts: -------------------------------------------------------------------------------- 1 | import { User } from './User'; 2 | import { List } from './List'; 3 | import { Mapping } from '../../../../src/Mapping'; 4 | 5 | export class Todo { 6 | static setMapping(mapping: Mapping) { 7 | mapping.forProperty('id').primary().increments(); 8 | 9 | mapping.manyToOne('list', { targetEntity: List, inversedBy: 'todos' }); 10 | mapping.field('task', { type: 'string' }); 11 | mapping.field('done', { type: 'boolean', nullable: true }); 12 | mapping.oneToOne('creator', { targetEntity: User }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/resource/entity/todo/User.ts: -------------------------------------------------------------------------------- 1 | import { Mapping } from '../../../../src/Mapping'; 2 | 3 | export class User { 4 | public name: string; 5 | 6 | static setMapping(mapping: Mapping) { 7 | mapping.forProperty('id').primary().increments(); 8 | mapping.field('name', { type: 'string', size: 24 }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/resource/fixtures/lifecycle/Pet.csv: -------------------------------------------------------------------------------- 1 | name 2 | Kyle 3 | Jill 4 | Jullia 5 | -------------------------------------------------------------------------------- /test/resource/fixtures/lifecycle/Post.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Main post", 4 | "content": "Content...." 5 | }, 6 | { 7 | "title": "Test", 8 | "content": "Content...." 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/resource/fixtures/lifecycle/User.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "Snowden", 4 | "password": "I hate NSA" 5 | }, 6 | { 7 | "username": "Sheldon", 8 | "password": "I love trains" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/resource/fixtures/nolifecycle/Pet.csv: -------------------------------------------------------------------------------- 1 | id,name 2 | 9,Kyle 3 | 2,Jill 4 | 10,Jullia 5 | -------------------------------------------------------------------------------- /test/resource/fixtures/nolifecycle/Post.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "author": "5", 5 | "title": "Main post", 6 | "content": "Content...." 7 | }, 8 | { 9 | "id": "2", 10 | "author": "89", 11 | "title": "Test", 12 | "content": "Content...." 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/resource/fixtures/nolifecycle/User.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 5, 4 | "posts": [1], 5 | "username": "Snowden", 6 | "password": "I hate NSA" 7 | }, 8 | { 9 | "id": 89, 10 | "posts": [2], 11 | "username": "Sheldon", 12 | "password": "I love trains" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/resource/migrations.ts: -------------------------------------------------------------------------------- 1 | export interface MigrationsResults { 2 | baz: string; 3 | foo: string; 4 | bar: string; 5 | } 6 | 7 | let migrations: {up?: MigrationsResults, down?: MigrationsResults, latest?: string, revert?: string} = {}; 8 | 9 | migrations.up = { 10 | baz: 'create table `ticket` (`id` int unsigned not null auto_increment primary key, `name` varchar(255))', 11 | foo: [ 12 | 'create table `person` (`id` int unsigned not null auto_increment primary key, `name` varchar(255), `creationTime` timestamp default CURRENT_TIMESTAMP);', 13 | 'create table `animal` (`id` int unsigned not null auto_increment primary key, `name` varchar(255))', 14 | 'create table `robot` (`id` int unsigned not null auto_increment primary key, `name` varchar(255), `deadly_skill` varchar(255))', 15 | ].join('\n'), 16 | bar: 'create table `user` (`id` int unsigned not null auto_increment primary key, `name` varchar(255))', 17 | }; 18 | 19 | migrations.down = { 20 | bar: 'drop table `user`', 21 | baz: 'drop table `ticket`', 22 | foo: [ 23 | 'drop table `person`;', 24 | 'drop table `animal`', 25 | 'drop table `robot`', 26 | ].join('\n'), 27 | }; 28 | 29 | migrations.latest = [ 30 | migrations.up.baz, 31 | migrations.up.foo, 32 | migrations.up.bar, 33 | ].join('\n'); 34 | 35 | migrations.revert = [ 36 | migrations.down.bar, 37 | migrations.down.foo, 38 | migrations.down.baz, 39 | ].join('\n'); 40 | 41 | export { migrations }; 42 | -------------------------------------------------------------------------------- /test/resource/migrations/20161004123411_baz.ts: -------------------------------------------------------------------------------- 1 | export class Migration { 2 | public static up(migration) { 3 | let schemaBuilder = migration.getSchemaBuilder(); 4 | 5 | schemaBuilder.createTable('ticket', tableBuilder => { 6 | tableBuilder.increments(); 7 | tableBuilder.string('name'); 8 | }); 9 | } 10 | 11 | public static down(migration) { 12 | let schemaBuilder = migration.getSchemaBuilder(); 13 | 14 | schemaBuilder.dropTable('ticket'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/resource/migrations/20161004123412_foo.ts: -------------------------------------------------------------------------------- 1 | export class Migration { 2 | public static up(migration) { 3 | let builder = migration.getBuilder(); 4 | let schemaBuilder = builder.schema; 5 | let knex = builder.knex; 6 | 7 | schemaBuilder.createTable('person', tableBuilder => { 8 | tableBuilder.increments(); 9 | tableBuilder.string('name'); 10 | tableBuilder.timestamp('creationTime').defaultTo(knex.fn.now()); 11 | }); 12 | 13 | schemaBuilder.createTable('animal', tableBuilder => { 14 | tableBuilder.increments(); 15 | tableBuilder.string('name'); 16 | }); 17 | 18 | let schemaBuilder2 = migration.getSchemaBuilder(); 19 | 20 | schemaBuilder2.createTable('robot', tableBuilder => { 21 | tableBuilder.increments(); 22 | tableBuilder.string('name'); 23 | tableBuilder.string('deadly_skill'); 24 | }); 25 | } 26 | 27 | public static down(migration) { 28 | let schemaBuilder = migration.getSchemaBuilder(); 29 | 30 | schemaBuilder.dropTable('person'); 31 | schemaBuilder.dropTable('animal'); 32 | 33 | let schemaBuilder2 = migration.getSchemaBuilder(); 34 | 35 | schemaBuilder2.dropTable('robot'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/resource/migrations/20161004123413_bar.ts: -------------------------------------------------------------------------------- 1 | export class Migration { 2 | public static up(migration) { 3 | let schemaBuilder = migration.getSchemaBuilder(); 4 | 5 | schemaBuilder.createTable('user', tableBuilder => { 6 | tableBuilder.increments(); 7 | tableBuilder.string('name'); 8 | }); 9 | } 10 | 11 | public static down(migration) { 12 | let schemaBuilder = migration.getSchemaBuilder(); 13 | 14 | schemaBuilder.dropTable('user'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/resource/repository/CustomRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository } from '../../../src/EntityRepository'; 2 | 3 | export class CustomRepository extends EntityRepository { 4 | public foo() { 5 | return 'bar'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/resource/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpoonX/wetland/78abeb380dbf0dfc9c0d5236dbb02e63c8980aaf/test/resource/tmp/.gitkeep -------------------------------------------------------------------------------- /test/unit/ArrayCollection.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from '../../src/ArrayCollection'; 2 | import { assert } from 'chai'; 3 | 4 | 5 | describe('ArrayCollection', () => { 6 | describe('.add()', () => { 7 | it('should add an item to the collection', () => { 8 | let collection = new ArrayCollection; 9 | 10 | assert.isArray(collection); 11 | collection.add('foo'); 12 | collection.add('foo'); 13 | assert.equal(collection[0], 'foo'); 14 | assert.isUndefined(collection[1]); 15 | collection.add('foo', 'foo', 'foo'); 16 | assert.equal(collection[0], 'foo'); 17 | assert.isUndefined(collection[1]); 18 | collection.add('bar'); 19 | assert.equal(collection[1], 'bar'); 20 | }); 21 | 22 | it('should return this (ArrayCollection)', () => { 23 | let collection = new ArrayCollection; 24 | 25 | assert.strictEqual(collection.add('foo'), collection); 26 | }); 27 | }); 28 | 29 | describe('.remove', () => { 30 | it('should remove an item from the collection', () => { 31 | let collection = new ArrayCollection; 32 | 33 | collection.add('foo'); 34 | assert.equal(collection[0], 'foo'); 35 | collection.remove('foo'); 36 | assert.isUndefined(collection[0]); 37 | }); 38 | 39 | it('should return this (ArrayCollection)', () => { 40 | let collection = new ArrayCollection; 41 | 42 | assert.strictEqual(collection.remove('foo'), collection); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/unit/Cleaner.spec.ts: -------------------------------------------------------------------------------- 1 | import { Wetland } from '../../src/Wetland'; 2 | import { Schema } from '../resource/Schema'; 3 | import * as path from 'path'; 4 | import { tmpTestDir, fixturesDir, getType, User, Pet, Post, rmDataDir } from '../resource/Seeder'; 5 | 6 | describe('Cleaner', () => { 7 | beforeEach(() => rmDataDir()); 8 | 9 | describe('.clean() : database', () => { 10 | before((done) => { 11 | Schema.resetDatabase(done); 12 | }); 13 | 14 | it('Should clean the database correctly and be able to do the migration', () => { 15 | const bypassLifecyclehooks = false; 16 | 17 | const wetland = new Wetland({ 18 | dataDirectory: `${tmpTestDir}/.data`, 19 | stores: { 20 | defaultStore: { 21 | client: 'mysql', 22 | connection: { 23 | database: 'wetland_test', 24 | user: 'root', 25 | password: '', 26 | }, 27 | }, 28 | }, 29 | seed: { 30 | fixturesDirectory: path.join(fixturesDir, getType(bypassLifecyclehooks)), 31 | clean: true, 32 | bypassLifecyclehooks, 33 | }, 34 | entities: [ User, Pet, Post ], 35 | }); 36 | 37 | const seeder = wetland.getSeeder(); 38 | const cleaner = wetland.getCleaner(); 39 | const migrator = wetland.getMigrator(); 40 | 41 | return migrator.devMigrations(false) 42 | .then(() => seeder.seed()) 43 | .then(() => cleaner.clean()) 44 | .then(() => migrator.devMigrations(false)) 45 | .then(() => seeder.seed()); 46 | }); 47 | }); 48 | 49 | describe('.clean() : embedded database', () => { 50 | it('Should clean the database correctly and be able to do the migration', () => { 51 | const bypassLifecyclehooks = false; 52 | 53 | const wetland = new Wetland({ 54 | dataDirectory: `${tmpTestDir}/.data`, 55 | stores: { 56 | defaultStore: { 57 | client: 'sqlite3', 58 | useNullAsDefault: true, 59 | connection: { 60 | filename: `${tmpTestDir}/cleaner.sqlite`, 61 | }, 62 | }, 63 | }, 64 | seed: { 65 | fixturesDirectory: path.join(fixturesDir, getType(bypassLifecyclehooks)), 66 | clean: true, 67 | bypassLifecyclehooks, 68 | }, 69 | entities: [ User, Pet, Post ], 70 | }); 71 | 72 | const seeder = wetland.getSeeder(); 73 | const cleaner = wetland.getCleaner(); 74 | const migrator = wetland.getMigrator(); 75 | 76 | return migrator.devMigrations(false) 77 | .then(() => seeder.seed()) 78 | .then(() => cleaner.clean()) 79 | .then(() => migrator.devMigrations(false)) 80 | .then(() => seeder.seed()); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/unit/EntityManager.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '../../src/EntityManager'; 2 | import { Wetland } from '../../src/Wetland'; 3 | import { User } from '../resource/entity/postal/User'; 4 | import { Order } from '../resource/entity/postal/Order'; 5 | import { Address } from '../resource/entity/postal/Address'; 6 | import { Tracker } from '../resource/entity/postal/Tracker'; 7 | import { assert } from 'chai'; 8 | function getManager (): EntityManager { 9 | let wetland = new Wetland({ 10 | entities: [ User, Order ], 11 | mapping : { 12 | defaultNamesToUnderscore: true, 13 | }, 14 | }); 15 | 16 | return wetland.getEntityManager(); 17 | } 18 | 19 | describe('EntityManager', () => { 20 | describe('.constructor()', () => { 21 | it('should set wetland', () => { 22 | let config = getManager().getConfig(); 23 | 24 | assert.propertyVal(config.fetch('mapping'), 'defaultNamesToUnderscore', true); 25 | }); 26 | }); 27 | 28 | describe('.createScope()', () => { 29 | it('should create a scope', () => { 30 | let scope = getManager().createScope(); 31 | 32 | assert.property(scope.getIdentityMap(), 'map'); 33 | }); 34 | }); 35 | 36 | describe('.getEntity()', () => { 37 | it('should fetch the entity', () => { 38 | let entity = getManager().getEntity('User'); 39 | 40 | assert.typeOf(entity, 'function'); 41 | }); 42 | 43 | it('should throw an error while fetching an unknown entity', () => { 44 | assert.throws(() => { 45 | return getManager().getEntity('Product'); 46 | }, 'No entity found for "Product".'); 47 | }); 48 | }); 49 | 50 | describe('.getEntities()', () => { 51 | it('should retrieve all the registered entities', () => { 52 | let entities = getManager().getEntities(); 53 | 54 | assert.property(entities, 'User'); 55 | assert.property(entities, 'Order'); 56 | }); 57 | }); 58 | 59 | describe('.registerEntity()', () => { 60 | it('Should register an entity', () => { 61 | let manager = getManager().registerEntity(Address); 62 | 63 | assert.doesNotThrow(() => { 64 | return manager.getEntity('Address'); 65 | }, 'No entity found for "Address".'); 66 | }); 67 | }); 68 | 69 | describe('.getMapping()', () => { 70 | it('Should get the mapping of the provided entity', () => { 71 | let fieldNames = getManager().getMapping('User').getFieldNames(); 72 | 73 | assert.deepEqual(fieldNames, [ 'id', 'name' ]); 74 | }); 75 | }); 76 | 77 | describe('.registerEntities()', () => { 78 | it('Should register multiple entities', () => { 79 | let manager = getManager().registerEntities([ Address, Tracker ]); 80 | 81 | assert.doesNotThrow(() => { 82 | return manager.getEntity('Address'); 83 | }, 'No entity found for "Address".'); 84 | 85 | assert.doesNotThrow(() => { 86 | return manager.getEntity('Tracker'); 87 | }, 'No entity found for "Tracker".'); 88 | }); 89 | }); 90 | 91 | describe('.resolveEntityReference()', () => { 92 | it('Should resolve provided value to an entity reference', () => { 93 | assert.equal(getManager().resolveEntityReference('User'), getManager().getEntity('User')); 94 | assert.isFunction(getManager().resolveEntityReference(() => {})); 95 | assert.isArray(getManager().resolveEntityReference([])); 96 | assert.isNull(getManager().resolveEntityReference(null)); 97 | }); 98 | }); 99 | 100 | }); 101 | -------------------------------------------------------------------------------- /test/unit/Hydrator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Wetland } from '../../src/Wetland'; 2 | import { Scope } from '../../src/Scope'; 3 | import { Hydrator } from '../../src/Hydrator'; 4 | import { User } from '../resource/entity/postal/User'; 5 | import { Tracker } from '../resource/entity/postal/Tracker'; 6 | import { assert } from 'chai'; 7 | 8 | function getHydrator (): Hydrator { 9 | return new Hydrator(getManager()); 10 | } 11 | 12 | function getManager (): Scope { 13 | let wetland = new Wetland(); 14 | 15 | return wetland.getManager(); 16 | } 17 | 18 | describe('Hydrator', () => { 19 | describe('.consturctor()', () => { 20 | it('should define constructor properties', () => { 21 | let hydrator = getHydrator(); 22 | 23 | assert.property(hydrator, 'unitOfWork'); 24 | assert.property(hydrator, 'entityManager'); 25 | assert.property(hydrator, 'identityMap'); 26 | }); 27 | }); 28 | 29 | describe('.fromSchema()', () => { 30 | it('should map to entities', () => { 31 | let entity = getHydrator().fromSchema({ 32 | name: 'foo', 33 | }, User); 34 | 35 | assert.propertyVal(entity, 'name', 'foo'); 36 | }); 37 | 38 | it('should not map invalid value to entities', () => { 39 | let entity = getHydrator().fromSchema({ 40 | bar: 'foo', 41 | }, User); 42 | 43 | assert.notProperty(entity, 'bar'); 44 | }); 45 | 46 | it('should map to entities with empty object', () => { 47 | let entity = getHydrator().fromSchema({ 48 | name: 'foo', 49 | }, {}); 50 | 51 | assert.notProperty(entity, 'name'); 52 | }); 53 | }); 54 | 55 | describe('.addRecipe', () => { 56 | it('should add a recipe', () => { 57 | let recipe = getHydrator().addRecipe(null, 'foo', getManager().getMapping(User), 'single'); 58 | 59 | assert.deepEqual(recipe.primaryKey, { alias: 'foo.id', property: 'id' }); 60 | assert.isNull(recipe.parent); 61 | assert.isFalse(recipe.hydrate); 62 | assert.equal(recipe.type, 'single'); 63 | assert.isObject(recipe.columns); 64 | assert.isUndefined(recipe.property); 65 | assert.equal(recipe.alias, 'foo'); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/unit/Lifecyclehooks.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { Wetland } from '../../src/Wetland'; 3 | import { Entity } from '../../src/Entity'; 4 | import * as path from 'path'; 5 | import * as Bluebird from 'bluebird'; 6 | import * as rimraf from 'rimraf'; 7 | import { EntityCtor } from '../../src'; 8 | 9 | const tmpTestDir = path.join(__dirname, '../.tmp'); 10 | const dataDir = `${tmpTestDir}/.data`; 11 | 12 | class User extends Entity { 13 | id; 14 | 15 | username; 16 | 17 | password; 18 | 19 | static setMapping(mapping) { 20 | mapping.forProperty('id').increments().primary(); 21 | mapping.forProperty('username').field({ type: 'string' }); 22 | mapping.forProperty('password').field({ type: 'string' }); 23 | } 24 | 25 | beforeCreate() { 26 | this.password = Buffer.from(this.password).toString('base64'); // Do not do that in prod 27 | } 28 | 29 | beforeUpdate(values) { 30 | values.password = Buffer.from(values.password).toString('base64'); // Do not do that in prod 31 | } 32 | } 33 | 34 | function getWetland (name) { 35 | return new Wetland({ 36 | entities : [ User ], 37 | dataDirectory: `${tmpTestDir}/.data`, 38 | stores : { 39 | defaultStore: { 40 | useNullAsDefault: true, 41 | client : 'sqlite3', 42 | connection : { 43 | filename: `${tmpTestDir}/lifecyclehooks-${name}.sqlite`, 44 | }, 45 | }, 46 | }, 47 | }); 48 | } 49 | 50 | describe('Lifecyclehooks', () => { 51 | beforeEach(() => { 52 | const rmDir: any = Bluebird.promisify(rimraf); 53 | 54 | return rmDir(dataDir); 55 | }); 56 | 57 | describe('.beforeCreate()', () => { 58 | it('should correctly base64 the password before create', () => { 59 | const wetland = getWetland('before-create'); 60 | const manager = wetland.getManager(); 61 | const UserRepository = manager.getRepository(User); 62 | 63 | const password = 'Test'; 64 | const username = 'Test'; 65 | const newUser = new User; 66 | newUser.username = username; 67 | newUser.password = password; 68 | 69 | return wetland.getMigrator().devMigrations(false) 70 | .then(() => { 71 | return manager.persist(newUser) 72 | .flush(); 73 | }) 74 | .then(() => { 75 | return UserRepository.findOne({ username }); 76 | }) 77 | .then((user: User) => { 78 | assert.notEqual(user.password, password); 79 | assert.equal(user.password, Buffer.from(password).toString('base64')); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('.beforeUpdate()', () => { 85 | it('should correctly base64 the password before update', () => { 86 | const wetland = getWetland('before-update'); 87 | 88 | const manager = wetland.getManager(); 89 | const UserRepository = manager.getRepository(manager.getEntity('User') as EntityCtor); 90 | 91 | const username = 'John Doe.'; 92 | const password = '123456789'; 93 | const updatedPassword = 'popopopop'; 94 | const newUser = new User; 95 | newUser.username = username; 96 | newUser.password = password; 97 | 98 | return wetland.getMigrator().devMigrations(false) 99 | .then(() => { 100 | return manager.persist(newUser).flush(); 101 | }) 102 | .then(() => { 103 | return UserRepository.findOne({ username }); 104 | }) 105 | .then((user: User) => { 106 | user.password = updatedPassword; 107 | return manager.flush(); 108 | }) 109 | .then(() => { 110 | return UserRepository.findOne({ username }); 111 | }) 112 | .then((user: User) => { 113 | assert.notEqual(user.password, updatedPassword); 114 | assert.equal(user.password, Buffer.from(updatedPassword).toString('base64')); 115 | }); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/unit/MetaData.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { Wetland } from '../../src/Wetland'; 3 | import { MetaData } from '../../src/MetaData'; 4 | import { Mapping } from '../../src/Mapping'; 5 | import { Product } from '../resource/entity/shop/product'; 6 | import { Homefront } from 'homefront'; 7 | import { EntityProxy } from '../../src/EntityProxy'; 8 | 9 | function getUnitOfWork (entity) { 10 | let wetland = new Wetland; 11 | 12 | if (entity) { 13 | wetland.registerEntity(Product); 14 | } 15 | 16 | return wetland.getManager().getUnitOfWork(); 17 | } 18 | 19 | describe('forTarget()', () => { 20 | it('should get metadata for provided target', () => { 21 | let metaData = MetaData.forTarget(Product); 22 | 23 | assert.isTrue(MetaData['metaMap'].has(Product)); 24 | assert.instanceOf(metaData, Homefront); 25 | assert.instanceOf(metaData.fetch('mapping'), Mapping); 26 | }); 27 | }); 28 | 29 | describe('ensure', () => { 30 | it('should ensure metadata', () => { 31 | let metaData = MetaData.forTarget(Product); 32 | 33 | assert.isTrue(MetaData['metaMap'].has(Product)); 34 | assert.instanceOf(metaData, Homefront); 35 | }); 36 | }); 37 | 38 | describe('forInstance()', () => { 39 | it('should get metadata for provided instance', () => { 40 | let unitOfWork = getUnitOfWork(Product); 41 | let proxied = EntityProxy.patchEntity(new Product, unitOfWork.getEntityManager()); 42 | 43 | unitOfWork.registerNew(proxied.getTarget()); 44 | 45 | proxied.activateProxying(); 46 | 47 | let metadata = MetaData.forInstance(proxied); 48 | 49 | assert.strictEqual(metadata.fetch('entityState.state'), 'new'); 50 | }); 51 | }); 52 | 53 | describe('clear()', () => { 54 | it('should clear metadata for provided targets', () => { 55 | assert.isTrue(MetaData['metaMap'].has(Product)); 56 | 57 | MetaData.clear(Product); 58 | 59 | assert.isFalse(MetaData['metaMap'].has(Product)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/Migrator/MigrationFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { MigrationFile } from '../../../src/Migrator/MigrationFile'; 5 | 6 | let migrationsDir = __dirname + '/../../resource/migrations'; 7 | let tmpMigrations = path.join(migrationsDir, 'tmp'); 8 | let cleanDirectory = directory => { 9 | try { 10 | fs.readdirSync(directory).forEach(file => { 11 | try { 12 | fs.unlinkSync(path.join(directory, file)); 13 | } catch (error) { 14 | cleanDirectory(path.join(directory, file)); 15 | fs.unlinkSync(path.join(directory, file)); 16 | } 17 | }); 18 | fs.rmdirSync(directory); 19 | } catch (e) { 20 | } 21 | }; 22 | 23 | describe('MigrationFile', () => { 24 | describe('.constructor()', () => { 25 | it('should set the config', () => { 26 | let config = { extension: 'js', tableName: 'wetland_migrations', directory: tmpMigrations }; 27 | let migrationFile = new MigrationFile(config); 28 | 29 | assert.typeOf(migrationFile.getConfig(), 'object'); 30 | }); 31 | }); 32 | 33 | describe('.getConfig()', () => { 34 | it('should get the config', () => { 35 | let config = { directory: '' }; 36 | let migrationFile = new MigrationFile(config); 37 | 38 | assert.typeOf(migrationFile.getConfig(), 'object'); 39 | }); 40 | }); 41 | 42 | describe('.create()', () => { 43 | before(() => { 44 | cleanDirectory(tmpMigrations); 45 | fs.mkdirSync(tmpMigrations); 46 | }); 47 | 48 | after(() => { 49 | cleanDirectory(tmpMigrations); 50 | }); 51 | 52 | it('should create a new migration file', done => { 53 | let migrationFile = new MigrationFile({ 54 | extension: 'js', 55 | tableName: 'wetland_migrations', 56 | directory: tmpMigrations, 57 | }); 58 | 59 | migrationFile.create('created').then(() => { 60 | let migrations = fs.readdirSync(tmpMigrations); 61 | let migration = require(tmpMigrations + '/' + migrations[0])['Migration']; 62 | 63 | assert.isTrue(Reflect.has(migration, 'up')); 64 | assert.isTrue(Reflect.has(migration, 'down')); 65 | assert.match(migrations[0], /^\d{14}_created\.js$/); 66 | assert.equal(migrations.length, 1); 67 | 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should complain about an invalid write path', done => { 73 | let migrationFile = new MigrationFile({ 74 | extension: 'js', 75 | tableName: 'wetland_migrations', 76 | directory: tmpMigrations, 77 | }); 78 | 79 | migrationFile.create('foo/created').catch(error => { 80 | assert.equal(error.code, 'ENOENT'); 81 | 82 | done(); 83 | }); 84 | }); 85 | 86 | it('should complain about an invalid read path', done => { 87 | let migrationFile = new MigrationFile({ 88 | extension: 'js', 89 | tableName: 'wetland_migrations', 90 | directory: tmpMigrations + '/ooooops', 91 | }); 92 | 93 | migrationFile.create('foo/created').catch(error => { 94 | assert.equal(error.code, 'ENOENT'); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('.getMigrations()', () => { 101 | it('Should give me the names of all available migrations', done => { 102 | let migrationFile = new MigrationFile({ 103 | extension: 'js', 104 | tableName: 'wetland_migrations', 105 | directory: __dirname + '/../../resource/migrations', 106 | }); 107 | 108 | migrationFile.getMigrations().then(files => { 109 | expect(files).to.have.same.members([ 110 | '20161004123412_foo', 111 | '20161004123413_bar', 112 | '20161004123411_baz', 113 | ]); 114 | 115 | done(); 116 | }).catch(done); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/unit/SchemaBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as Promise from 'bluebird'; 3 | import { Wetland } from '../../src/Wetland'; 4 | import { SchemaBuilder } from '../../src/SchemaBuilder'; 5 | import { Store } from '../../src/Store'; 6 | import { Schema } from '../resource/Schema'; 7 | import { schemas } from '../resource/schemas'; 8 | 9 | describe('SchemaBuilder', () => { 10 | beforeEach(done => { 11 | Schema.resetDatabase(done); 12 | }); 13 | 14 | describe('.create()', () => { 15 | it('should create my tables (todo)', done => testEntities('todo', done)); 16 | it('should create my tables (postal)', done => testEntities('postal', done)); 17 | }); 18 | }); 19 | 20 | function testEntities (section, done) { 21 | let wetland = new Wetland({ 22 | entityPath: __dirname + '/../resource/entity/' + section, 23 | stores : { 24 | defaultStore: { 25 | client : 'mysql', 26 | connection: { 27 | user : 'root', 28 | host : '127.0.0.1', 29 | database: 'wetland_test', 30 | }, 31 | }, 32 | }, 33 | }); 34 | 35 | let connection = wetland.getStore().getConnection(Store.ROLE_MASTER); 36 | 37 | wetland.getSchemaManager().create().then(() => { 38 | return testProperty(connection, section, 'columns', 'columns') 39 | .then(() => testProperty(connection, section, 'constraints', 'key_column_usage')) 40 | .then(() => testProperty(connection, section, 'referentialConstraints', 'referential_constraints')) 41 | .then(() => done()) 42 | .catch(done); 43 | }); 44 | } 45 | 46 | function testProperty (connection, section, property, table) { 47 | return Promise.all(schemas[section][property].map(target => { 48 | return connection 49 | .from('information_schema.' + table) 50 | .where(target) 51 | .where(property === 'columns' ? 'table_schema' : 'constraint_schema', '=', 'wetland_test') 52 | .then(result => assert.lengthOf(result, 1, `'${section}' broken with ${JSON.stringify(target)}`)); 53 | })); 54 | } 55 | -------------------------------------------------------------------------------- /test/unit/Scope.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { Wetland } from '../../src/Wetland'; 3 | import { Scope } from '../../src/Scope'; 4 | import { UnitOfWork } from '../../src/UnitOfWork'; 5 | import { Simple } from '../resource/entity/Simple'; 6 | import { EntityRepository } from '../../src/EntityRepository'; 7 | import { WithCustomRepository } from '../resource/entity/WithCustomRepository'; 8 | import { CustomRepository } from '../resource/repository/CustomRepository'; 9 | import { NoAutoIncrement } from '../resource/entity/NoAutoIncrement'; 10 | import { Schema } from '../resource/Schema'; 11 | 12 | function entityManager (entities?): Scope { 13 | let wetland = new Wetland({}); 14 | 15 | if (entities) { 16 | wetland.registerEntities(entities); 17 | } 18 | 19 | return wetland.getManager(); 20 | } 21 | 22 | describe('Scope', () => { 23 | describe('.getRepository()', () => { 24 | it('should return a default repository', () => { 25 | let scope = entityManager([ Simple ]); 26 | 27 | assert.instanceOf(scope.getRepository(Simple), EntityRepository); 28 | }); 29 | 30 | it('should return a custom repository', () => { 31 | let scope = entityManager([ WithCustomRepository ]); 32 | 33 | assert.instanceOf(scope.getRepository(WithCustomRepository), EntityRepository); 34 | assert.instanceOf(scope.getRepository(WithCustomRepository), CustomRepository); 35 | }); 36 | }); 37 | 38 | describe('.getUnitOfWork()', () => { 39 | it('should return the UnitOfWork', () => { 40 | let scope = entityManager(); 41 | 42 | assert.instanceOf(scope.getUnitOfWork(), UnitOfWork); 43 | }); 44 | }); 45 | 46 | describe('.persist()', () => { 47 | it('should add the entity to the unitOfWork', () => { 48 | let scope = entityManager([ Simple ]); 49 | let simple = new Simple; 50 | 51 | scope.persist(simple); 52 | 53 | assert.strictEqual(UnitOfWork.getObjectState(simple), UnitOfWork.STATE_NEW); 54 | }); 55 | 56 | it('should return Scope', () => { 57 | let scope = entityManager([ Simple ]); 58 | 59 | assert.strictEqual(scope.persist(new Simple), scope); 60 | }); 61 | }); 62 | 63 | describe('.remove()', () => { 64 | it('should add the entity to the unitOfWork as "deleted"', () => { 65 | let scope = entityManager([ Simple ]); 66 | let simple = new Simple; 67 | 68 | scope.remove(simple); 69 | 70 | assert.strictEqual(UnitOfWork.getObjectState(simple), UnitOfWork.STATE_DELETED); 71 | }); 72 | 73 | it('should return Scope', () => { 74 | let scope = entityManager([ Simple ]); 75 | 76 | assert.strictEqual(scope.remove(new Simple), scope); 77 | }); 78 | }); 79 | 80 | describe('.getReference()', () => { 81 | it('should return a reference', () => { 82 | // @todo test if delete works like this: 83 | // entityManager.remove(entityManager.getReference('Foo', 6)); 84 | }); 85 | }); 86 | 87 | describe('.refresh()', () => { 88 | const wetland = new Wetland({ 89 | stores: { 90 | defaultStore: { 91 | client: 'mysql', 92 | connection: { 93 | user: 'root', 94 | host: '127.0.0.1', 95 | database: 'wetland_test', 96 | }, 97 | }, 98 | }, 99 | entities: [ NoAutoIncrement ], 100 | }); 101 | 102 | before((done) => { 103 | Schema.resetDatabase(() => wetland.getSchemaManager().create().then(() => done())); 104 | }); 105 | 106 | it('should throw an error on refresh without AI if refresh is enabled.', done => { 107 | const scope = wetland.getManager(); 108 | 109 | scope.persist(Object.assign(new NoAutoIncrement, { id: 123, foo: 'foo' })); 110 | 111 | // Flush with default (refresh enabled). 112 | scope.flush(false, false) 113 | .then(() => done('flush should have failed.')) 114 | .catch(error => { 115 | assert.strictEqual(error.message, 'Cannot refresh entity without a PK value.'); 116 | 117 | done(); 118 | }); 119 | }); 120 | 121 | it('should not throw an error on refresh without PK if refresh is disabled', done => { 122 | const scope = wetland.getManager(); 123 | 124 | scope.persist(Object.assign(new NoAutoIncrement, { id: 456, foo: 'foo' })); 125 | 126 | // Flush with refresh disabled. 127 | scope.flush(false, false, { refreshCreated: false }) 128 | .then(() => done()) 129 | .catch(() => done('Flush should have succeeded.')); 130 | }); 131 | }); 132 | 133 | describe('.clear()', () => { 134 | it('should reset the unit of work', () => { 135 | let scope = entityManager([ Simple ]); 136 | let simple = new Simple; 137 | 138 | scope.remove(simple); 139 | 140 | assert.strictEqual(UnitOfWork.getObjectState(simple), UnitOfWork.STATE_DELETED); 141 | 142 | scope.clear(); 143 | 144 | assert.strictEqual(UnitOfWork.getObjectState(simple), UnitOfWork.STATE_UNKNOWN); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/unit/SnapshotManager.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { Wetland } from '../../src/Wetland'; 3 | import { SchemaBuilder } from '../../src/SchemaBuilder'; 4 | import { SnapshotManager } from '../../src/SnapshotManager'; 5 | import oldEntities from '../resource/entity/snapshot/old'; 6 | import newEntities from '../resource/entity/snapshot/new'; 7 | import { Book } from '../resource/entity/book/book'; 8 | import { Publisher } from '../resource/entity/book/publisher'; 9 | 10 | function getWetland (entities?): Wetland { 11 | let wetland = new Wetland({ 12 | stores: { 13 | defaultStore: { 14 | client : 'mysql', 15 | connection: { 16 | user : 'root', 17 | host : '127.0.0.1', 18 | database: 'wetland_test', 19 | }, 20 | }, 21 | }, 22 | }); 23 | 24 | if (entities) { 25 | wetland.registerEntities(entities); 26 | } 27 | 28 | return wetland; 29 | } 30 | 31 | function getMapping (entities): Object { 32 | return getWetland(entities).getSnapshotManager().getSerializable(); 33 | } 34 | 35 | describe('SnapshotManager', () => { 36 | describe('diff(fk): change foreign key', () => { 37 | it('Should drop the old foreign key and create a new one', () => { 38 | let oldMapping = getMapping(oldEntities), 39 | newMapping = getMapping(newEntities); 40 | 41 | let wetland = getWetland(), 42 | snapshotManager = wetland.getSnapshotManager(), 43 | schemaBuilder = new SchemaBuilder(wetland.getManager()); 44 | 45 | let diff = snapshotManager.diff(oldMapping, newMapping); 46 | 47 | let sqlStatement = schemaBuilder.process(diff).getSQL().split('\n'); 48 | 49 | assert.equal(sqlStatement[0], 'alter table `media` drop foreign key `media_offer_id_foreign`;'); 50 | assert.equal(sqlStatement[1], 'alter table `media` add constraint `media_offer_id_foreign` foreign key (`offer_id`) references `offer` (`id`) on delete cascade'); 51 | }); 52 | }); 53 | 54 | describe('diff(jc): create join column', () => { 55 | it('Should be able to create a non null foreign key', () => { 56 | let oldMapping = getMapping([]), 57 | newMapping = getMapping([ Book, Publisher ]); 58 | 59 | let wetland = getWetland(), 60 | snapshotManager = wetland.getSnapshotManager(), 61 | schemaBuilder = new SchemaBuilder(wetland.getManager()); 62 | 63 | let diff = snapshotManager.diff(oldMapping, newMapping); 64 | 65 | let sqlStatement = schemaBuilder.process(diff).getSQL().split('\n'); 66 | 67 | assert.equal(sqlStatement[1], 'create table `publisher` (`id` int unsigned not null auto_increment primary key, `name` varchar(24) not null);'); 68 | assert.equal(sqlStatement[0], 'create table `book` (`id` int unsigned not null auto_increment primary key, `name` varchar(24) not null, `publisher_id` int unsigned not null);'); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/unit/Wetland.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { entityExtensionRegexp, entityFilterRegexp } from '../../src/Wetland'; 3 | 4 | describe('Wetland', () => { 5 | describe('.initializeConfig()', () => { 6 | it('load the entities we expect using the regex used.', () => { 7 | const testFiles = [ 8 | 'some-entity.js', 9 | 'some-entity.garbage', 10 | 'some-entity.d.js', 11 | 'some.entity.js', 12 | 'some.other-entity.js', 13 | 'some.other-entity.model.thing.js', 14 | '-some.other-entity.model.thing.js', 15 | 'some-entity.ts', 16 | 'some-entity.d.ts', 17 | 'some.entity.ts', 18 | 'some.other-entity.ts', 19 | 'some.other-entity.model.thing.ts', 20 | '-some.other-entity.model.thing.ts', 21 | ]; 22 | 23 | const expected = [ 24 | 'some-entity', 25 | 'some.entity', 26 | 'some.other-entity', 27 | 'some.other-entity.model.thing', 28 | '-some.other-entity.model.thing', 29 | 'some-entity', 30 | 'some.entity', 31 | 'some.other-entity', 32 | 'some.other-entity.model.thing', 33 | '-some.other-entity.model.thing', 34 | ]; 35 | 36 | const result = testFiles 37 | .filter(match => match.search(entityFilterRegexp) > -1) 38 | .map(entity => entity.replace(entityExtensionRegexp, '')); 39 | 40 | assert.deepEqual(result, expected); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "2.0.0", 3 | "compileOnSave" : true, 4 | "compilerOptions": { 5 | "declaration" : true, 6 | "target" : "es6", 7 | "moduleResolution" : "node", 8 | "module" : "commonjs", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata" : true, 11 | "outDir" : "dist", 12 | "lib" : [ 13 | "es5", 14 | "es6", 15 | "es7" 16 | ] 17 | }, 18 | "lib" : "es2015", 19 | "exclude" : [ 20 | "dist", 21 | ".*", 22 | "node_modules", 23 | "coverage", 24 | "typings" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-eslint-rules", "tslint-config-prettier"], 3 | "rules": { 4 | "quotes": ["error", "single"], 5 | "semi": ["error", "always"], 6 | "comma-dangle": ["error", { 7 | "arrays": "always-multiline", 8 | "objects": "always-multiline", 9 | "imports": "never", 10 | "exports": "never", 11 | "functions": "ignore" 12 | }], 13 | "varspacing/var-spacing": ["error"], 14 | "key-spacing": ["error", {"align": "colon"}], 15 | "padded-blocks": ["error", "never"], 16 | "padding-line-between-statements": [ 17 | "error", 18 | {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"}, 19 | {"blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]}, 20 | {"blankLine": "always", "prev": "*", "next": "return"} 21 | ], 22 | "indent": [true, "2"], 23 | "function-paren-newline": ["error", "multiline"], 24 | "space-before-function-paren": [true, { 25 | "anonymous": "always", 26 | "named": "always", 27 | "asyncArrow": "always" 28 | }], 29 | "generator-star-spacing": ["error", "after"], 30 | "object-curly-spacing": [true, "always"], 31 | "array-bracket-spacing": [ 32 | true, 33 | "always" 34 | ], 35 | "quotemark": [true, "single", "jsx-double", "avoid-escape"], 36 | "semicolon": [true, "always"], 37 | "trailing-comma": [ 38 | true, 39 | { 40 | "multiline": "always", 41 | "singleline": "never" 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function(w) { 2 | return { 3 | files : [ 4 | 'src/**/*.ts', 5 | 'src/**/*.dist', 6 | 'test/resource/**/*.ts' 7 | ], 8 | tests : [ 9 | 'test/helper.ts', 10 | 'test/**/*.spec.ts' 11 | ], 12 | env : { 13 | type : 'node', 14 | runner: 'node' 15 | }, 16 | workers : { 17 | recycle: true 18 | }, 19 | testFramework: 'mocha', 20 | debug : false, 21 | compilers : { 22 | '**/*.ts': w.compilers.typeScript({ 23 | module : 'commonjs', "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | }) 26 | } 27 | }; 28 | }; 29 | --------------------------------------------------------------------------------