├── .eslintrc.js ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .husky ├── .gitignore └── pre-commit ├── .vscode ├── divider.code-snippets └── export.code-snippets ├── LICENSE ├── README.md ├── README_zh.md ├── docs ├── .vuepress │ └── config.js ├── en │ ├── advanced │ │ ├── entity-events.md │ │ ├── identity-map.md │ │ └── serializers.md │ ├── guide │ │ ├── accessing-fields.md │ │ ├── defining-entities.md │ │ ├── exporting-entities.md │ │ ├── introduction.md │ │ ├── preparing-the-orm.md │ │ └── resolving-data.md │ └── index.md └── zh │ ├── advanced │ ├── entity-events.md │ ├── identity-map.md │ └── serializers.md │ ├── guide │ ├── accessing-fields.md │ ├── defining-entities.md │ ├── exporting-entities.md │ ├── introduction.md │ ├── preparing-the-orm.md │ └── resolving-data.md │ └── index.md ├── jest.config.js ├── lint-staged.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── res ├── defining-entities.gif └── exporting-entities.gif ├── src ├── common │ ├── extended-promise.class.ts │ ├── matched-keys.type.ts │ ├── type.interface.ts │ └── unmatched-keys.type.ts ├── core │ ├── __test__ │ │ ├── berry-orm.class.spec.ts │ │ ├── entity-event-manager.class.spec.ts │ │ ├── entity-manager.class.spec.ts │ │ └── identity-map.spec.ts │ ├── berry-orm.class.ts │ ├── entity-event-manager.class.ts │ ├── entity-manager-export-expansions-empty.type.ts │ ├── entity-manager-export-expansions.type.ts │ ├── entity-manager.class.ts │ ├── entity-relation-manager.class.ts │ └── identity-map.class.ts ├── entity │ ├── any-entity.type.ts │ ├── base-entity.class.ts │ ├── entity-data │ │ ├── entity-data-common.type.ts │ │ ├── entity-data-exported.type.ts │ │ ├── entity-data-relational.type.ts │ │ └── entity-data.type.ts │ ├── entity-from-relation-field-value.type.ts │ ├── entity-representation.type.ts │ └── entity-type.interface.ts ├── field │ ├── field-accessors │ │ ├── __test__ │ │ │ └── base-field.accessor.spec.ts │ │ ├── base-field.accessor.ts │ │ ├── common-field.accessor.ts │ │ ├── field-access-denied.error.ts │ │ ├── primary-field.accessor.ts │ │ ├── relation-field-to-many.accessor.ts │ │ └── relation-field-to-one.accessor.ts │ ├── field-data │ │ └── relation-field-data.type.ts │ ├── field-discriminator.class.ts │ ├── field-names │ │ ├── common-field.type.ts │ │ ├── entity-field-base.type.ts │ │ ├── entity-field.type.ts │ │ ├── primary-field-possible.type.ts │ │ ├── primary-field.type.ts │ │ ├── relation-field-to-many.type.ts │ │ ├── relation-field-to-one.type.ts │ │ └── relation-field.type.ts │ └── field-values │ │ ├── collection.class.ts │ │ ├── empty-value.type.ts │ │ ├── primary-key-possible.type.ts │ │ └── primary-key.type.ts ├── index.ts ├── meta │ ├── entity-meta.error.ts │ ├── meta-decorators │ │ ├── __test__ │ │ │ ├── entity.decorator.spec.ts │ │ │ ├── field.decorator.spec.ts │ │ │ ├── primary.decorator.spec.ts │ │ │ └── relation.decorator.spec.ts │ │ ├── entity.decorator.ts │ │ ├── field.decorator.ts │ │ ├── primary.decorator.ts │ │ └── relation.decorator.ts │ └── meta-objects │ │ ├── entity-field-meta.class.ts │ │ ├── entity-meta.class.ts │ │ └── entity-relation-meta.class.ts ├── serializer │ ├── abstract.serializer.ts │ ├── built-in │ │ └── date.serializer.ts │ ├── scalar.type.ts │ ├── serializer-map │ │ ├── nested-serializer-map-empty.type.ts │ │ ├── nested-serializer-map.type.ts │ │ ├── serializer-map-empty.type.ts │ │ └── serializer-map.type.ts │ └── serializer-type.interface.ts ├── symbols.ts ├── tsconfig.build.json └── tsconfig.json ├── tsconfig.base.json └── tsconfig.build.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | jest: true, 7 | node: true, 8 | }, 9 | parser: "@typescript-eslint/parser", 10 | plugins: ["@typescript-eslint/eslint-plugin", "simple-import-sort"], 11 | extends: [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "prettier", 15 | ], 16 | rules: { 17 | "@typescript-eslint/no-unused-vars": [ 18 | "error", 19 | { argsIgnorePattern: "^_", ignoreRestSiblings: true }, 20 | ], 21 | "@typescript-eslint/no-non-null-assertion": "off", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "simple-import-sort/imports": "error", 24 | "simple-import-sort/exports": "error", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | branches: 9 | - develop 10 | - master 11 | 12 | jobs: 13 | lint: 14 | timeout-minutes: 30 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v2 19 | - name: Installation 20 | uses: bahmutov/npm-install@v1 21 | with: 22 | install-command: npm install 23 | - name: Lint 24 | run: npm run lint 25 | - name: Style check with Prettier 26 | run: npm run format:diff 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | branches: 9 | - develop 10 | - master 11 | 12 | jobs: 13 | test: 14 | timeout-minutes: 30 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node: ["14"] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node }} 25 | - name: Installation 26 | uses: bahmutov/npm-install@v1 27 | with: 28 | install-command: npm install 29 | - name: Test 30 | run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | 4 | /docs/.vuepress/dist 5 | .eslintcache 6 | 7 | /.vscode/settings.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gh-pages"] 2 | path = gh-pages 3 | url = https://github.com/thenightmarex/berry-orm 4 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged --allow-empty 5 | -------------------------------------------------------------------------------- /.vscode/divider.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "devider": { 3 | "scope": "javascript,typescript", 4 | "prefix": "divider", 5 | "body": [ 6 | "// --------------------------------------------------------------------------", 7 | "// $0" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/export.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your berry-orm 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "export": { 19 | "prefix": "expo", 20 | "scope": "javascript,typescript", 21 | "body": ["export { $0 } from \"./$1\";"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Berry ORM 2 | 3 | [中文](./README_zh.md) 4 | 5 | A pure ORM that takes full advantage of TypeScript's type system. 6 | 7 | ```sh 8 | npm i berry-orm 9 | ``` 10 | 11 | > **Tested TypeScript Compiler Version: 4.1/4.2/4.3/4.4/4.5** 12 | 13 | # Why? What for? 14 | 15 | Berry ORM is a **pure** ORM designed mainly for type-safe Web frontend development. It only focuses on mapping data to class instances, no database, no query. 16 | 17 | Wait, frontend? Why we need an ORM for frontend? 18 | 19 | ## The Issue 20 | 21 | Let's say we have two entities at our backend: 22 | 23 | ```ts 24 | interface User { 25 | id: number; 26 | name: string; 27 | posts: Post[]; 28 | } 29 | 30 | interface Post { 31 | id: number; 32 | owner: User; 33 | title: string; 34 | } 35 | ``` 36 | 37 | And we have an API `/users` that returns a list of user data: 38 | 39 | ```json 40 | [ 41 | { 42 | "id": 1, 43 | "name": "Charles", 44 | "posts": [ 45 | { "id": 1, "title": "Hello" }, 46 | { "id": 2, "title": "World" } 47 | ] 48 | } 49 | ] 50 | ``` 51 | 52 | How would we process the data at the frontend? 53 | 54 | Since the `owner` field of `Post` doesn't exist, we cannot simply copy the entity type from the backend, so maybe we could adjust the interface and simply store it without any processing: 55 | 56 | ```ts 57 | interface User { 58 | id: number; 59 | name: string; 60 | posts: Post[]; 61 | } 62 | 63 | interface Post { 64 | id: number; 65 | title: string; 66 | // owner: User; 67 | } 68 | ``` 69 | 70 | This works, but we loss the ability to access the inverse relation from posts: 71 | 72 | ```ts 73 | user.posts[0].owner; 74 | ``` 75 | 76 | What's more, what if we call an API elsewhere that returns the data of the updated post? 77 | 78 | ```json 79 | { "id": 1, "title": "Hello!!!" } 80 | ``` 81 | 82 | Yes, there would be two objects storing different data for one post. Data inconsistencies can occur. 83 | 84 | Now let's consider another API `/posts` that returns a list of post data: 85 | 86 | ```json 87 | [ 88 | { 89 | "id": 1, 90 | "owner": { "id": 1, "name": "Charles" }, 91 | "title": "Hello" 92 | }, 93 | { 94 | "id": 2, 95 | "owner": { "id": 1, "name": "Charles" }, 96 | "title": "World" 97 | } 98 | ] 99 | ``` 100 | 101 | Now the `owner` field of `Post` is back, while the `posts` field of `User` is gone. 102 | 103 | How should we define the interfaces? 104 | 105 | We can repeat ourselves by defining another two interfaces, or we can make a mess by using utility types `Pick` and `Omit` everywhere. 106 | 107 | ## The Solution 108 | 109 | Berry ORM is here! 110 | 111 | With Berry ORM, we can define the schema of the entities just like at the backend: 112 | 113 | ```ts 114 | @Entity() 115 | class User extends BaseEntity { 116 | @Primary() 117 | @Field() 118 | id!: number; 119 | 120 | @Field() 121 | name!: string; 122 | 123 | @Relation({ target: () => Post, inverse: "owner", multi: true }) 124 | @Field() 125 | posts!: Collection; 126 | } 127 | 128 | @Entity() 129 | class Post extends BaseEntity { 130 | @Primary() 131 | @Field() 132 | id!: number; 133 | 134 | @Relation({ target: () => User, inverse: "posts" }) 135 | @Field() 136 | owner?: User; 137 | 138 | @Field() 139 | title!: string; 140 | } 141 | ``` 142 | 143 | You never have to worry about relational fields again: 144 | 145 | ```ts 146 | const user = orm.em.resolve(User, { 147 | id: 1, 148 | name: "Charles", 149 | posts: [ 150 | { id: 1, title: "Hello" }, 151 | { id: 2, title: "World" }, 152 | ], 153 | }); 154 | ``` 155 | 156 | Berry ORM will flatten the nested data, construct the missing relations and ensure that there is only one object for one entity. 157 | 158 | ```ts 159 | for (const post of user.posts) { 160 | console.log(post.owner == user); // true 161 | } 162 | ``` 163 | 164 | ```ts 165 | user.posts.clear(); 166 | for (const post of user.posts) { 167 | console.log(post.owner); // undefined 168 | } 169 | ``` 170 | 171 | ```ts 172 | user.name = "Berry"; 173 | for (const post of user.posts) { 174 | console.log(post.owner.name); // "Berry" 175 | } 176 | ``` 177 | 178 | If you have read until here, it's time to give it a try! 179 | 180 | # Documents 181 | 182 | https://thenightmarex.github.io/berry-orm/ 183 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Berry ORM 2 | 3 | [English](./README.md) 4 | 5 | 充分利用 TypeScript 类型系统的纯粹的对象关系映射器。 6 | 7 | ```sh 8 | npm i berry-orm 9 | ``` 10 | 11 | > **已测试的 TypeScript 编译器版本: TypeScript 4.1/4.2/4.3/4.4/4.5** 12 | 13 | # 为什么?这是做什么的? 14 | 15 | Berry ORM 是一个 **纯粹** 的 ORM,主要用于类型安全的 Web 前端开发。它只专注于将数据映射到类实例,不存在数据库或查询的概念。 16 | 17 | 等等,前端?为什么我会在前端需要一个 ORM? 18 | 19 | ## 我们解决的问题 20 | 21 | 假设我们在后端有两个实体: 22 | 23 | ```ts 24 | interface User { 25 | id: number; 26 | name: string; 27 | posts: Post[]; 28 | } 29 | 30 | interface Post { 31 | id: number; 32 | owner: User; 33 | title: string; 34 | } 35 | ``` 36 | 37 | 以及一个会返回一个 user 数据列表的 API `/users`: 38 | 39 | ```json 40 | [ 41 | { 42 | "id": 1, 43 | "name": "Charles", 44 | "posts": [ 45 | { "id": 1, "title": "Hello" }, 46 | { "id": 2, "title": "World" } 47 | ] 48 | } 49 | ] 50 | ``` 51 | 52 | 我们会如何处理这些数据呢? 53 | 54 | 由于 `Post` 的 `owner` 字段不存在,我们不能简单地复制后端的实体类型,那么或许我们可以调整类型,然后不加处理直接保存这些数据: 55 | 56 | ```ts 57 | interface User { 58 | id: number; 59 | name: string; 60 | posts: Post[]; 61 | } 62 | 63 | interface Post { 64 | id: number; 65 | title: string; 66 | // owner: User; 67 | } 68 | ``` 69 | 70 | 这样是有效的,但我们失去了从 post 反向访问关系的能力: 71 | 72 | ```ts 73 | user.posts[0].owner; 74 | ``` 75 | 76 | 更重要的是,如果我们在其他地方调用了一个 API,并且这个 API 返回了更新后的 post,那会怎么样? 77 | 78 | ```json 79 | { "id": 1, "title": "Hello!!!" } 80 | ``` 81 | 82 | 没错,将会同时存在两个对象保存有同一个实体在不同时刻的不同数据。数据不一致的情况就可能出现了。 83 | 84 | 现在来考虑一下另一个会返回一个 post 数据列表的 API: 85 | 86 | ```json 87 | [ 88 | { 89 | "id": 1, 90 | "owner": { "id": 1, "name": "Charles" }, 91 | "title": "Hello" 92 | }, 93 | { 94 | "id": 2, 95 | "owner": { "id": 1, "name": "Charles" }, 96 | "title": "World" 97 | } 98 | ] 99 | ``` 100 | 101 | 现在 `Post` 的 `owner` 字段回来了,但 `User` 的 `posts` 字段却又不见了。 102 | 103 | 我们应该如何定义接口类型呢? 104 | 105 | 我们可以定义更多的接口,进行重复操作,或者到处用 `Pick` 和 `Omit` 这类工具类型,把事情搞复杂。 106 | 107 | ## 我们解决方案 108 | 109 | Berry ORM 在此! 110 | 111 | 使用 Berry ORM,我们可以像在后端做的那样在前端定义实体结构: 112 | 113 | ```ts 114 | @Entity() 115 | class User extends BaseEntity { 116 | @Primary() 117 | @Field() 118 | id!: number; 119 | 120 | @Field() 121 | name!: string; 122 | 123 | @Relation({ target: () => Post, inverse: "owner", multi: true }) 124 | @Field() 125 | posts!: Collection; 126 | } 127 | 128 | @Entity() 129 | class Post extends BaseEntity { 130 | @Primary() 131 | @Field() 132 | id!: number; 133 | 134 | @Relation({ target: () => User, inverse: "posts" }) 135 | @Field() 136 | owner?: User; 137 | 138 | @Field() 139 | title!: string; 140 | } 141 | ``` 142 | 143 | 你再也不需要担心关系字段的处理了: 144 | 145 | ```ts 146 | const user = orm.em.resolve(User, { 147 | id: 1, 148 | name: "Charles", 149 | posts: [ 150 | { id: 1, title: "Hello" }, 151 | { id: 2, title: "World" }, 152 | ], 153 | }); 154 | ``` 155 | 156 | Berry ORM 会将这样嵌套的数据展平,构建缺失的对象关系,并确保对于一个实体只存在一个对象。 157 | 158 | ```ts 159 | for (const post of user.posts) { 160 | console.log(post.owner == user); // true 161 | } 162 | ``` 163 | 164 | ```ts 165 | user.posts.clear(); 166 | for (const post of user.posts) { 167 | console.log(post.owner); // undefined 168 | } 169 | ``` 170 | 171 | ```ts 172 | user.name = "Berry"; 173 | for (const post of user.posts) { 174 | console.log(post.owner.name); // "Berry" 175 | } 176 | ``` 177 | 178 | 既然你已经读到这儿了,那么现在是时候试一试了! 179 | 180 | # Documents 181 | 182 | https://thenightmarex.github.io/berry-orm/ 183 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: "/berry-orm/", 3 | title: "Berry ORM", 4 | description: "Object Relational Mapping for front-ends", 5 | 6 | themeConfig: { 7 | search: true, 8 | repo: "https://github.com/TheNightmareX/berry-orm", 9 | docsBranch: "develop", 10 | repoLabel: "GitHub", 11 | docsDir: "docs", 12 | editLinks: true, 13 | locales: { 14 | "/en/": { 15 | selectText: "Languages", 16 | label: "English", 17 | lastUpdated: "Last Updated", 18 | sidebar: [ 19 | { 20 | title: "Guide", 21 | collapsable: false, 22 | children: [ 23 | "/en/guide/introduction", 24 | "/en/guide/defining-entities", 25 | "/en/guide/preparing-the-orm", 26 | "/en/guide/resolving-data", 27 | "/en/guide/accessing-fields", 28 | "/en/guide/exporting-entities", 29 | ], 30 | }, 31 | { 32 | title: "Advanced", 33 | collapsable: false, 34 | children: [ 35 | "/en/advanced/serializers", 36 | "/en/advanced/identity-map", 37 | "/en/advanced/entity-events", 38 | ], 39 | }, 40 | ], 41 | }, 42 | "/zh/": { 43 | selectText: "语言", 44 | label: "简体中文", 45 | lastUpdated: "最后更新", 46 | sidebar: [ 47 | { 48 | title: "指南", 49 | collapsable: false, 50 | children: [ 51 | "/zh/guide/introduction", 52 | "/zh/guide/defining-entities", 53 | "/zh/guide/preparing-the-orm", 54 | "/zh/guide/resolving-data", 55 | "/zh/guide/accessing-fields", 56 | "/zh/guide/exporting-entities", 57 | ], 58 | }, 59 | { 60 | title: "进阶", 61 | collapsable: false, 62 | children: [ 63 | "/zh/advanced/serializers", 64 | "/zh/advanced/identity-map", 65 | "/zh/advanced/entity-events", 66 | ], 67 | }, 68 | ], 69 | }, 70 | }, 71 | }, 72 | 73 | locales: { 74 | "/en/": { 75 | lang: "en-US", 76 | }, 77 | "/zh/": { 78 | lang: "zh-CN", 79 | }, 80 | }, 81 | 82 | plugins: [ 83 | [ 84 | "redirect", 85 | { 86 | locales: true, 87 | }, 88 | ], 89 | ], 90 | }; 91 | -------------------------------------------------------------------------------- /docs/en/advanced/entity-events.md: -------------------------------------------------------------------------------- 1 | # Entity Events 2 | 3 | We can listen for entity events through the `orm.eem`. (`eem` means `EntityEventManager`). 4 | 5 | All event listeners will be removed after invoking `orm.reset()`. 6 | 7 | | Event | Timing | 8 | | ----------- | ----------------------------------------------------------- | 9 | | `"resolve"` | Called after resolved using `orm.resolve()` | 10 | | `"update"` | Called after resolved and any field is assigned a new value | 11 | 12 | ## Listening for Events 13 | 14 | We can use `orm.eem.on()` or `orm.eem.once()` to listen for events. The entity which triggered the event will be passed to event listeners. 15 | 16 | The event target can be: 17 | 18 | - A specific entity 19 | - Entities of a specific entity class 20 | - Any entities 21 | 22 | ```ts 23 | orm.eem.on(book, "update", (book) => console.debug(book)); 24 | orm.eem.on(Book, "update", (book) => console.debug(book)); 25 | orm.eem.on("any", "update", (book) => console.debug(book)); 26 | ``` 27 | 28 | ## Removing Event Listeners 29 | 30 | We can remove: 31 | 32 | - A specific event listener of a specific event of a specific event target. 33 | - All event listeners of a specific event of a specific event target. 34 | - All event listeners of a specific event target. 35 | - All event listeners. 36 | 37 | ```ts 38 | orm.eem.off(book, "update", callback); 39 | orm.eem.off(Book, "update", callback); 40 | orm.eem.off("any", "update", callback); 41 | 42 | orm.eem.off(book, "update"); 43 | orm.eem.off(Book, "update"); 44 | orm.eem.off("any", "update"); 45 | 46 | orm.eem.off(book); 47 | orm.eem.off(Book); 48 | orm.eem.off("any"); 49 | 50 | orm.eem.off(); 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/en/advanced/identity-map.md: -------------------------------------------------------------------------------- 1 | # Identity Map 2 | 3 | There is an `IdentityMap` instance belonging to each `BerryOrm` instance, which is accessible through `orm.map`. 4 | 5 | The `IdentityMap` instance stores every accessed entities. When an entity is needed, Berry ORM calls `orm.map.get()` internally to get or instantiate the entity. Therefore, `Book` entities with a same primary key is always the same object and can be compared directly. 6 | 7 | ```ts 8 | book1.id; // 1 9 | book2.id; // 2 10 | book1 == book2; // true 11 | ``` 12 | 13 | ## Clearing the Identity Map 14 | 15 | Sometimes we don't want previous entities to be accessible, for example, when a user logs out of a web application. 16 | 17 | We can clear the `IdentityMap` instance and make previous entities inaccessible through `orm.reset()`. After invoking `orm.reset()`, a `FieldAccessDeniedError` will be thrown if previous entities are accessed, because the previous entities should not be referenced and should be cleared by GC. 18 | 19 | ```ts 20 | const bookOld = orm.em.resolve(Book, { id: 1 }); 21 | orm.reset(); 22 | bookOld.id; // FieldAccessDeniedError 23 | ``` 24 | 25 | ::: tip 26 | 27 | The `IdentityMap` instance can be also cleared using `orm.map.clear()`, which is not recommended because the previous entities will still be accessible so there will be potential issues. 28 | 29 | ::: 30 | 31 | ## Scoped Identity Maps 32 | 33 | We may hope to have a clean `IdentityMap` instance in some scopes, for example a request scoped one. 34 | 35 | We can achieve that by invoking `orm.fork()` to create a new `BerryOrm` instance from the original one with a same entity registry and new child instances including a clean `IdentityMap` instance. 36 | 37 | ```ts 38 | const orm = new BerryOrm({ entities: [Book] }); 39 | const ormChild = orm.fork(); 40 | orm.registry == ormChild.registry; // true 41 | orm.map == ormChild.map; // false 42 | ``` 43 | 44 | We can access the parent of a `BerryOrm` instance through its `.parent` property. 45 | 46 | ```ts 47 | orm.parent; // undefined 48 | ormChild.parent; // orm 49 | ``` 50 | 51 | ## Version of ORMs and Entities 52 | 53 | Every `BerryOrm` instance have an auto-incremental number as its version, which is incremented during instantiation or invoking `orm.reset()` or `orm.fork()`。 54 | 55 | ```ts 56 | const orm1 = new BerryOrm({ entities: [] }); 57 | const orm2 = new BerryOrm({ entities: [] }); 58 | orm1.version; // 1 59 | orm2.version; // 2 60 | 61 | orm2.reset(); 62 | orm2.version; // 3 63 | 64 | const orm3 = orm2.fork(); 65 | orm3.version; // 4 66 | ``` 67 | 68 | When an entity is instantiated, its ORM instance's current version will be stored to its `[VERSION]` property, which can be used to identity entities from different `IdentityMap` instances. 69 | 70 | ```ts 71 | const orm = new BerryOrm({ entities: [Book] }); 72 | const book1 = orm.resolve(Book, { id: 1 }); 73 | orm.reset(); 74 | const book2 = orm.resolve(Book, { id: 1 }); 75 | 76 | book1 == book2; // false because they are from different `IdentityMap`s 77 | book1[VERSION]; // 1 78 | book2[VERSION]; // 2 79 | ``` 80 | 81 | ::: tip 82 | 83 | `VERSION` is a `symbol`. 84 | 85 | ::: 86 | -------------------------------------------------------------------------------- /docs/en/advanced/serializers.md: -------------------------------------------------------------------------------- 1 | # Serializers 2 | 3 | Serializers allow **non-relational** fields' representations more diverse. 4 | 5 | ## Using Serializers 6 | 7 | Berry ORM has a built-in `DateSerializer` allowing fields with a `Date` type to be represented using a `string` instead of a `Date` in plain data objects. 8 | 9 | ### Resolving Data with Serializers 10 | 11 | By using `DateSerializer`, the representation of `joinedAt` in the plain data object can be `Date | string` instead of `Date`. 12 | 13 | ```ts {5,7} 14 | const user = orm.em.resolve( 15 | User, 16 | { 17 | id: 1, 18 | joinedAt: new Date().toISOString(), 19 | }, 20 | { joinedAt: DateSerializer }, 21 | ); 22 | 23 | user.joinedAt instanceof Date; // true 24 | ``` 25 | 26 | ::: tip 27 | 28 | The type of the `data` parameter of `orm.em.resolve()` is dynamic based on the `serializers` parameter you specified. 29 | 30 | ::: 31 | 32 | ### Exporting Entities with Serializers 33 | 34 | By using `DateSerializer`, the representation of `user.joinedAt` in the exported object will be a `string` instead of a `Date`. 35 | 36 | ```ts {1} 37 | const data = orm.em.export(user, {}, { joinedAt: DateSerializer }); 38 | 39 | typeof data.joinedAt; // "string" 40 | ``` 41 | 42 | ::: tip 43 | 44 | The return type of `orm.em.export()` is dynamic based on the `serializers` parameter you specified. 45 | 46 | ::: 47 | 48 | ## Creating Serializers 49 | 50 | Serializers are classes extending `AbstractSerializer` with its abstract methods implemented. 51 | 52 | ```ts 53 | export class DateSerializer extends AbstractSerializer { 54 | serialize(value: Date): string { 55 | return value.toISOString(); 56 | } 57 | deserialize(value: string | Date): Date { 58 | return new Date(value); 59 | } 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/en/guide/accessing-fields.md: -------------------------------------------------------------------------------- 1 | # Accessing Fields 2 | 3 | Accessors will be applied to entity fields during entity instantiations to change the behavior when accessing entity fields. 4 | 5 | ## Primary Fields 6 | 7 | Primary fields are read-only. An `FieldAccessDeniedError` will be thrown when trying to set its value. 8 | 9 | ## Common Fields 10 | 11 | Common Fields are read-write, just like normal properties. 12 | 13 | Note that updates of fields will propagate in your whole application because of the `IdentityMap`. 14 | 15 | ```ts 16 | user1.id; // 1 17 | user2.id; // 1 18 | user1 == user2; // true because of the IdentityMap 19 | ``` 20 | 21 | ```ts {1} 22 | user1.name = "new"; 23 | user2.name; // "new" 24 | ``` 25 | 26 | ## Relation Fields 27 | 28 | Assignments to _OneToOne_ and _ManyToOne_ fields will update entity relations. 29 | 30 | ```ts {1} 31 | user.profile = newProfile; // construct/update the relation 32 | oldProfile.owner; // undefined 33 | newProfile.owner; // user 34 | ``` 35 | 36 | ```ts {1} 37 | user.profile = undefined; // destruct the relation 38 | ``` 39 | 40 | _OneToMany_ and _ManyToMany_ fields (`Collection` fields) cannot be assigned new values directly (read-only like primary fields), but you can manage entity relations by invoking its methods. 41 | 42 | ```ts {1} 43 | department.members.add(user); // construct relations 44 | department.members.has(user) == true; 45 | user.department == department; 46 | ``` 47 | 48 | ```ts {1} 49 | department.members.delete(user); // destruct relations 50 | department.members.has(user) == false; 51 | user.department === undefined; 52 | ``` 53 | 54 | ```ts {1} 55 | department.members.clear(); // destruct all relations 56 | department.members.size == 0; 57 | user.department === undefined; 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/en/guide/defining-entities.md: -------------------------------------------------------------------------------- 1 | # Defining Entities 2 | 3 | Entities in Berry ORM are instances of decorated classes extending `BaseEntity`. There is no need to worry about property name pollutions because there are only a few `Symbol` properties defined in the `BaseEntity`. 4 | 5 | ## Preparing the Class 6 | 7 | Entity class should extend `BaseEntity` and be applied the `@Entity()` decorator. 8 | Some type parameters are required when extending `BaseEntity`. One is the entity class itself which is being defined, while the other is the primary field of that entity class. 9 | 10 | ```ts 11 | @Entity() 12 | export class User extends BaseEntity { 13 | // ... 14 | } 15 | ``` 16 | 17 | Don't worry about the type error. It is an intentional behavior because the primary field specified in the type parameters has not been defined yet. 18 | 19 | ::: tip Recommended Practices 20 | 21 | - Write a `@Entity()` at the very beginning to have a type error when the entity class is defined incorrectly. 22 | - Define entities in separate files to avoid potential issues caused by circular references. (e.g. `src/entities/user.entity.ts`) 23 | 24 | ::: 25 | 26 | ::: warning 27 | 28 | There will be an type error thrown by `@Entity()` if the constructor is overwritten with incompatible parameters. 29 | 30 | ::: 31 | 32 | ## Defining Fields 33 | 34 | ### Common Fields 35 | 36 | For most fields storing data, there is only a `@Field()` required for them to be declared. 37 | 38 | ```ts {1} 39 | @Field() 40 | username!: string; 41 | ``` 42 | 43 | ```ts {1} 44 | @Field() 45 | joinedAt!: Date; 46 | ``` 47 | 48 | ::: tip Non-Nullable Operator 49 | 50 | We are using the non-nullable operator `!` here because TypeScript compiler in strict mode require class properties to have a default or to be assigned a value in the constructor, unless there is a non-nullable operator suffixed. 51 | 52 | ::: 53 | 54 | ::: warning 55 | 56 | - There will be an `EntityMetaError` when `@Field()` is applied for multiple times on one field. 57 | 58 | ::: 59 | 60 | ### The Primary Field 61 | 62 | There **must be one and only one** primary field defined in an entity class. To define the primary field, simply apply a `@Primary()` **after(above)** `@Field()`. 63 | 64 | ```ts {1} 65 | @Primary() 66 | @Field() 67 | id!: number; 68 | ``` 69 | 70 | ```ts {1} 71 | @Primary() 72 | @Field() 73 | uuid!: string; 74 | ``` 75 | 76 | ::: warning 77 | 78 | - There will be an `EntityMetaError` when `@Primary()` is applied before(below) `@Field()` or is applied for multiple times in one entity class. 79 | - There will be a type error unless `@Primary()` is applied on a field with type `string` or `number`. Fields with type `string | number` are not valid choices either. 80 | 81 | ::: 82 | 83 | ### Relation Fields 84 | 85 | Similarly, for relations, apply a `@Relation()` **after(above)** `@Field()` 86 | 87 | ```ts {2,4} 88 | // class Book 89 | @Relation({ 90 | target: () => Author, 91 | inverse: "books", 92 | multi: false, // optional 93 | }) 94 | @Field() 95 | author?: Author; 96 | ``` 97 | 98 | In the code above, we defined a relation field in the `Book` class with `multi` set to `false`. Such relation fields can represent _OneToOne_ or _ManyToOne_ relations. 99 | 100 | ::: tip Recommended Practices 101 | 102 | Use the optional operator `?` instead of the non-nullable operator `!`. Because values of such relation fields may be `undefined` if there is no plain data object specifying its relation, and what's more, we can assign an `undefined` to the field to destruct its relation, which will be talked about in [Accessing Fields](./accessing-fields.html#relation-fields) 103 | 104 | ::: 105 | 106 | The value of `inverse` should be a relation field of the target entity class which points back to this entity class. And of course, it is strictly typed, with value limited and auto-completion enabled. 107 | 108 | ```ts {2,4} 109 | // class Author 110 | @Relation({ 111 | target: () => Book, 112 | inverse: "author", 113 | multi: true, 114 | }) 115 | @Field() 116 | books!: Collection; 117 | ``` 118 | 119 | And here we defined another relation field in the `Author` class but with `multi` set to `true`, which can represent _OneToMany_ or _ManyToMany_ relations. 120 | 121 | _ToMany_ relation fields should have a `Collection` type and will be automatically assigned a `Collection` instance during the instantiation of entities. 122 | 123 | ::: tip 124 | 125 | `Collection` is derived from `Set`. 126 | 127 | ::: 128 | 129 | ::: warning 130 | 131 | - There will be an `EntityMetaError` when `@Relation()` is applied before(below) `@Field()` or is applied for multiple times on one field. 132 | - There will be a type error unless `@Relation()` is applied to a field with type `Collection<...>` matching the `target` option specified. 133 | 134 | ::: 135 | 136 | ## Accessors and Methods 137 | 138 | You can define accessors and methods on entity classes. 139 | 140 | ```ts 141 | get fullName() { 142 | return `${this.firstName} ${this.lastName}` 143 | } 144 | ``` 145 | 146 | ```ts 147 | getSuperiorMembers(user: User) { 148 | return [...this.department.members].filter( 149 | (member) => member.level > user.level, 150 | ); 151 | } 152 | ``` 153 | -------------------------------------------------------------------------------- /docs/en/guide/exporting-entities.md: -------------------------------------------------------------------------------- 1 | # Exporting Entities 2 | 3 | Entities can also be converted back to plain data object through `orm.em.export()`. 4 | 5 | ## Basic 6 | 7 | The primary key and common fields' values in the exported plain data object will be directly copied from the entity, and relations will be represented using primary keys by default. 8 | 9 | ```ts 10 | const book = orm.em.resolve(Book, { 11 | id: 1, 12 | name: "Book", 13 | author: { id: 1, name: "Char2s" }, 14 | }); 15 | 16 | const data = orm.em.export(book); 17 | data.id; // 'id' 18 | data.name; // "Book" 19 | data.author; // 1 20 | ``` 21 | 22 | ## Expansions 23 | 24 | Relations are represented using primary keys in exported objects by default, while we can specify which relations to be expanded into nested objects through the second parameter `expansions`. 25 | 26 | ```ts 27 | const data = orm.em.export(user, { 28 | profile: true, 29 | friends: { profile: true }, 30 | }); 31 | ``` 32 | 33 | ```ts 34 | typeof data.profile; // "object" 35 | data.friends.forEach((friend) => { 36 | typeof friend; // "object" 37 | typeof friend.profile; // "object" 38 | }); 39 | ``` 40 | 41 | ::: tip 42 | 43 | `orm.em.export()` has been strictly typed. 44 | 45 | - The return type is dynamically generated. 46 | - There will be a type error if invalid value is passed to `expansions`. But auto-completion is not supported because of TypeScript's limitations. 47 | 48 | ![](../../../res/exporting-entities.gif) 49 | 50 | ::: 51 | 52 | ::: warning 53 | 54 | Relations specified in `expansions` cannot have [skeleton entities](./resolving-data.html#skeleton-entities), otherwise an error will be thrown. 55 | 56 | ::: 57 | -------------------------------------------------------------------------------- /docs/en/guide/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Berry ORM is a lightweight object relation mapper with **_❗SUPER AWESOME TYPINGS❗_**. 4 | 5 | ```sh 6 | npm i berry-orm 7 | ``` 8 | 9 | There are much efforts spent on types, which is the key feature of this project, to show how strict the types can be in an ORM and how convenient it can be to work with these super awesome types! 10 | 11 | > **Requires: TypeScript 4.1/4.2/4.3/4.4/4.5** 12 | 13 | ## Possible Scenarios 14 | 15 | - Manage relational state in a web application 16 | - Keep large amounts of relational data in IndexedDB 17 | - Store relational data in simple files like "data.json" 18 | 19 | ## Examples 20 | 21 | ### Defining Entities 22 | 23 |
24 | 25 | ```ts 26 | @Entity() 27 | class Book extends BaseEntity { 28 | @Primary() 29 | @Field() 30 | id!: number; 31 | 32 | @Field() 33 | name!: string; 34 | 35 | @Relation({ 36 | target: () => Author, 37 | inverse: "books", 38 | }) 39 | @Field() 40 | author!: Author; 41 | } 42 | 43 | @Entity() 44 | class Author extends BaseEntity { 45 | @Primary() 46 | @Field() 47 | id!: number; 48 | 49 | @Field() 50 | name!: string; 51 | 52 | @Relation({ 53 | target: () => Book, 54 | inverse: "author", 55 | multi: true, 56 | }) 57 | @Field() 58 | books!: Collection; 59 | } 60 | ``` 61 | 62 |
63 | 64 | ![](../../../res/defining-entities.gif) 65 | 66 | ### Resolving Data 67 | 68 | ```ts 69 | const orm = new BerryOrm({ entities: [Book, Author] }); 70 | 71 | const book1 = orm.em.resolve(Book, { 72 | id: 1, 73 | name: "1000 Ways to Code", 74 | author: 1, 75 | }); 76 | 77 | book1[RESOLVED]; // true 78 | book1.author[RESOLVED]; // false 79 | 80 | const book2 = orm.em.resolve(Book, { 81 | id: 2, 82 | name: "2000 Ways to Code", 83 | author: { id: 1, name: "Char2s" }, 84 | }); 85 | 86 | book2[RESOLVED]; // true 87 | book2.author[RESOLVED]; // true 88 | 89 | book1.author == book2.author; // true 90 | ``` 91 | 92 | ### Exporting Entities 93 | 94 |
95 | 96 | ```ts 97 | const orm = new BerryOrm({ entities: [Book, Author] }); 98 | 99 | const book = orm.em.resolve(Book, { 100 | id: 1, 101 | name: "1000 Ways to Code", 102 | author: { id: 1, name: "Char2s" }, 103 | }); 104 | 105 | const data = orm.em.export(book, { author: { books: { author: true } } }); 106 | data.author.books[0].author. 107 | ``` 108 | 109 |
110 | 111 | ![](../../../res/exporting-entities.gif) 112 | -------------------------------------------------------------------------------- /docs/en/guide/preparing-the-orm.md: -------------------------------------------------------------------------------- 1 | # Preparing the ORM 2 | 3 | Having defined our entities, it is now time to put them to use. 4 | 5 | ```ts 6 | export const orm = new BerryOrm({ 7 | entities: [User, Department, Profile], 8 | }); 9 | ``` 10 | 11 | All the entities we want to use in this `BerryOrm` instance should be passed to the `entities` option. 12 | 13 | Entity inspections will be performed during the instantiation of `BerryOrm` to ensure they can work properly. 14 | 15 | - `@Entity()` must be applied to the entity classes. 16 | - If one entity class is registered, all its relational entity classes must be also registered. 17 | - Cannot register entities having duplicate class names. 18 | -------------------------------------------------------------------------------- /docs/en/guide/resolving-data.md: -------------------------------------------------------------------------------- 1 | # Resolving Data 2 | 3 | `orm.em.resolve()` converts a _plain data object_ into an _entity_. (`em` means `EntityManager`) 4 | 5 | Of course, `orm.em.resolve()` is also strictly typed to restrict the type of the plain data object passed. 6 | 7 | ## Basic Resolving 8 | 9 | The primary key and all the common fields' values must be specified in the plain data object, while relations can be optional. 10 | 11 | For the primary field and common fields, their values in entities are simply copied from the plain data object. 12 | 13 | ```ts 14 | const book = orm.em.resolve(Book, { 15 | id: 1, 16 | name: "Book", 17 | }); 18 | ``` 19 | 20 | ```ts 21 | book instanceof Book; // true 22 | book.id == 1; // true 23 | book.name == "Book"; // true 24 | ``` 25 | 26 | ## Duplicated Resolving 27 | 28 | There is an `IdentityMap` for each `BerryOrm` instance storing every accessed entities. Therefore, when another plain data object is resolved with a existing primary key, the existing entity will be updated and returned. 29 | 30 | ```ts 31 | const bookNew = orm.em.resolve(Book, { 32 | id: 1, 33 | name: "New Name", 34 | }); 35 | ``` 36 | 37 | ```ts 38 | bookOld == bookNew; // true 39 | bookOld.name == "New Name"; // true 40 | ``` 41 | 42 | ## Relational Resolving 43 | 44 | For relation fields , their values can be represented using both primary keys or nested plain data objects. 45 | 46 | ### Constructing Relations 47 | 48 | ```ts {4} 49 | const book = orm.em.resolve(Book, { 50 | id: 1, 51 | name: "Book", 52 | author: { id: 1, name: "Char2s" }, 53 | }); 54 | 55 | const author = orm.em.resolve(Author, { 56 | id: 1, 57 | name: "Char2s", 58 | }); 59 | ``` 60 | 61 | Bilateral relations will be constructed when relational data are resolved, which means that although `author.books` was not specified in its plain data object, the `book` was added to `author.books` automatically, and vice versa. 62 | 63 | ```ts 64 | book.author == author; // true 65 | author.books.has(book); // true 66 | ``` 67 | 68 | We can also use primary keys to represent relations, but note that the target entities will be [skeleton entities](#skeleton-entities) if they have not been resolved before, about which we will talk later. 69 | 70 | ### Updating Relations 71 | 72 | In a [duplicated resolving](#duplicated-resolving), old relations will be destructed and then new relations will be constructed. 73 | 74 | ### Destructing Relations 75 | 76 | Relations will be destructed when an `undefined` is specified as the value of relation fields representing _OneToOne_ or _ManyToOne_ relations, or an array with some relations removed as the value of relation fields representing _OneToMany_ or _ManyToMany_ relations. 77 | 78 | ```ts 79 | orm.em.resolve(Book, { 80 | id: 1, 81 | name: "Book", 82 | author: undefined, 83 | }); 84 | 85 | // or 86 | 87 | orm.em.resolve(Author, { 88 | id: 1, 89 | name: "Char2s", 90 | books: [], 91 | }); 92 | ``` 93 | 94 | ::: tip 95 | 96 | This approach is not being used to destruct relations intentionally. See [Accessing Fields](./accessing-fields.html#relation-fields) for more information. 97 | 98 | ::: 99 | 100 | ## Skeleton Entities 101 | 102 | We can use primary keys to represent the value of relation fields in plain data objects. 103 | 104 | ```ts {4} 105 | const book = orm.em.resolve(Book, { 106 | id: 1, 107 | name: "Book", 108 | author: 1, 109 | }); 110 | ``` 111 | 112 | `book.author` here is a _skeleton entity_ because `Author` with `id: 1` is never resolved before, which means that we can only ensure that its primary key is known and most of its common fields and relation fields will probably be `undefined`. 113 | 114 | ```ts 115 | book.author.id == 1; // true 116 | book.author.books.has(book); // true 117 | book.author.name === undefined; // true 118 | ``` 119 | 120 | We can recognize skeleton entities through the `[RESOLVED]` property. 121 | 122 | ```ts 123 | book[RESOLVED]; // true 124 | book.author[RESOLVED]; // false 125 | ``` 126 | 127 | ::: tip 128 | 129 | `RESOLVED` is a `symbol`. 130 | 131 | ::: 132 | -------------------------------------------------------------------------------- /docs/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: Berry ORM 4 | tagline: A Lightweight ORM with SUPER AWESOME TYPINGS 5 | actionText: Getting Started → 6 | actionLink: ./guide/introduction 7 | features: 8 | - title: Strictly Typed 9 | details: Enjoy the pleasing development experience brought by the extremely strict types, and maximize the advantages of TypeScript. 10 | - title: Lightweight and Generic 11 | details: Only object relational mapping without anything redundant, which can be used in any scenario. 12 | - title: Easy to Use 13 | details: Fully control over object relations with simple assignments and calls. 14 | footer: Apache-2.0 Licensed | Copyright © 2021-present Charles Gu 15 | --- 16 | -------------------------------------------------------------------------------- /docs/zh/advanced/entity-events.md: -------------------------------------------------------------------------------- 1 | # 实体事件 2 | 3 | 我们可以通过 `orm.eem` 监听实体事件。(`eem` 意为 `EntityEventManager`) 4 | 5 | 所有的事件监听器都将在调用 `orm.reset()` 后移除。 6 | 7 | | 事件 | 时机 | 8 | | ----------- | --------------------------------- | 9 | | `"resolve"` | 在使用 `orm.resolve()` 解析后调用 | 10 | | `"update"` | 在已解析且任何字段被赋值后调用 | 11 | 12 | ## 监听事件 13 | 14 | 我们可以使用 `orm.eem.on()` 或者 `orm.eem.once()` 来监听事件。触发事件的实体将被传递给事件监听器。 15 | 16 | 事件目标可以是: 17 | 18 | - 一个特定的实体 19 | - 一个特定实体类的实体 20 | - 任何实体 21 | 22 | ```ts 23 | orm.eem.on(book, "update", (book) => console.debug(book)); 24 | orm.eem.on(Book, "update", (book) => console.debug(book)); 25 | orm.eem.on("any", "update", (book) => console.debug(book)); 26 | ``` 27 | 28 | ## 移除事件监听器 29 | 30 | 我们可以移除: 31 | 32 | - 一个特定事件目标的一个特定事件的一个特定事件监听器 33 | - 一个特定事件目标的一个特定事件的所有事件监听器 34 | - 一个特定事件目标的所有事件监听器 35 | - 所有事件监听器 36 | 37 | ```ts 38 | orm.eem.off(book, "update", callback); 39 | orm.eem.off(Book, "update", callback); 40 | orm.eem.off("any", "update", callback); 41 | 42 | orm.eem.off(book, "update"); 43 | orm.eem.off(Book, "update"); 44 | orm.eem.off("any", "update"); 45 | 46 | orm.eem.off(book); 47 | orm.eem.off(Book); 48 | orm.eem.off("any"); 49 | 50 | orm.eem.off(); 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/zh/advanced/identity-map.md: -------------------------------------------------------------------------------- 1 | # Identity Map 2 | 3 | 在每一个 `BerryOrm` 的实例中都有一个 `IdentityMap` 实例,可以通过 `orm.map` 来访问。 4 | 5 | 该 `IdentityMap` 实例保存每一个访问过的实体。当需要某一实体时,Berry ORM 在内部调用 `orm.map.get()` 来获取或实例化该实体。因此,具有相同主键的 `Book` 实体总是同一个对象,可以直接用来比较。 6 | 7 | ```ts 8 | book1.id; // 1 9 | book2.id; // 2 10 | book1 == book2; // true 11 | ``` 12 | 13 | ## 清除 Identity Map 14 | 15 | 有时,我们不想要先前的实体被访问到。例如,当一个用户登出了一个 Web 应用。 16 | 17 | 我们可以通过 `orm.reset()` 来清除 `IdentityMap` 实例并使先前的实体不可访问。在调用 `orm.reset()` 后,如果访问先前的实体,将会抛出一个 `FieldAccessDeniedError`,因为先前的实体不应该再被引用且应该被 GC 清理掉。 18 | 19 | ```ts 20 | const bookOld = orm.em.resolve(Book, { id: 1 }); 21 | orm.reset(); 22 | bookOld.id; // FieldAccessDeniedError 23 | ``` 24 | 25 | ::: tip 26 | 27 | `IdentityMap` 实例也可以通过 `orm.map.clear()` 来清除。这是不被推荐的,因为先前的实体将仍然可以访问,因此可能存在潜在的问题。 28 | 29 | ::: 30 | 31 | ## 范围 Identity Map 32 | 33 | 我们可能希望在某些范围内拥有一个干净的 `IdentityMap` 实例,例如请求范围的 `IdentityMap` 实例。 34 | 35 | 我们可以通过调用 `orm.fork()` 来从原先的 `BerryOrm` 实例创建一个新的 `BerryOrm` 实例,它具有相同的实体注册和新的子实例,包括一个干净的 `IdentityMap` 实例。 36 | 37 | ```ts 38 | const orm = new BerryOrm({ entities: [Book] }); 39 | const ormChild = orm.fork(); 40 | orm.registry == ormChild.registry; // true 41 | orm.map == ormChild.map; // false 42 | ``` 43 | 44 | 我们可以通过其 `.parent` 属性来访问一个 `BerryOrm` 实例的父实例。 45 | 46 | ```ts 47 | orm.parent; // undefined 48 | ormChild.parent; // orm 49 | ``` 50 | 51 | ## ORM 与实体的版本 52 | 53 | 每个 `BerryOrm` 实例都拥有一个自动递增的数字作为它的版本,该数字在实例化或调用 `orm.reset()` 或 调用 `orm.fork()` 时递增。 54 | 55 | ```ts 56 | const orm1 = new BerryOrm({ entities: [] }); 57 | const orm2 = new BerryOrm({ entities: [] }); 58 | orm1.version; // 1 59 | orm2.version; // 2 60 | 61 | orm2.reset(); 62 | orm2.version; // 3 63 | 64 | const orm3 = orm2.fork(); 65 | orm3.version; // 4 66 | ``` 67 | 68 | 当一个实体被实例化时,其 ORM 的当前版本将被保存到其 `[VERSION]` 属性,该版本可以用来辨别来自不同 `IdentityMap` 实例的实体。 69 | 70 | ```ts 71 | const orm = new BerryOrm({ entities: [Book] }); 72 | const book1 = orm.resolve(Book, { id: 1 }); 73 | orm.reset(); 74 | const book2 = orm.resolve(Book, { id: 1 }); 75 | 76 | book1 == book2; // false,因为它们来自不同的 `IdentityMap` 实例 77 | book1[VERSION]; // 1 78 | book2[VERSION]; // 2 79 | ``` 80 | 81 | ::: tip 82 | 83 | `VERSION` 是一个 `symbol`. 84 | 85 | ::: 86 | -------------------------------------------------------------------------------- /docs/zh/advanced/serializers.md: -------------------------------------------------------------------------------- 1 | # 序列化器 2 | 3 | 序列化器允许**非关系**字段的表示变得更加多样。 4 | 5 | ## 使用序列化器 6 | 7 | Berry ORM 内置一个 `DateSerializer`,允许具有 `Date` 类型的字段在普通数据对象中被表示为 `string` 而不是 `Date`。 8 | 9 | ### 解析数据时使用序列化器 10 | 11 | 通过使用 `DateSerializer`,`joinedAt` 的在普通数据对象中的表示可以是 `Date | string` 而不是 `Date`。 12 | 13 | ```ts {5,7} 14 | const user = orm.em.resolve( 15 | User, 16 | { 17 | id: 1, 18 | joinedAt: new Date().toISOString(), 19 | }, 20 | { joinedAt: DateSerializer }, 21 | ); 22 | 23 | user.joinedAt instanceof Date; // true 24 | ``` 25 | 26 | ::: tip 27 | 28 | `orm.em.resolve()` 的 `data` 参数的类型是动态的,基于你所指定的 `serializers` 参数。 29 | 30 | ::: 31 | 32 | ### 导出实体时使用序列化器 33 | 34 | 通过使用 `DateSerializer`,`user.joinedAt` 在普通数据对象中的表示方式将会是 `string` 而不是 `Date`。 35 | 36 | ```ts {1} 37 | const data = orm.em.export(user, {}, { joinedAt: DateSerializer }); 38 | 39 | typeof data.joinedAt; // "string" 40 | ``` 41 | 42 | ::: tip 43 | 44 | `orm.em.export()` 的返回值类型是动态的,基于你所指定的 `serializers` 参数。 45 | 46 | ::: 47 | 48 | ## 创建序列化器 49 | 50 | 序列化器是继承了 `AbstractSerializer` 并实现了其抽象方法的类。 51 | 52 | ```ts 53 | export class DateSerializer extends AbstractSerializer { 54 | serialize(value: Date): string { 55 | return value.toISOString(); 56 | } 57 | deserialize(value: string | Date): Date { 58 | return new Date(value); 59 | } 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/zh/guide/accessing-fields.md: -------------------------------------------------------------------------------- 1 | # 访问字段 2 | 3 | 在实体实例化过程中,访问器将被应用于实体字段,以改变访问实体字段时的行为。 4 | 5 | ## 主键字段 6 | 7 | 主键字段是只读的。当试图对其赋值时,将抛出一个 `FieldAccessDeniedError`。 8 | 9 | ## 普通字段 10 | 11 | 普通字段是可读可写的,和普通属性一样。 12 | 13 | 请注意,由于 `IdentityMap` 的存在,对字段的更新将在你的整个应用程序中传播。 14 | 15 | ```ts 16 | user1.id; // 1 17 | user2.id; // 1 18 | user1 == user2; // true,因为有 IdentityMap 19 | ``` 20 | 21 | ```ts {1} 22 | user1.name = "new"; 23 | user2.name; // "new" 24 | ``` 25 | 26 | ## 关系字段 27 | 28 | 对 _一对一_ 和 _多对一_ 字段的赋值将更新实体关系。 29 | 30 | ```ts {1}. 31 | user.profile = newProfile; // 构建/更新关系 32 | oldProfile.owner; // undefined 33 | newProfile.owner; // user 34 | ``` 35 | 36 | ```ts{1} 37 | user.profile = undefined; // 破坏关系 38 | ``` 39 | 40 | _一对多_ 和 _多对多_ 字段(`Collection` 字段)不能直接赋值(像主字段一样只读),但是你可以通过调用它的方法来管理实体关系。 41 | 42 | ```ts {1} 43 | department.members.add(user); // 构建关系 44 | department.members.has(user) == true; 45 | user.department == department; 46 | ``` 47 | 48 | ```ts {1} 49 | department.members.delete(user); // 破坏关系 50 | department.members.has(user) == false; 51 | user.department === undefined; 52 | ``` 53 | 54 | ```ts {1} 55 | department.members.clear(); // 破坏所有关系 56 | department.members.size == 0; 57 | user.department === undefined; 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/zh/guide/defining-entities.md: -------------------------------------------------------------------------------- 1 | # 定义实体 2 | 3 | 在 Berry ORM 中,实体是应用了装饰器并继承了 `BaseEntity` 的类的实例。 无需担心属性名称污染,因为 `BaseEntity` 上仅仅定义了极少数的 `symbol` 属性。 4 | 5 | ## 准备实体类 6 | 7 | 实体类需要继承 `BaseEntity` 并应用 `@Entity()` 装饰器。 8 | 继承 `BaseEntity` 时需要一些类型参数。一个是正在定义的类本身,另一个是该类的主键字段。 9 | 10 | ```ts 11 | @Entity() 12 | export class User extends BaseEntity { 13 | // ... 14 | } 15 | ``` 16 | 17 | 无需担心出现的类型错误。这是一个有意的行为,因为在类型参数中指定的主键字段目前还未被定义。 18 | 19 | ::: tip 建议的实践 20 | 21 | - 在最开始的时候就先写一个 `@Entity()`,以此来在错误定义实体类时获取一个类型错误。 22 | - 在分开的文件中定义实体,以此来避免因循环引用而导致的潜在问题。(例如 `src/entities/user.entity.ts`) 23 | 24 | ::: 25 | 26 | ::: warning 27 | 28 | 如果构造函数被重写,且参数类型不兼容,`@Entity()` 将会抛出一个类型错误。 29 | 30 | ::: 31 | 32 | ## 定义字段 33 | 34 | ### 普通字段 35 | 36 | 对于大多数存储数据的字段,声明它们只需要一个 `@Field()`。 37 | 38 | ```ts {1} 39 | @Field() 40 | username!: string; 41 | ``` 42 | 43 | ```ts {1} 44 | @Field() 45 | joinedAt!: Date; 46 | ``` 47 | 48 | ::: tip 非空操作符 49 | 50 | 我们在这里使用了非空操作符 `!`,因为在严格模式下,TypeScript 编译器要求类的属性必须具有一个默认值或在构造函数中被赋值,除非有一个非空操作符作为后缀。 51 | 52 | ::: 53 | 54 | ::: warning 55 | 56 | - 当 `@Field()` 在一个字段上应用多次时,将会抛出一个 `EntityMetaError`。 57 | 58 | ::: 59 | 60 | ### 主键字段 61 | 62 | 在一个实体类中,**必须有且仅有**一个主键字段被定义。要定义主键字段,只需在 `@Field()` **之后(之上)** 应用一个 `@Primary()`。 63 | 64 | ```ts {1} 65 | @Primary() 66 | @Field() 67 | id!: number; 68 | ``` 69 | 70 | ```ts {1} 71 | @Primary() 72 | @Field() 73 | uuid!: string; 74 | ``` 75 | 76 | ::: warning 77 | 78 | - 当 `@Primary()` 在 `@Field()` 之前(之下)应用,或者在一个实体类中多次应用时,将会抛出一个 `EntityMetaError`。 79 | - 除非 `@Primary()` 在一个具有 `string` 或 `number` 类型的字段上应用,否则将会出现一个类型错误。具有 `string | number` 类型的字段同样不是合法的选择。 80 | 81 | ::: 82 | 83 | ### 关系字段 84 | 85 | 相似地,对于关系,在 `@Field()` **之后(之上)** 应用一个 `@Relation()`。 86 | 87 | ```ts {2,4} 88 | // class Book 89 | @Relation({ 90 | target: () => Author, 91 | inverse: "books", 92 | multi: false, // 可选的 93 | }) 94 | @Field() 95 | author?: Author; 96 | ``` 97 | 98 | 在上面的代码中,我们在 `Book` 类中定义了一个将 `multi` 设置为 `false` 的关系字段。这样的关系字段可以表示 _一对一_ 或 _多对一_ 关系。 99 | 100 | ::: tip 建议的实践 101 | 102 | 使用可选操作符 `?` 代替非空操作符 `!`。因为当没有数据对象指定其关系时,这样的关系字段的值可能会是 `undefined`。此外,[访问字段](./accessing-fields.html#relation-fields) 中会说到,我们可以对该字段赋值一个 `undefined` 来拆除关系。 103 | 104 | ::: 105 | 106 | `inverse` 的值需要是目标实体类的一个关系字段,该字段反过来指向这个实体类。当然,它是具备了严格类型的,具有值的限制和自动补全。 107 | 108 | ```ts {2,4} 109 | // class Author 110 | @Relation({ 111 | target: () => Book, 112 | inverse: "author", 113 | multi: true, 114 | }) 115 | @Field() 116 | books!: Collection; 117 | ``` 118 | 119 | 这里我们在 `Author` 类中定义了另一个关系字段,但这次 `multi` 设置为了 `true`,这样的字段可以表示 _一对多_ 或 _多对多_ 关系。 120 | 121 | _对多_ 关系字段需要具有一个 `Collection` 类型,并且在实例化实体时,它们会自动被赋值一个 `Collection` 实例。 122 | 123 | ::: tip 124 | 125 | `Collection` 是从 `Set` 派生的。 126 | 127 | ::: 128 | 129 | ::: warning 130 | 131 | - 当 `@Relation()` 在 `@Field()` 之前(之下)应用,或在一个字段上多次应用时,将会抛出一个 `EntityMetaError`。 132 | - 除非 `@Relation()` 被应用于类型为 `Collection<...>` 的字段,且与指定的 `target` 选项相匹配,否则将会出现一个类型错误。 133 | 134 | ::: 135 | 136 | ## 访问器和方法 137 | 138 | 你可以在实体类上定义访问器和方法。 139 | 140 | ```ts 141 | get fullName() { 142 | return `${this.firstName} ${this.lastName}` 143 | } 144 | ``` 145 | 146 | ```ts 147 | getSuperiorMembers(user: User) { 148 | return [...this.department.members].filter( 149 | (member) => member.level > user.level, 150 | ); 151 | } 152 | ``` 153 | -------------------------------------------------------------------------------- /docs/zh/guide/exporting-entities.md: -------------------------------------------------------------------------------- 1 | # 导出实体 2 | 3 | 实体也可以通过 `orm.em.export()` 转换回普通数据对象。 4 | 5 | ## 基本 6 | 7 | 主键和公共字段在导出的普通字段中的值将直接从实体中复制,关系将默认使用主键来表示。 8 | 9 | ```ts 10 | const book = orm.em.resolve(Book, { 11 | id: 1, 12 | name: "Book", 13 | 作者。{ id: 1, name: "Char2s" }, 14 | }); 15 | 16 | const data = orm.em.export(book); 17 | data.id; // "id 18 | data.name; // "Book" 19 | data.author; // 1 20 | ``` 21 | 22 | ## 扩展 23 | 24 | 关系在导出的对象中默认使用主键表示,但我们可以通过第二个参数 `expansions` 来指定哪些关系要被扩展为嵌套的普通数据对象。 25 | 26 | ```ts 27 | const data = orm.em.export(user, { 28 | profile: true, 29 | friends: { profile: true }, 30 | }); 31 | ``` 32 | 33 | ```ts 34 | typeof data.profile; // "object" 35 | data.friends.forEach((friend) => { 36 | typeof friend; // "object" 37 | typeof friend.profile; // "object" 38 | }); 39 | ``` 40 | 41 | ::: tip 42 | 43 | `orm.em.export()` 具备严格的类型。 44 | 45 | - 返回值类型是动态生成的。 46 | - 如果不合法的值被传递给 `expansions`,将会出现类型错误。不过由于 TypeScript 的限制,不支持自动补全。 47 | 48 | ![](../../../res/exporting-entities.gif) 49 | 50 | ::: 51 | 52 | ::: warning 53 | 54 | 在 `expansions` 中指定的关系中不能有[骨架实体](./resolving-data.html#skeleton-entities),否则将抛出错误。 55 | 56 | ::: 57 | -------------------------------------------------------------------------------- /docs/zh/guide/introduction.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 一个具备 **_❗ 超 棒 类 型 ❗_** 的轻量级对象关系映射器。 4 | 5 | ```sh 6 | npm i berry-orm 7 | ``` 8 | 9 | 我们在类型上花费了大量的精力,用以揭示一个 ORM 中的类型能严格到什么程度,以及使用这些超棒的类型有多令人愉悦,这也是这个项目的最大特色。 10 | 11 | > **需要: TypeScript 4.1/4.2/4.3/4.4/4.5** 12 | 13 | ## 可能的使用情景 14 | 15 | - 在 Web 应用中管理关系状态 16 | - 在 IndexedDB 中保存大量关系数据 17 | - 在类似 "data.json" 的简单文件中保存关系数据 18 | 19 | ## 示例 20 | 21 | ### 定义实体 22 | 23 |
24 | 25 | ```ts 26 | @Entity() 27 | class Book extends BaseEntity { 28 | @Primary() 29 | @Field() 30 | id!: number; 31 | 32 | @Field() 33 | name!: string; 34 | 35 | @Relation({ 36 | target: () => Author, 37 | inverse: "books", 38 | }) 39 | @Field() 40 | author!: Author; 41 | } 42 | 43 | @Entity() 44 | class Author extends BaseEntity { 45 | @Primary() 46 | @Field() 47 | id!: number; 48 | 49 | @Field() 50 | name!: string; 51 | 52 | @Relation({ 53 | target: () => Book, 54 | inverse: "author", 55 | multi: true, 56 | }) 57 | @Field() 58 | books!: Collection; 59 | } 60 | ``` 61 | 62 |
63 | 64 | ![](../../../res/defining-entities.gif) 65 | 66 | ### 解析数据 67 | 68 | ```ts 69 | const orm = new BerryOrm({ entities: [Book, Author] }); 70 | 71 | const book1 = orm.em.resolve(Book, { 72 | id: 1, 73 | name: "1000 Ways to Code", 74 | author: 1, 75 | }); 76 | 77 | book1[RESOLVED]; // true 78 | book1.author[RESOLVED]; // false 79 | 80 | const book2 = orm.em.resolve(Book, { 81 | id: 2, 82 | name: "2000 Ways to Code", 83 | author: { id: 1, name: "Char2s" }, 84 | }); 85 | 86 | book2[RESOLVED]; // true 87 | book2.author[RESOLVED]; // true 88 | 89 | book1.author == book2.author; // true 90 | ``` 91 | 92 | ### 导出实体 93 | 94 |
95 | 96 | ```ts 97 | const orm = new BerryOrm({ entities: [Book, Author] }); 98 | 99 | const book = orm.em.resolve(Book, { 100 | id: 1, 101 | name: "1000 Ways to Code", 102 | author: { id: 1, name: "Char2s" }, 103 | }); 104 | 105 | const data = orm.em.export(book, { author: { books: { author: true } } }); 106 | data.author.books[0].author. 107 | ``` 108 | 109 |
110 | 111 | ![](../../../res/exporting-entities.gif) 112 | -------------------------------------------------------------------------------- /docs/zh/guide/preparing-the-orm.md: -------------------------------------------------------------------------------- 1 | # 准备 ORM 2 | 3 | 我们的实体已经定义好了,现在是时候使用它们了。 4 | 5 | ```ts 6 | export const orm = new BerryOrm({ 7 | entities: [User, Department, Profile], 8 | }); 9 | ``` 10 | 11 | 所有我们想要在 `BerryOrm` 实例中使用的实体都需要被传递到 `entities` 选项。 12 | 13 | 实体检查将会在 `BerryOrm` 实例化的过程中执行,以确保这些实体可以正常使用。 14 | 15 | - 每个实体类都必须应用 `@Entity()`。 16 | - 如果注册了一个实体类,与其存在关系的所有实体类都需要同时注册。 17 | - 不允许注册具有相同类名的实体类。 18 | -------------------------------------------------------------------------------- /docs/zh/guide/resolving-data.md: -------------------------------------------------------------------------------- 1 | # 解析数据 2 | 3 | `orm.em.resolve()`将一个*普通数据对象*转换为一个*实体*。(`em` 意为 `EntityManager`) 4 | 5 | 当然,`orm.em.resolve()` 也是严格类型化的,以限制传递的普通数据对象的类型。 6 | 7 | ## 基本解析 8 | 9 | 主键和所有普通字段的值必须在普通数据对象中指定,而关系字段的值是可选的。 10 | 11 | 对于主字段和公共字段,它们在实体中的值只是简单地从普通数据对象中复制而来。 12 | 13 | ```ts 14 | const book = orm.em.resolve(Book, { 15 | id: 1, 16 | name: "Book", 17 | }); 18 | ``` 19 | 20 | ```ts 21 | book instanceof Book; // true 22 | book.id == 1; // true 23 | book.name == "Book"; // true 24 | ``` 25 | 26 | ## 重复解析 27 | 28 | 每个 `BerryOrm` 实例都有一个 `IdentityMap` 用以存储每个访问过的实体。因此,当另一个具有已知主键的普通数据对象被解析时,现有的实体将被更新并返回。 29 | 30 | ```ts 31 | const bookNew = orm.em.resolve(Book, { 32 | id: 1, 33 | name: "New Name", 34 | }); 35 | ``` 36 | 37 | ```ts 38 | bookOld == bookNew; // true 39 | bookOld.name == "New Name"; // true 40 | ``` 41 | 42 | ## 关系解析 43 | 44 | 对于关系字段,它们的值可以用主键或嵌套的普通数据对象来表示。 45 | 46 | ### 构建关系 47 | 48 | ```ts {4} 49 | const book = orm.em.resolve(Book, { 50 | id: 1, 51 | name: "Book", 52 | author: { id: 1, name: "Char2s" }, 53 | }); 54 | 55 | const author = orm.em.resolve(Author, { 56 | id: 1, 57 | name: "Char2s", 58 | }); 59 | ``` 60 | 61 | 双向的关系将在关系数据解析后构建,这意味着尽管 `author.books` 没有在其普通数据对象中指定,`book` 仍然被自动地添加进了 `author.books` 中,反之亦然。 62 | 63 | ```ts 64 | book.author == author; // true 65 | author.books.has(book); // true 66 | ``` 67 | 68 | 我们也可以使用主键来表示关系,但要注意的是,如果目标实体在之前没有被解析过,那么它们将成为[骨架实体](#skeleton-entities),关于这一点我们将在后面讲到。 69 | 70 | ### 更新关系 71 | 72 | 在[重复解析](#duplicated-resolving)中,旧的关系将被破坏,然后新的关系将被构建。 73 | 74 | ### 破坏关系 75 | 76 | 当一个 `undefined` 被指定为代表 _一对一_ 或 _多对一_ 关系字段的值时,或者一个移除了某些关系的数组被指定为代表 _一对多_ 或 _多对多_ 关系字段的值时,关系将被破坏。 77 | 78 | ```ts 79 | orm.em.resolve(Book, { 80 | id: 1, 81 | name: "Book", 82 | author: undefined, 83 | }); 84 | 85 | // 或者 86 | 87 | orm.em.resolve(Author, { 88 | id: 1, 89 | name: "Char2s", 90 | books: [], 91 | }); 92 | ``` 93 | 94 | ::: tip 95 | 96 | 这种方法不适合用来有目的地破坏关系。详见 [访问字段](./accessing-fields.html#relation-fields)。 97 | 98 | ::: 99 | 100 | ## 骨架实体 101 | 102 | 我们可以使用主键来表示普通数据对象中关系字段的值。 103 | 104 | ```ts {4} 105 | const book = orm.em.resolve(Book, { 106 | id: 1, 107 | name: "Book", 108 | author: 1, 109 | }); 110 | ``` 111 | 112 | 这里的 `book.author` 是一个 _骨架实体_,因为 `id: 1` 的 `Author` 此前还没有被解析过,这意味着我们只能确保它的主键是已知的,它的大部分普通字段和关系字段可能都是 `undefined`。 113 | 114 | ```ts 115 | book.author.id == 1; // true 116 | book.author.books.has(book); // true 117 | book.author.name === undefined; // true 118 | ``` 119 | 120 | 我们可以通过 `[RESOLVED]` 属性来识别骨架实体。 121 | 122 | ```ts 123 | book[RESOLVED]; // true 124 | book.author[RESOLVED]; // false 125 | ``` 126 | 127 | ::: tip 128 | 129 | `RESOLVED` 是一个 `symbol`。 130 | 131 | ::: 132 | -------------------------------------------------------------------------------- /docs/zh/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: Berry ORM 4 | tagline: 用于 Node.js 和浏览器的轻量级对象关系映射器。 5 | actionText: 快速上手 → 6 | actionLink: ./guide/introduction 7 | features: 8 | - title: 类型严格 9 | details: 享受极致严格的类型所带来的愉快的开发体验,最大化发挥TypeScript的作用。 10 | - title: 轻量通用 11 | details: 仅仅是对象关系映射,没有任何多余的功能,可在任何场景下使用。 12 | - title: 简单易用 13 | details: 通过简单的赋值和调用即可完全控制对象关系。 14 | footer: Apache-2.0 Licensed | Copyright © 2021-present Charles Gu 15 | --- 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | clearMocks: true, 5 | restoreMocks: true, 6 | globals: { 7 | "ts-jest": { 8 | tsconfig: "src/tsconfig.json", 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.ts": ["eslint --fix", "prettier --write"], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "berry-orm", 3 | "version": "0.3.0", 4 | "description": "A lightweight ORM with ❗SUPER AWESOME TYPINGS❗", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "prebuild": "rimraf lib", 9 | "build": "tsc -b tsconfig.build.json", 10 | "test": "jest", 11 | "format": "prettier --write \"src/**/*.ts\" \"*.js\" \"*.json\"", 12 | "format:diff": "prettier --list-different \"src/**/*.ts\"", 13 | "docs:dev": "vuepress dev docs", 14 | "docs:build": "vuepress build docs", 15 | "lint": "eslint --cache \"src/**/*.ts\"", 16 | "lint:fix": "eslint --cache --fix \"src/**/*.ts\"" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/TheNightmareX/berry-orm.git" 21 | }, 22 | "keywords": [ 23 | "orm", 24 | "database", 25 | "typescript", 26 | "frontend" 27 | ], 28 | "author": { 29 | "name": "Char2s", 30 | "email": "Char2s@outlook.com" 31 | }, 32 | "license": "Apache-2.0", 33 | "bugs": { 34 | "url": "https://github.com/TheNightmareX/berry-orm/issues" 35 | }, 36 | "homepage": "https://github.com/TheNightmareX/berry-orm#readme", 37 | "devDependencies": { 38 | "@types/jest": "^26.0.24", 39 | "@typescript-eslint/eslint-plugin": "^4.29.3", 40 | "@typescript-eslint/parser": "^4.29.3", 41 | "eslint": "^7.32.0", 42 | "eslint-config-prettier": "^8.3.0", 43 | "eslint-plugin-simple-import-sort": "^7.0.0", 44 | "husky": "^7.0.1", 45 | "jest": "^27.0.6", 46 | "lint-staged": "^11.1.2", 47 | "prettier": "^2.3.2", 48 | "rimraf": "^3.0.2", 49 | "ts-jest": "^27.0.4", 50 | "ts-node": "^10.1.0", 51 | "tslib": "^2.3.0", 52 | "typescript": "^4.5.4", 53 | "vuepress": "^1.8.2", 54 | "vuepress-plugin-redirect": "^1.2.5" 55 | }, 56 | "dependencies": { 57 | "lazy-promise": "^4.0.0" 58 | }, 59 | "files": [ 60 | "src", 61 | "lib", 62 | "!**/__test__", 63 | "!**/tsconfig.*", 64 | "README*.md" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "all", 3 | }; 4 | -------------------------------------------------------------------------------- /res/defining-entities.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Char2sGu/berry-orm/abe75c5a56b00f653933b19a16d8a93745e20593/res/defining-entities.gif -------------------------------------------------------------------------------- /res/exporting-entities.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Char2sGu/berry-orm/abe75c5a56b00f653933b19a16d8a93745e20593/res/exporting-entities.gif -------------------------------------------------------------------------------- /src/common/extended-promise.class.ts: -------------------------------------------------------------------------------- 1 | import Promise from "lazy-promise"; 2 | 3 | /** 4 | * Class-based and Lazy-resolved promise with synchronous access to the 5 | * execution result. 6 | */ 7 | export abstract class ExtendedPromise extends Promise { 8 | /** 9 | * Exists only after the promise is resolved. 10 | */ 11 | result?: Result; 12 | 13 | constructor() { 14 | super((resolve, reject) => { 15 | this.execute().then(resolve).catch(reject); 16 | }); 17 | } 18 | 19 | /** 20 | * Executor of the promise. 21 | */ 22 | protected abstract execute(): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /src/common/matched-keys.type.ts: -------------------------------------------------------------------------------- 1 | export type MatchedKeys = { 2 | [K in keyof T]: T[K] extends Condition ? K : never; 3 | }[keyof T]; 4 | -------------------------------------------------------------------------------- /src/common/type.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Type { 2 | new (...args: any[]): T; 3 | prototype: T; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/unmatched-keys.type.ts: -------------------------------------------------------------------------------- 1 | export type UnmatchedKeys = { 2 | [K in keyof T]: T[K] extends Condition ? never : K; 3 | }[keyof T]; 4 | -------------------------------------------------------------------------------- /src/core/__test__/berry-orm.class.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager, EntityRelationManager } from "../.."; 2 | import { BaseEntity } from "../../entity/base-entity.class"; 3 | import { EntityMetaError } from "../../meta/entity-meta.error"; 4 | import { Entity } from "../../meta/meta-decorators/entity.decorator"; 5 | import { Field } from "../../meta/meta-decorators/field.decorator"; 6 | import { Primary } from "../../meta/meta-decorators/primary.decorator"; 7 | import { Relation } from "../../meta/meta-decorators/relation.decorator"; 8 | import { BerryOrm } from "../berry-orm.class"; 9 | import { EntityEventManager } from "../entity-event-manager.class"; 10 | 11 | describe("BerryOrm", () => { 12 | describe("new", () => { 13 | it("should instantiate correctly in the simplest case", () => { 14 | @Entity() 15 | class TestingEntity extends BaseEntity { 16 | @Primary() 17 | @Field() 18 | id!: number; 19 | } 20 | new BerryOrm({ entities: [TestingEntity] }); 21 | }); 22 | 23 | it("should throw when relation entity is not registered", () => { 24 | @Entity() 25 | class TestingEntity1 extends BaseEntity { 26 | @Primary() 27 | @Field() 28 | id!: number; 29 | 30 | @Relation({ target: () => TestingEntity2, inverse: "entity" }) 31 | @Field() 32 | entity!: TestingEntity2; 33 | } 34 | 35 | @Entity() 36 | class TestingEntity2 extends BaseEntity { 37 | @Primary() 38 | @Field() 39 | id!: number; 40 | 41 | @Relation({ target: () => TestingEntity1, inverse: "entity" }) 42 | @Field() 43 | entity!: TestingEntity1; 44 | } 45 | 46 | expect(() => { 47 | new BerryOrm({ entities: [TestingEntity1] }); 48 | }).toThrowError(EntityMetaError); 49 | }); 50 | 51 | it("should throw when entity class is not decorated", () => { 52 | class TestingEntity extends BaseEntity { 53 | id!: number; 54 | } 55 | expect(() => { 56 | new BerryOrm({ entities: [TestingEntity] }); 57 | }).toThrowError(EntityMetaError); 58 | }); 59 | }); 60 | 61 | describe(".fork()", () => { 62 | it("should fork the original instance", () => { 63 | const base = new BerryOrm({ entities: [] }); 64 | const sub = base.fork(); 65 | 66 | expect(sub.parent).toBeInstanceOf(BerryOrm); 67 | expect(sub.em).toBeInstanceOf(EntityManager); 68 | expect(sub.erm).toBeInstanceOf(EntityRelationManager); 69 | expect(sub.eem).toBeInstanceOf(EntityEventManager); 70 | expect(sub.registry).toBeInstanceOf(Set); 71 | 72 | expect(sub.version).toBeDefined(); 73 | expect(sub.version).not.toBe(base.version); 74 | expect(sub.parent).toBe(base); 75 | expect(sub.em).not.toBe(base.em); 76 | expect(sub.erm).not.toBe(base.erm); 77 | expect(sub.eem).not.toBe(base.eem); 78 | expect(sub.registry).toBe(base.registry); 79 | }); 80 | }); 81 | 82 | describe(".reset()", () => { 83 | it("should update the version", () => { 84 | const orm = new BerryOrm({ entities: [] }); 85 | const version = orm.version; 86 | orm.reset(); 87 | expect(orm.version).toBe(version + 1); 88 | }); 89 | 90 | it("should work when reset for ", () => { 91 | const orm = new BerryOrm({ entities: [] }); 92 | orm.reset(); 93 | orm.reset(); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/core/__test__/entity-event-manager.class.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "../../entity/base-entity.class"; 2 | import { Entity } from "../../meta/meta-decorators/entity.decorator"; 3 | import { Field } from "../../meta/meta-decorators/field.decorator"; 4 | import { Primary } from "../../meta/meta-decorators/primary.decorator"; 5 | import { BerryOrm } from "../berry-orm.class"; 6 | 7 | describe("EntityEventManager", () => { 8 | @Entity() 9 | class TestingEntity extends BaseEntity { 10 | @Primary() 11 | @Field() 12 | id!: number; 13 | 14 | @Field() 15 | value!: string; 16 | } 17 | 18 | let orm: BerryOrm; 19 | let entity: TestingEntity; 20 | 21 | beforeEach(() => { 22 | orm = new BerryOrm({ entities: [TestingEntity] }); 23 | entity = orm.map.get(TestingEntity, 1); 24 | }); 25 | 26 | describe(".on()", () => { 27 | it("should invoke the resolve callback when the entity is resolved for the first time", () => { 28 | const spy = jest.fn(); 29 | orm.eem.on(entity, "resolve", spy); 30 | expect(spy).not.toHaveBeenCalled(); 31 | entity = orm.em.resolve(TestingEntity, { id: 1, value: "" }); 32 | expect(spy).toHaveBeenCalledWith(entity); 33 | }); 34 | 35 | it("should not invoke the update callback when the entity is resolved for the first time", () => { 36 | const spy = jest.fn(); 37 | orm.eem.on(entity, "update", spy); 38 | entity = orm.em.resolve(TestingEntity, { id: 1, value: "" }); 39 | expect(spy).not.toHaveBeenCalled(); 40 | }); 41 | 42 | it("should not invoke the callback when a field of an unresolved entity is assigned a new value", () => { 43 | const spy = jest.fn(); 44 | orm.eem.on(entity, "update", spy); 45 | entity.value = "new"; 46 | expect(spy).not.toHaveBeenCalled(); 47 | }); 48 | 49 | it("should invoke the update callback when a field of a resolved entity is assigned a new value", () => { 50 | const spy = jest.fn(); 51 | orm.eem.on(entity, "update", spy); 52 | entity = orm.em.resolve(TestingEntity, { id: 1, value: "" }); 53 | entity.value = "new"; 54 | expect(spy).toHaveBeenCalledWith(entity); 55 | }); 56 | }); 57 | 58 | describe(".off()", () => { 59 | it.each` 60 | index | exec 61 | ${1} | ${(cb: any) => orm.eem.off(entity, "update", cb)} 62 | ${2} | ${() => orm.eem.off(entity, "update")} 63 | ${3} | ${() => orm.eem.off(entity)} 64 | ${4} | ${() => orm.eem.off()} 65 | `("should not invoke the removed callback: $index", () => { 66 | const callback = jest.fn(); 67 | orm.eem.on(entity, "update", callback); 68 | orm.eem.off(entity, "update", callback); 69 | orm.eem.emit(entity, "update"); 70 | expect(callback).not.toHaveBeenCalled(); 71 | }); 72 | }); 73 | 74 | describe(".emit()", () => { 75 | it("should work for all the callbacks", () => { 76 | const cb1 = jest.fn(); 77 | const cb2 = jest.fn(); 78 | const cb3 = jest.fn(); 79 | orm.eem.on(entity, "update", cb1); 80 | orm.eem.on(TestingEntity, "update", cb2); 81 | orm.eem.on("any", "update", cb3); 82 | orm.eem.emit(entity, "update"); 83 | expect(cb1).toHaveBeenCalledTimes(1); 84 | expect(cb2).toHaveBeenCalledTimes(1); 85 | expect(cb3).toHaveBeenCalledTimes(1); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/core/__test__/entity-manager.class.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "../../entity/base-entity.class"; 2 | import { EntityData } from "../../entity/entity-data/entity-data.type"; 3 | import { EntityType } from "../../entity/entity-type.interface"; 4 | import { Collection } from "../../field/field-values/collection.class"; 5 | import { Entity } from "../../meta/meta-decorators/entity.decorator"; 6 | import { Field } from "../../meta/meta-decorators/field.decorator"; 7 | import { Primary } from "../../meta/meta-decorators/primary.decorator"; 8 | import { Relation } from "../../meta/meta-decorators/relation.decorator"; 9 | import { DateSerializer } from "../../serializer/built-in/date.serializer"; 10 | import { RESOLVED } from "../../symbols"; 11 | import { BerryOrm } from "../berry-orm.class"; 12 | 13 | describe("EntityManager", () => { 14 | let orm: BerryOrm; 15 | 16 | describe(".resolve()", () => { 17 | describe("Scalars", () => { 18 | @Entity() 19 | class TestingEntity extends BaseEntity { 20 | @Primary() 21 | @Field() 22 | id!: number; 23 | 24 | @Field() 25 | field1!: Date; 26 | } 27 | 28 | beforeEach(() => { 29 | prepare([TestingEntity]); 30 | }); 31 | 32 | let entity: TestingEntity; 33 | 34 | it.each` 35 | resolve 36 | ${() => orm.em.resolve(TestingEntity, { id: 1, field1: new Date() })} 37 | ${() => orm.em.resolve(TestingEntity, { id: 1, field1: new Date().toISOString() }, { field1: DateSerializer })} 38 | `("should populate the fields", ({ resolve }) => { 39 | entity = resolve(); 40 | expect(entity.id).toBe(1); 41 | expect(entity.field1).toBeInstanceOf(Date); 42 | expect(entity.field1).not.toBeNaN(); 43 | }); 44 | }); 45 | 46 | describe("Relations: One", () => { 47 | @Entity() 48 | class TestingEntity1 extends BaseEntity { 49 | @Primary() 50 | @Field() 51 | id!: number; 52 | 53 | @Relation({ 54 | target: () => TestingEntity2, 55 | inverse: "entity1", 56 | }) 57 | @Field() 58 | entity2!: TestingEntity2; 59 | } 60 | 61 | @Entity() 62 | class TestingEntity2 extends BaseEntity { 63 | @Primary() 64 | @Field() 65 | id!: number; 66 | 67 | @Relation({ 68 | target: () => TestingEntity1, 69 | inverse: "entity2", 70 | }) 71 | @Field() 72 | entity1!: TestingEntity1; 73 | } 74 | 75 | let result: TestingEntity1; 76 | 77 | beforeEach(() => { 78 | prepare([TestingEntity1, TestingEntity2]); 79 | }); 80 | 81 | describe("Foreign Keys", () => { 82 | describe("Create", () => { 83 | beforeEach(() => { 84 | result = orm.em.resolve(TestingEntity1, { 85 | id: 1, 86 | entity2: 1, 87 | }); 88 | }); 89 | 90 | it("should return an instance", () => { 91 | expect(result).toBeInstanceOf(TestingEntity1); 92 | }); 93 | 94 | it("should mark the entity as resolved", () => { 95 | expect(result[RESOLVED]).toBe(true); 96 | }); 97 | 98 | it("should build the bilateral relations", () => { 99 | expect(result.entity2).toBeInstanceOf(TestingEntity2); 100 | expect(result.entity2.entity1).toBe(result); 101 | }); 102 | 103 | it("should mark the relation entity as unresolved", () => { 104 | expect(result.entity2[RESOLVED]).toBe(false); 105 | }); 106 | }); 107 | 108 | describe("Update", () => { 109 | describe("Change", () => { 110 | beforeEach(() => { 111 | result = orm.em.resolve(TestingEntity1, { 112 | id: 1, 113 | entity2: 1, 114 | }); 115 | orm.em.resolve(TestingEntity1, { 116 | id: 1, 117 | entity2: 2, 118 | }); 119 | }); 120 | 121 | it("should construct the relation", () => { 122 | expect(result.entity2.id).toBe(2); 123 | expect(result.entity2.entity1).toBe(result); 124 | }); 125 | 126 | it("should destruct the previous relation", () => { 127 | const previous = orm.map.get(TestingEntity2, 1); 128 | expect(previous.entity1).toBeUndefined(); 129 | }); 130 | }); 131 | 132 | describe.each` 133 | value 134 | ${null} 135 | ${undefined} 136 | `("Remove: $value", ({ value }) => { 137 | beforeEach(() => { 138 | result = orm.em.resolve(TestingEntity1, { 139 | id: 1, 140 | entity2: 1, 141 | }); 142 | orm.em.resolve(TestingEntity1, { id: 1, entity2: value }); 143 | }); 144 | 145 | it("should destruct the relation", () => { 146 | expect(result.entity2).toBeFalsy(); 147 | }); 148 | }); 149 | }); 150 | }); 151 | 152 | describe("Nested Data", () => { 153 | describe("Create", () => { 154 | beforeEach(() => { 155 | result = orm.em.resolve(TestingEntity1, { 156 | id: 1, 157 | entity2: { id: 1 }, 158 | }); 159 | }); 160 | 161 | it("should return an instance", () => { 162 | expect(result).toBeInstanceOf(TestingEntity1); 163 | }); 164 | 165 | it("should mark the entity as resolved", () => { 166 | expect(result[RESOLVED]).toBe(true); 167 | }); 168 | 169 | it("should build the bilateral relations", () => { 170 | expect(result.entity2).toBeInstanceOf(TestingEntity2); 171 | expect(result.entity2.entity1).toBe(result); 172 | }); 173 | 174 | it("should mark the relation entity as resolved", () => { 175 | expect(result.entity2[RESOLVED]).toBe(true); 176 | }); 177 | }); 178 | 179 | describe("Update", () => { 180 | beforeEach(() => { 181 | result = orm.em.resolve(TestingEntity1, { 182 | id: 1, 183 | entity2: 1, 184 | }); 185 | orm.em.resolve(TestingEntity1, { id: 1, entity2: 2 }); 186 | }); 187 | 188 | it("should construct the new relation", () => { 189 | expect(result.entity2.id).toBe(2); 190 | expect(result.entity2.entity1).toBe(result); 191 | }); 192 | 193 | it("should destruct the previous relation", () => { 194 | const previous = orm.map.get(TestingEntity2, 1); 195 | expect(previous.entity1).toBeUndefined(); 196 | }); 197 | }); 198 | }); 199 | }); 200 | 201 | describe("Relations: Many", () => { 202 | @Entity() 203 | class TestingEntityParent extends BaseEntity { 204 | @Primary() 205 | @Field() 206 | id!: number; 207 | 208 | @Relation({ 209 | target: () => TestingEntityChild, 210 | inverse: "parent", 211 | multi: true, 212 | }) 213 | @Field() 214 | children!: Collection; 215 | } 216 | 217 | @Entity() 218 | class TestingEntityChild extends BaseEntity { 219 | @Primary() 220 | @Field() 221 | id!: number; 222 | 223 | @Relation({ 224 | target: () => TestingEntityParent, 225 | inverse: "children", 226 | }) 227 | @Field() 228 | parent!: TestingEntityParent; 229 | } 230 | 231 | beforeEach(() => { 232 | prepare([TestingEntityChild, TestingEntityParent]); 233 | }); 234 | 235 | describe("Foreign Keys", () => { 236 | describe("Parent", () => { 237 | let result: TestingEntityParent; 238 | 239 | describe("Create", () => { 240 | beforeEach(() => { 241 | result = orm.em.resolve(TestingEntityParent, { 242 | id: 1, 243 | children: [1], 244 | }); 245 | }); 246 | 247 | it("should return an instance", () => { 248 | expect(result).toBeInstanceOf(TestingEntityParent); 249 | }); 250 | 251 | it("should build the relations", () => { 252 | expect(result.children).toBeInstanceOf(Collection); 253 | expect(result.children.size).toBe(1); 254 | expect([...result.children][0].parent).toBe(result); 255 | }); 256 | }); 257 | 258 | describe("Update", () => { 259 | beforeEach(() => { 260 | result = orm.em.resolve(TestingEntityParent, { 261 | id: 1, 262 | children: [1], 263 | }); 264 | orm.em.resolve(TestingEntityParent, { 265 | id: 1, 266 | children: [2], 267 | }); 268 | }); 269 | 270 | it("should construct the relation", () => { 271 | expect(result.children.size).toBe(1); 272 | const child = [...result.children][0]; 273 | expect(child.id).toBe(2); 274 | expect(child.parent).toBe(result); 275 | }); 276 | 277 | it("should destruct the relation", () => { 278 | const previous = orm.map.get(TestingEntityChild, 1); 279 | expect(previous.parent).toBeUndefined(); 280 | }); 281 | }); 282 | }); 283 | 284 | describe("Child", () => { 285 | let result: TestingEntityChild; 286 | 287 | describe("Create", () => { 288 | beforeEach(() => { 289 | result = orm.em.resolve(TestingEntityChild, { 290 | id: 1, 291 | parent: 1, 292 | }); 293 | }); 294 | 295 | it("should return an instance", () => { 296 | expect(result).toBeInstanceOf(TestingEntityChild); 297 | }); 298 | 299 | it("should build the relations", () => { 300 | expect(result.parent).toBeInstanceOf(TestingEntityParent); 301 | expect(result.parent.children).toBeInstanceOf(Collection); 302 | expect([...result.parent.children][0]).toBe(result); 303 | }); 304 | }); 305 | 306 | describe("Update", () => { 307 | beforeEach(() => { 308 | result = orm.em.resolve(TestingEntityChild, { 309 | id: 1, 310 | parent: 1, 311 | }); 312 | orm.em.resolve(TestingEntityChild, { id: 1, parent: 2 }); 313 | }); 314 | 315 | it("should construct the relation", () => { 316 | expect(result.parent.id).toBe(2); 317 | expect([...result.parent.children][0]).toBe(result); 318 | }); 319 | 320 | it("should destruct the previous relation", () => { 321 | const previous = orm.map.get(TestingEntityParent, 1); 322 | expect(previous.children.size).toBe(0); 323 | }); 324 | }); 325 | }); 326 | }); 327 | 328 | describe("Nested Data", () => { 329 | describe("Parent", () => { 330 | let result: TestingEntityParent; 331 | 332 | describe("Create", () => { 333 | beforeEach(() => { 334 | result = orm.em.resolve(TestingEntityParent, { 335 | id: 1, 336 | children: [{ id: 1 }], 337 | }); 338 | }); 339 | 340 | it("should return an instance", () => { 341 | expect(result).toBeInstanceOf(TestingEntityParent); 342 | }); 343 | 344 | it("should build the relations", () => { 345 | expect(result.children).toBeInstanceOf(Collection); 346 | expect([...result.children][0]).toBeInstanceOf( 347 | TestingEntityChild, 348 | ); 349 | expect([...result.children][0].parent).toBe(result); 350 | }); 351 | }); 352 | 353 | describe("Update", () => { 354 | beforeEach(() => { 355 | result = orm.em.resolve(TestingEntityParent, { 356 | id: 1, 357 | children: [{ id: 1 }], 358 | }); 359 | orm.em.resolve(TestingEntityParent, { 360 | id: 1, 361 | children: [{ id: 2 }], 362 | }); 363 | }); 364 | 365 | it("should construct the relation", () => { 366 | const child = [...result.children][0]; 367 | expect(child.id).toBe(2); 368 | expect(child.parent).toBe(result); 369 | }); 370 | 371 | it("should destruct the previous relation", () => { 372 | const previous = orm.map.get(TestingEntityChild, 1); 373 | expect(previous.parent).toBeUndefined(); 374 | }); 375 | }); 376 | }); 377 | 378 | describe("Child", () => { 379 | let result: TestingEntityChild; 380 | 381 | describe("Create", () => { 382 | beforeEach(() => { 383 | result = orm.em.resolve(TestingEntityChild, { 384 | id: 1, 385 | parent: { id: 1 }, 386 | }); 387 | }); 388 | 389 | it("should return an instance", () => { 390 | expect(result).toBeInstanceOf(TestingEntityChild); 391 | }); 392 | 393 | it("should build the relations", () => { 394 | expect(result.parent).toBeInstanceOf(TestingEntityParent); 395 | expect(result.parent.children).toBeInstanceOf(Collection); 396 | expect([...result.parent.children][0]).toBe(result); 397 | }); 398 | }); 399 | 400 | describe("Update", () => { 401 | beforeEach(() => { 402 | result = orm.em.resolve(TestingEntityChild, { 403 | id: 1, 404 | parent: 1, 405 | }); 406 | orm.em.resolve(TestingEntityChild, { id: 1, parent: 2 }); 407 | }); 408 | 409 | it("should construct the relation", () => { 410 | expect(result.parent.id).toBe(2); 411 | expect([...result.parent.children][0]).toBe(result); 412 | }); 413 | 414 | it("should destruct the relation", () => { 415 | const previous = orm.map.get(TestingEntityParent, 1); 416 | expect(previous.children.size).toBe(0); 417 | }); 418 | }); 419 | }); 420 | }); 421 | }); 422 | }); 423 | 424 | describe(".export()", () => { 425 | it("should return an object equal to the data when there are only scalars in the entity", () => { 426 | @Entity() 427 | class TestingEntity extends BaseEntity { 428 | @Primary() @Field() id!: number; 429 | @Field() field!: string; 430 | } 431 | prepare([TestingEntity]); 432 | const originalData: EntityData = { id: 1, field: "a" }; 433 | const entity = orm.em.resolve(TestingEntity, originalData); 434 | const exportedData = orm.em.export(entity); 435 | expect(exportedData).toEqual(originalData); 436 | }); 437 | 438 | it("should apply the serializers when serializers are specified", () => { 439 | @Entity() 440 | class TestingEntity extends BaseEntity { 441 | @Primary() @Field() id!: number; 442 | @Field() date!: Date; 443 | } 444 | prepare([TestingEntity]); 445 | const date = new Date(); 446 | const entity = orm.em.resolve(TestingEntity, { id: 1, date }); 447 | const data = orm.em.export(entity, undefined, { 448 | date: DateSerializer, 449 | }); 450 | expect(typeof data.date == "string").toBe(true); 451 | expect(data.date).toBe(date.toISOString()); 452 | }); 453 | 454 | it("should expand the relations when expansions are specified as a to-one field", () => { 455 | @Entity() 456 | class TestingEntity extends BaseEntity { 457 | @Primary() @Field() id!: number; 458 | @Relation({ target: () => TestingEntity, inverse: "field" }) 459 | @Field() 460 | field!: TestingEntity; 461 | } 462 | prepare([TestingEntity]); 463 | const entity = orm.em.resolve(TestingEntity, { 464 | id: 1, 465 | field: { id: 2 }, 466 | }); 467 | const data = orm.em.export(entity, { field: true }, undefined); 468 | expect(typeof data.field == "object").toBe(true); 469 | }); 470 | 471 | it("should expand the relations when expansions are specified as a to-many field", () => { 472 | @Entity() 473 | class TestingEntity extends BaseEntity { 474 | @Primary() @Field() id!: number; 475 | @Relation({ 476 | target: () => TestingEntity, 477 | inverse: "field", 478 | multi: true, 479 | }) 480 | @Field() 481 | field!: Collection; 482 | } 483 | prepare([TestingEntity]); 484 | const entity = orm.em.resolve(TestingEntity, { 485 | id: 1, 486 | field: [{ id: 2 }], 487 | }); 488 | const data = orm.em.export(entity, { field: true }, undefined); 489 | expect(data.field).toBeInstanceOf(Array); 490 | expect(data.field[0].id).toBe(2); 491 | }); 492 | 493 | it("should apply the nested serializers when the specified serializers are nested", () => { 494 | @Entity() 495 | class TestingEntity extends BaseEntity { 496 | @Primary() @Field() id!: number; 497 | @Relation({ target: () => TestingEntity, inverse: "field1" }) 498 | @Field() 499 | field1!: TestingEntity; 500 | @Field() 501 | field2!: Date; 502 | } 503 | prepare([TestingEntity]); 504 | const entity = orm.em.resolve(TestingEntity, { 505 | id: 1, 506 | field2: new Date(), 507 | field1: { id: 2, field2: new Date() }, 508 | }); 509 | const data = orm.em.export( 510 | entity, 511 | { field1: true }, 512 | { field1: { field2: DateSerializer } }, 513 | ); 514 | expect(typeof data.field1 == "object").toBe(true); 515 | expect(data.field2).toBeInstanceOf(Date); 516 | expect(typeof data.field1.field2 == "string").toBe(true); 517 | }); 518 | }); 519 | 520 | function prepare(entities: EntityType[]) { 521 | orm = new BerryOrm({ entities }); 522 | } 523 | }); 524 | -------------------------------------------------------------------------------- /src/core/__test__/identity-map.spec.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../.."; 2 | import { BaseEntity } from "../../entity/base-entity.class"; 3 | import { Entity } from "../../meta/meta-decorators/entity.decorator"; 4 | import { Field } from "../../meta/meta-decorators/field.decorator"; 5 | import { Primary } from "../../meta/meta-decorators/primary.decorator"; 6 | import { IdentityMap } from "../identity-map.class"; 7 | 8 | describe("IdentityMap", () => { 9 | describe("*[Symbol.iterator]", () => { 10 | it("should iterate", () => { 11 | @Entity() 12 | class TestingEntity extends BaseEntity { 13 | @Primary() 14 | @Field() 15 | id!: number; 16 | } 17 | 18 | const orm = new BerryOrm({ entities: [TestingEntity] }); 19 | const map = new IdentityMap(orm); 20 | const entity = new TestingEntity(orm, 1); 21 | map.set(TestingEntity, 1, entity); 22 | 23 | for (const v of map) { 24 | expect(v).toEqual(["TestingEntity:1", entity]); 25 | } 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/core/berry-orm.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../entity/any-entity.type"; 2 | import { EntityType } from "../entity/entity-type.interface"; 3 | import { FieldDiscriminator } from "../field/field-discriminator.class"; 4 | import { EntityField } from "../field/field-names/entity-field.type"; 5 | import { EntityMetaError } from "../meta/entity-meta.error"; 6 | import { META } from "../symbols"; 7 | import { EntityEventManager } from "./entity-event-manager.class"; 8 | import { EntityManager } from "./entity-manager.class"; 9 | import { EntityRelationManager } from "./entity-relation-manager.class"; 10 | import { IdentityMap } from "./identity-map.class"; 11 | 12 | export class BerryOrm { 13 | private static nextVersion = 1; 14 | 15 | readonly parent?: BerryOrm; 16 | readonly registry: Set; 17 | readonly version = BerryOrm.nextVersion++; 18 | 19 | readonly em!: EntityManager; 20 | readonly erm!: EntityRelationManager; 21 | readonly eem!: EntityEventManager; 22 | readonly map!: IdentityMap; 23 | readonly discriminator!: FieldDiscriminator; 24 | 25 | constructor(options: BerryOrmOptions) { 26 | this.registry = new Set(options.entities); 27 | this.inspect(); 28 | this.provide(); 29 | } 30 | 31 | fork(): BerryOrm { 32 | const orm: BerryOrm = Object.create(BerryOrm.prototype); 33 | orm.define("parent", this); 34 | orm.define("registry", this.registry); 35 | orm.define("version", BerryOrm.nextVersion++); 36 | orm.provide(); 37 | return orm; 38 | } 39 | 40 | /** 41 | * Clear the identity map and update the ORM version, which will make all 42 | * existing entities belonging to this ORM instance to be unreachable. 43 | * @returns 44 | */ 45 | reset(): this { 46 | this.map.clear(); 47 | this.eem.off(); 48 | this.define("version", BerryOrm.nextVersion++); 49 | return this; 50 | } 51 | 52 | private provide() { 53 | this.define("em", new EntityManager(this)); 54 | this.define("erm", new EntityRelationManager(this)); 55 | this.define("eem", new EntityEventManager(this)); 56 | this.define("map", new IdentityMap(this)); 57 | this.define("discriminator", new FieldDiscriminator(this)); 58 | } 59 | 60 | private inspect() { 61 | const names = new Set(); 62 | 63 | this.registry.forEach((type) => { 64 | if (names.has(type.name)) throw new Error("Entity names must be unique"); 65 | names.add(type.name); 66 | this.inspectEntity(type); 67 | }); 68 | } 69 | 70 | private inspectEntity(type: EntityType) { 71 | const meta = type.prototype[META]; 72 | 73 | if (!meta?.completed) 74 | throw new EntityMetaError({ type, message: "@Entity() must be applied" }); 75 | 76 | for (const field in meta.fields) { 77 | this.inspectEntityField(type, field as EntityField); 78 | } 79 | } 80 | 81 | private inspectEntityField( 82 | type: EntityType, 83 | field: EntityField, 84 | ) { 85 | const meta = type.prototype[META].fields[field]; 86 | if (meta.relation) { 87 | if (!this.registry.has(meta.relation.target())) 88 | throw new EntityMetaError({ 89 | type, 90 | field: meta.name, 91 | message: "The relation entity must be also registered", 92 | }); 93 | } 94 | } 95 | 96 | /** 97 | * Bypass the `readonly` check. 98 | * @param key 99 | * @param value 100 | */ 101 | private define(key: Key, value: this[Key]) { 102 | this[key] = value; 103 | } 104 | } 105 | 106 | interface BerryOrmOptions { 107 | entities: EntityType[]; 108 | } 109 | -------------------------------------------------------------------------------- /src/core/entity-event-manager.class.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | import { AnyEntity } from "../entity/any-entity.type"; 4 | import { EntityType } from "../entity/entity-type.interface"; 5 | import { META } from "../symbols"; 6 | import { BerryOrm } from "./berry-orm.class"; 7 | 8 | const events = ["resolve", "update"] as const; 9 | 10 | type EntityEvent = typeof events[number]; 11 | type Listener = (entity: Entity) => void; 12 | 13 | export class EntityEventManager { 14 | private emitter = new EventEmitter(); 15 | 16 | constructor(readonly orm: BerryOrm) {} 17 | 18 | on( 19 | target: EventTarget, 20 | event: EntityEvent, 21 | listener: Listener, 22 | ): this { 23 | this.emitter.on(this.identify(target, event), listener); 24 | return this; 25 | } 26 | 27 | once( 28 | target: EventTarget, 29 | event: EntityEvent, 30 | listener: Listener, 31 | ): this { 32 | this.emitter.once(this.identify(target, event), listener); 33 | return this; 34 | } 35 | 36 | off( 37 | target: EventTarget, 38 | event: EntityEvent, 39 | listener: Listener, 40 | ): this; 41 | off( 42 | target: EventTarget, 43 | event: EntityEvent, 44 | ): this; 45 | off(target: EventTarget): this; 46 | off(): void; 47 | off( 48 | target?: EventTarget, 49 | event?: EntityEvent, 50 | listener?: Listener, 51 | ): this { 52 | if (target && event && listener) { 53 | this.emitter.off(this.identify(target, event), listener); 54 | } else if (target && event) { 55 | const id = this.identify(target, event); 56 | const listeners = this.emitter.listeners(id) as Listener[]; 57 | listeners.forEach((listener) => this.off(target, event, listener)); 58 | } else if (target) { 59 | events.forEach((event) => this.off(target, event)); 60 | } else { 61 | this.emitter.removeAllListeners(); 62 | } 63 | return this; 64 | } 65 | 66 | emit(entity: AnyEntity, event: EntityEvent): this { 67 | const type = entity.constructor as EntityType; 68 | this.emitter.emit(this.identify(entity, event), entity); 69 | this.emitter.emit(this.identify(type, event), entity); 70 | this.emitter.emit(this.identify("any", event), entity); 71 | 72 | return this; 73 | } 74 | 75 | private identify( 76 | target: EventTarget, 77 | event: EntityEvent, 78 | ) { 79 | if (typeof target == "string") { 80 | return `${target}:${event}`; 81 | } else if (target instanceof Function) { 82 | const name = target.name; 83 | return `${name}:${event}`; 84 | } else { 85 | const name = target.constructor.name; 86 | const pk = target[target[META].primary]; 87 | return `${name}:${pk}:${event}` as const; 88 | } 89 | } 90 | } 91 | 92 | type EventTarget = 93 | | Entity 94 | | EntityType 95 | | "any"; 96 | -------------------------------------------------------------------------------- /src/core/entity-manager-export-expansions-empty.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../entity/any-entity.type"; 2 | import { EntityManagerExportExpansions } from "./entity-manager-export-expansions.type"; 3 | 4 | export type EntityManagerExportExpansionsEmpty = 5 | Partial, undefined>>; 6 | -------------------------------------------------------------------------------- /src/core/entity-manager-export-expansions.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../entity/any-entity.type"; 2 | import { EntityFromRelationFieldValue } from "../entity/entity-from-relation-field-value.type"; 3 | import { RelationField } from "../field/field-names/relation-field.type"; 4 | 5 | export type EntityManagerExportExpansions = { 6 | [Field in RelationField]?: 7 | | EntityManagerExportExpansions> 8 | | boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/entity-manager.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../entity/any-entity.type"; 2 | import { EntityData } from "../entity/entity-data/entity-data.type"; 3 | import { EntityDataExported } from "../entity/entity-data/entity-data-exported.type"; 4 | import { EntityRepresentation } from "../entity/entity-representation.type"; 5 | import { EntityType } from "../entity/entity-type.interface"; 6 | import { RelationFieldData } from "../field/field-data/relation-field-data.type"; 7 | import { EntityField } from "../field/field-names/entity-field.type"; 8 | import { RelationField } from "../field/field-names/relation-field.type"; 9 | import { Collection } from "../field/field-values/collection.class"; 10 | import { PrimaryKey } from "../field/field-values/primary-key.type"; 11 | import { PrimaryKeyPossible } from "../field/field-values/primary-key-possible.type"; 12 | import { AbstractSerializer } from "../serializer/abstract.serializer"; 13 | import { NestedSerializerMap } from "../serializer/serializer-map/nested-serializer-map.type"; 14 | import { NestedSerializerMapEmpty } from "../serializer/serializer-map/nested-serializer-map-empty.type"; 15 | import { SerializerMap } from "../serializer/serializer-map/serializer-map.type"; 16 | import { SerializerMapEmpty } from "../serializer/serializer-map/serializer-map-empty.type"; 17 | import { SerializerType } from "../serializer/serializer-type.interface"; 18 | import { META, RESOLVED } from "../symbols"; 19 | import { BerryOrm } from "./berry-orm.class"; 20 | import { EntityManagerExportExpansions } from "./entity-manager-export-expansions.type"; 21 | import { EntityManagerExportExpansionsEmpty } from "./entity-manager-export-expansions-empty.type"; 22 | 23 | export class EntityManager { 24 | constructor(readonly orm: BerryOrm) {} 25 | 26 | resolve< 27 | Entity extends AnyEntity, 28 | Serializers extends SerializerMap = SerializerMapEmpty, 29 | >( 30 | type: EntityType, 31 | data: EntityData, 32 | serializers?: Serializers, 33 | ): Entity { 34 | const primaryKey = data[type.prototype[META].primary] as PrimaryKey; 35 | const entity = this.orm.map.get(type, primaryKey); 36 | 37 | for (const k in entity[META].fields) { 38 | const field = k as EntityField; 39 | if (!(field in data)) continue; 40 | if (this.orm.discriminator.isPrimaryField(entity, field)) continue; 41 | 42 | if (!this.orm.discriminator.isRelationField(entity, field)) { 43 | if (!serializers || !(field in serializers)) { 44 | entity[field] = data[field] as Entity[typeof field]; 45 | } else { 46 | type Type = SerializerType>; 47 | const serializer = new (serializers[field] as Type)(this.orm); 48 | entity[field] = serializer.deserialize(data[field] as any); 49 | } 50 | } else { 51 | this.resolveRelation( 52 | entity, 53 | field, 54 | data[field] as RelationFieldData, 55 | ); 56 | } 57 | } 58 | 59 | entity[RESOLVED] = true; 60 | this.orm.eem.emit(entity, "resolve"); 61 | 62 | return entity; 63 | } 64 | 65 | /** 66 | * Resolve the data to update the relation on the specified field of the 67 | * entity. 68 | * @param entity 69 | * @param field 70 | * @param data 71 | * @returns 72 | */ 73 | resolveRelation< 74 | Entity extends AnyEntity, 75 | Field extends RelationField, 76 | >( 77 | entity: Entity, 78 | field: Field, 79 | data: RelationFieldData, 80 | ): void { 81 | this.orm.erm.clearRelations(entity, field); 82 | 83 | if (!data) return; 84 | 85 | const representations = ( 86 | this.orm.discriminator.isRelationFieldToOne(entity, field) ? [data] : data 87 | ) as EntityRepresentation[]; 88 | 89 | representations.forEach((data) => { 90 | const targetEntity = this.resolveRepresentation( 91 | entity[META].fields[field].relation!.target(), 92 | data, 93 | ); 94 | this.orm.erm.constructRelation(entity, field, targetEntity); 95 | }); 96 | } 97 | 98 | /** 99 | * Resolve a primary key or a data object. 100 | * @param entity 101 | * @param field 102 | * @param representation 103 | * @returns 104 | */ 105 | resolveRepresentation>( 106 | type: EntityType, 107 | representation: EntityRepresentation, 108 | ): AnyEntity { 109 | if (typeof representation == "object") { 110 | return this.resolve(type, representation); 111 | } else { 112 | return this.orm.map.get(type, representation); 113 | } 114 | } 115 | 116 | /** 117 | * Export data from the entity. 118 | * @param entity 119 | * @param serializers 120 | * @param expand 121 | */ 122 | export< 123 | Entity extends AnyEntity, 124 | Expansions extends EntityManagerExportExpansions = EntityManagerExportExpansionsEmpty, 125 | Serializers extends NestedSerializerMap = NestedSerializerMapEmpty, 126 | >( 127 | entity: Entity, 128 | expansions?: Expansions, 129 | serializers?: Serializers, 130 | ): EntityDataExported { 131 | if (!entity[RESOLVED]) 132 | throw new Error("Unpopulated entities cannot be exported"); 133 | 134 | const data: Partial> = 135 | {}; 136 | 137 | const meta = entity[META]; 138 | for (const k in meta.fields) { 139 | const field = k as EntityField; 140 | 141 | if (!this.orm.discriminator.isRelationField(entity, field)) { 142 | const serializerType = serializers?.[ 143 | field 144 | ] as SerializerType; 145 | if (!serializerType) { 146 | data[field] = entity[field] as any; 147 | } else { 148 | const serializer = new serializerType!(this.orm); 149 | const value = serializer.serialize(entity[field]); 150 | data[field] = value as typeof data[typeof field]; 151 | } 152 | } else { 153 | if (!expansions?.[field]) { 154 | const getPrimaryKey = (entity: Entity) => 155 | entity[entity[META].primary] as PrimaryKey; 156 | 157 | if (this.orm.discriminator.isRelationFieldToOne(entity, field)) { 158 | data[field] = getPrimaryKey(entity[field] as AnyEntity); 159 | } else { 160 | const collection = entity[field] as Collection; 161 | const primaryKeys: PrimaryKeyPossible[] = []; 162 | collection.forEach((relationEntity) => { 163 | primaryKeys.push(getPrimaryKey(relationEntity)); 164 | }); 165 | } 166 | } else { 167 | const exportNested = (entity: AnyEntity) => { 168 | const nestExpansions = expansions?.[field as RelationField]; 169 | const nestSerializers = serializers?.[field]; 170 | return this.export( 171 | entity, 172 | typeof nestExpansions == "object" ? nestExpansions : undefined, 173 | nestSerializers, 174 | ); 175 | }; 176 | 177 | if (this.orm.discriminator.isRelationFieldToOne(entity, field)) { 178 | data[field] = exportNested( 179 | entity[field] as AnyEntity, 180 | ) as typeof data[typeof field]; 181 | } else { 182 | const collection = entity[field] as Collection; 183 | const dataList: EntityData[] = []; 184 | collection.forEach((relationEntity) => { 185 | dataList.push(exportNested(relationEntity)); 186 | }); 187 | data[field] = dataList as typeof data[typeof field]; 188 | } 189 | } 190 | } 191 | } 192 | 193 | return data as EntityDataExported; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/core/entity-relation-manager.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../entity/any-entity.type"; 2 | import { RelationField } from "../field/field-names/relation-field.type"; 3 | import { Collection } from "../field/field-values/collection.class"; 4 | import { EmptyValue } from "../field/field-values/empty-value.type"; 5 | import { META } from "../symbols"; 6 | import { BerryOrm } from "./berry-orm.class"; 7 | 8 | export class EntityRelationManager { 9 | constructor(readonly orm: BerryOrm) {} 10 | 11 | /** 12 | * Destruct any bilateral relation on the specified field of the entity. 13 | * @param entity 14 | * @param field 15 | */ 16 | clearRelations( 17 | entity: Entity, 18 | field: RelationField, 19 | ): this { 20 | this.invokeOnRelationField( 21 | entity, 22 | field, 23 | (relationEntity) => { 24 | if (!relationEntity) return; 25 | this.destructRelation(entity, field, relationEntity); 26 | return undefined; 27 | }, 28 | (relationEntities) => { 29 | relationEntities.forEach((relationEntity) => 30 | this.destructRelation(entity, field, relationEntity), 31 | ); 32 | return relationEntities; 33 | }, 34 | ); 35 | return this; 36 | } 37 | 38 | /** 39 | * Construct a bilateral relation with the target entity on the specified 40 | * field of the entity. 41 | * @param entity 42 | * @param field 43 | * @param targetEntity 44 | */ 45 | constructRelation( 46 | entity: Entity, 47 | field: RelationField, 48 | targetEntity: AnyEntity, 49 | ): this { 50 | this.invokeOnRelationFieldBilateral( 51 | entity, 52 | field, 53 | targetEntity, 54 | (targetEntity) => targetEntity, 55 | (targetEntity, entities) => entities.add(targetEntity), 56 | ); 57 | return this; 58 | } 59 | 60 | /** 61 | * Destruct the bilateral relation with the target entity on the specified 62 | * field of the entity if exists. 63 | * @param entity 64 | * @param field 65 | * @param targetEntity 66 | */ 67 | destructRelation( 68 | entity: Entity, 69 | field: RelationField, 70 | targetEntity: AnyEntity, 71 | ): this { 72 | this.invokeOnRelationFieldBilateral( 73 | entity, 74 | field, 75 | targetEntity, 76 | (targetEntity, entity) => (entity == targetEntity ? undefined : entity), 77 | (targetEntity, entities) => { 78 | entities?.delete(targetEntity); 79 | return entities; 80 | }, 81 | ); 82 | return this; 83 | } 84 | 85 | /** 86 | * A wrap of {@link EntityManager.invokeOnRelationField} which makes it 87 | * easier to operate on the both sides of the relation. 88 | * @param entity 89 | * @param field 90 | * @param targetEntity 91 | * @param onToOne 92 | * @param onToMany 93 | */ 94 | private invokeOnRelationFieldBilateral( 95 | entity: AnyEntity, 96 | field: string, 97 | targetEntity: AnyEntity, 98 | onToOne?: ( 99 | targetEntity: AnyEntity, 100 | entity: AnyEntity | EmptyValue, 101 | ) => AnyEntity | EmptyValue, 102 | onToMany?: ( 103 | targetEntity: AnyEntity, 104 | entities: Collection, 105 | ) => Collection, 106 | ) { 107 | const wrappedInvoke = ( 108 | entity: AnyEntity, 109 | field: string, 110 | targetEntity: AnyEntity, 111 | ) => 112 | this.invokeOnRelationField( 113 | entity, 114 | field, 115 | onToOne ? (entity) => onToOne(targetEntity, entity) : undefined, 116 | onToMany ? (entities) => onToMany(targetEntity, entities) : undefined, 117 | ); 118 | 119 | const relationMeta = entity[META].fields[field].relation!; 120 | wrappedInvoke(entity, field, targetEntity); 121 | wrappedInvoke(targetEntity, relationMeta.inverse, entity); 122 | return this; 123 | } 124 | 125 | /** 126 | * Invoke a callback based on the field's relation type. 127 | * @param entity 128 | * @param field 129 | * @param onToOne - The callback to be invoked on a to-one relation field. 130 | * The return value will be set as the value of the field. 131 | * @param onToMany - The callback to be invoked on a to-many relation field. 132 | */ 133 | private invokeOnRelationField( 134 | entity: AnyEntity, 135 | field: string, 136 | onToOne?: (entity: AnyEntity | EmptyValue) => AnyEntity | EmptyValue, 137 | onToMany?: (entities: Collection) => void, 138 | ) { 139 | if (this.orm.discriminator.isRelationFieldToMany(entity, field)) { 140 | if (!onToMany) return; 141 | const relationEntities = entity[field] as Collection; 142 | onToMany(relationEntities); 143 | } else { 144 | if (!onToOne) return; 145 | const relationEntity = entity[field] as AnyEntity | EmptyValue; 146 | const processed = onToOne(relationEntity); 147 | entity[field] = processed; 148 | } 149 | return this; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/core/identity-map.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../entity/any-entity.type"; 2 | import { EntityType } from "../entity/entity-type.interface"; 3 | import { PrimaryKey } from "../field/field-values/primary-key.type"; 4 | import { BerryOrm } from "./berry-orm.class"; 5 | 6 | export class IdentityMap { 7 | private map = new Map(); 8 | 9 | constructor(readonly orm: BerryOrm) {} 10 | 11 | get( 12 | type: EntityType, 13 | primaryKey: PrimaryKey, 14 | ): Entity { 15 | this.checkType(type); 16 | const id = this.identify(type, primaryKey); 17 | let entity = this.map.get(id) as Entity | undefined; 18 | if (!entity) { 19 | entity = new type(this.orm, primaryKey); 20 | this.set(type, primaryKey, entity); 21 | } 22 | return entity as Entity; 23 | } 24 | 25 | set( 26 | type: EntityType, 27 | primaryKey: PrimaryKey, 28 | entity: Entity, 29 | ): this { 30 | this.checkType(type); 31 | const id = this.identify(type, primaryKey); 32 | this.map.set(id, entity); 33 | return this; 34 | } 35 | 36 | has( 37 | type: EntityType, 38 | primaryKey: PrimaryKey, 39 | ): boolean { 40 | this.checkType(type); 41 | const id = this.identify(type, primaryKey); 42 | return this.map.has(id); 43 | } 44 | 45 | /** 46 | * Clear the identities. 47 | * 48 | * In most cases, you are NOT likely to invoke this method manually because 49 | * this won't deal with the existing entities. Maybe {@link BerryOrm.reset} 50 | * is what you want. 51 | */ 52 | clear(): void { 53 | this.map.clear(); 54 | } 55 | 56 | *[Symbol.iterator](): Iterator<[string, AnyEntity]> { 57 | yield* this.map[Symbol.iterator](); 58 | } 59 | 60 | private identify( 61 | type: EntityType, 62 | key: PrimaryKey, 63 | ) { 64 | return `${type.name}:${key}` as const; 65 | } 66 | 67 | private checkType(type: EntityType) { 68 | if (!this.orm.registry.has(type as EntityType)) 69 | throw new Error(`${type.name} is not a known entity type`); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/entity/any-entity.type.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryField } from "../field/field-names/primary-field.type"; 2 | import { BaseEntity } from "./base-entity.class"; 3 | 4 | /** 5 | * Represent an entity extending the `BaseEntity`. 6 | */ 7 | export type AnyEntity = any> = BaseEntity< 8 | Entity, 9 | PrimaryField 10 | > & 11 | Record; 12 | -------------------------------------------------------------------------------- /src/entity/base-entity.class.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm, PrimaryFieldPossible } from ".."; 2 | import { CommonFieldAccessor } from "../field/field-accessors/common-field.accessor"; 3 | import { PrimaryFieldAccessor } from "../field/field-accessors/primary-field.accessor"; 4 | import { RelationFieldToManyAccessor } from "../field/field-accessors/relation-field-to-many.accessor"; 5 | import { RelationFieldToOneAccessor } from "../field/field-accessors/relation-field-to-one.accessor"; 6 | import { CommonField } from "../field/field-names/common-field.type"; 7 | import { EntityField } from "../field/field-names/entity-field.type"; 8 | import { PrimaryField } from "../field/field-names/primary-field.type"; 9 | import { RelationFieldToMany } from "../field/field-names/relation-field-to-many.type"; 10 | import { RelationFieldToOne } from "../field/field-names/relation-field-to-one.type"; 11 | import { PrimaryKey } from "../field/field-values/primary-key.type"; 12 | import { EntityMeta } from "../meta/meta-objects/entity-meta.class"; 13 | import { META, RESOLVED, VERSION } from "../symbols"; 14 | import { AnyEntity } from "./any-entity.type"; 15 | import { EntityType } from "./entity-type.interface"; 16 | 17 | // It's not possible to use Active Record mode in Berry ORM because type 18 | // circular reference will happen and cause compile error. 19 | 20 | // It's also impossible to implement a `@Serializer()` decorator, because 21 | // the serializer type should be accessible in other part of this lib to infer 22 | // the data type properly, which is impossible in decorators. 23 | // By the way, defining a static property `serializers` will not be implemented 24 | // because this will make type inference much more difficult because the 25 | // auto-inference supports only one level, and this will also make the style 26 | // become messy because we used both static properties and decorators to define 27 | // metadata for fields. 28 | 29 | /** 30 | * The base class of every entity. 31 | * 32 | * It is recommended to create an own `BaseEntity`, which extends this one and 33 | * is defined getters so that the metadata can be accessed more conveniently. 34 | */ 35 | export abstract class BaseEntity< 36 | Entity extends AnyEntity, 37 | Primary extends PrimaryFieldPossible, 38 | > { 39 | private static init>( 40 | orm: BerryOrm, 41 | entity: Entity, 42 | primaryKey: PrimaryKey, 43 | ) { 44 | entity[VERSION] = orm.version; 45 | for (const field of Object.keys(entity[META].fields)) 46 | this.initField(orm, entity, field as EntityField); 47 | entity[entity[META].primary] = primaryKey; 48 | } 49 | 50 | private static initField( 51 | orm: BerryOrm, 52 | entity: Entity, 53 | field: EntityField, 54 | ) { 55 | const isPrimary = entity[META].primary == field; 56 | const isToMany = !!entity[META].fields[field].relation?.multi; 57 | const isToOne = !!entity[META].fields[field].relation && !isToMany; 58 | 59 | const accessor = isPrimary 60 | ? new PrimaryFieldAccessor(orm, entity, field as PrimaryField) 61 | : isToMany 62 | ? new RelationFieldToManyAccessor( 63 | orm, 64 | entity, 65 | field as RelationFieldToMany, 66 | ) 67 | : isToOne 68 | ? new RelationFieldToOneAccessor( 69 | orm, 70 | entity, 71 | field as RelationFieldToOne, 72 | ) 73 | : new CommonFieldAccessor(orm, entity, field as CommonField); 74 | 75 | accessor.apply(); 76 | } 77 | 78 | /** 79 | * Definition metadata of this entity type. 80 | * 81 | * Potentially `undefined` because it only exists when there are at least one 82 | * decorator applied. 83 | */ 84 | [META]: EntityMeta; 85 | 86 | /** 87 | * Indicates that the **data** fields (**relation** fields not included) of 88 | * the this has been populated. 89 | */ 90 | [RESOLVED] = false; 91 | 92 | /** 93 | * The ORM version of this entity which is used to prevent operations on 94 | * expired entities. 95 | */ 96 | [VERSION]: number; 97 | 98 | constructor(...[orm, primaryKey]: ConstructorParameters>) { 99 | BaseEntity.init(orm, this as unknown as Entity, primaryKey); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/entity/entity-data/entity-data-common.type.ts: -------------------------------------------------------------------------------- 1 | import { CommonField } from "../../field/field-names/common-field.type"; 2 | import { PrimaryField } from "../../field/field-names/primary-field.type"; 3 | import { AbstractSerializer } from "../../serializer/abstract.serializer"; 4 | import { SerializerMap } from "../../serializer/serializer-map/serializer-map.type"; 5 | import { SerializerMapEmpty } from "../../serializer/serializer-map/serializer-map-empty.type"; 6 | import { SerializerType } from "../../serializer/serializer-type.interface"; 7 | import { AnyEntity } from "../any-entity.type"; 8 | 9 | export type EntityDataCommon< 10 | Entity extends AnyEntity, 11 | Serializers extends SerializerMap = SerializerMapEmpty, 12 | > = { 13 | [Field in 14 | | CommonField 15 | | PrimaryField]: Serializers[Field] extends SerializerType< 16 | AbstractSerializer 17 | > 18 | ? Entity[Field] | Alternative 19 | : Entity[Field]; 20 | }; 21 | -------------------------------------------------------------------------------- /src/entity/entity-data/entity-data-exported.type.ts: -------------------------------------------------------------------------------- 1 | import { EntityManagerExportExpansions } from "../../core/entity-manager-export-expansions.type"; 2 | import { EntityManagerExportExpansionsEmpty } from "../../core/entity-manager-export-expansions-empty.type"; 3 | import { CommonField } from "../../field/field-names/common-field.type"; 4 | import { PrimaryField } from "../../field/field-names/primary-field.type"; 5 | import { RelationField } from "../../field/field-names/relation-field.type"; 6 | import { RelationFieldToMany } from "../../field/field-names/relation-field-to-many.type"; 7 | import { RelationFieldToOne } from "../../field/field-names/relation-field-to-one.type"; 8 | import { PrimaryKey } from "../../field/field-values/primary-key.type"; 9 | import { AbstractSerializer } from "../../serializer/abstract.serializer"; 10 | import { NestedSerializerMap } from "../../serializer/serializer-map/nested-serializer-map.type"; 11 | import { NestedSerializerMapEmpty } from "../../serializer/serializer-map/nested-serializer-map-empty.type"; 12 | import { SerializerType } from "../../serializer/serializer-type.interface"; 13 | import { AnyEntity } from "../any-entity.type"; 14 | import { EntityFromRelationFieldValue } from "../entity-from-relation-field-value.type"; 15 | 16 | export type EntityDataExported< 17 | Entity extends AnyEntity, 18 | Serializers extends NestedSerializerMap = NestedSerializerMapEmpty, 19 | Expansions extends EntityManagerExportExpansions = EntityManagerExportExpansionsEmpty, 20 | > = { 21 | [Field in 22 | | CommonField 23 | | PrimaryField]: Serializers[Field] extends SerializerType< 24 | AbstractSerializer 25 | > 26 | ? Value 27 | : Entity[Field]; 28 | } & 29 | { 30 | [Field in RelationField]: Expansions[Field] extends 31 | | true 32 | | EntityManagerExportExpansions< 33 | EntityFromRelationFieldValue 34 | > 35 | ? Field extends RelationFieldToOne 36 | ? EntityDataExported< 37 | EntityFromRelationFieldValue, 38 | NestedSerializerMapUniformed< 39 | EntityFromRelationFieldValue, 40 | Serializers[Field] 41 | >, 42 | RelationExpansionsUniformed< 43 | EntityFromRelationFieldValue, 44 | Expansions[Field] 45 | > 46 | > 47 | : Field extends RelationFieldToMany 48 | ? EntityDataExported< 49 | EntityFromRelationFieldValue, 50 | NestedSerializerMapUniformed< 51 | EntityFromRelationFieldValue, 52 | Serializers[Field] 53 | >, 54 | RelationExpansionsUniformed< 55 | EntityFromRelationFieldValue, 56 | Expansions[Field] 57 | > 58 | >[] 59 | : never 60 | : Field extends RelationFieldToOne 61 | ? PrimaryKey 62 | : Field extends RelationFieldToMany 63 | ? PrimaryKey[] 64 | : never; 65 | }; 66 | 67 | type NestedSerializerMapUniformed< 68 | Entity extends AnyEntity, 69 | Value, 70 | > = Value extends NestedSerializerMap 71 | ? Value 72 | : NestedSerializerMapEmpty; 73 | 74 | type RelationExpansionsUniformed< 75 | Entity extends AnyEntity, 76 | Value, 77 | > = Value extends EntityManagerExportExpansions 78 | ? Value 79 | : EntityManagerExportExpansionsEmpty; 80 | -------------------------------------------------------------------------------- /src/entity/entity-data/entity-data-relational.type.ts: -------------------------------------------------------------------------------- 1 | import { RelationFieldData } from "../../field/field-data/relation-field-data.type"; 2 | import { RelationField } from "../../field/field-names/relation-field.type"; 3 | import { AnyEntity } from "../any-entity.type"; 4 | 5 | export type EntityDataRelational = { 6 | [Field in RelationField]: RelationFieldData; 7 | }; 8 | -------------------------------------------------------------------------------- /src/entity/entity-data/entity-data.type.ts: -------------------------------------------------------------------------------- 1 | import { SerializerMap } from "../../serializer/serializer-map/serializer-map.type"; 2 | import { SerializerMapEmpty } from "../../serializer/serializer-map/serializer-map-empty.type"; 3 | import { AnyEntity } from "../any-entity.type"; 4 | import { EntityDataCommon } from "./entity-data-common.type"; 5 | import { EntityDataRelational } from "./entity-data-relational.type"; 6 | 7 | export type EntityData< 8 | Entity extends AnyEntity, 9 | Serializers extends SerializerMap = SerializerMapEmpty, 10 | > = EntityDataCommon & 11 | Partial>; 12 | -------------------------------------------------------------------------------- /src/entity/entity-from-relation-field-value.type.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "../field/field-values/collection.class"; 2 | import { AnyEntity } from "./any-entity.type"; 3 | 4 | export type EntityFromRelationFieldValue< 5 | FieldValue extends AnyEntity | Collection, 6 | > = FieldValue extends AnyEntity 7 | ? FieldValue 8 | : FieldValue extends Collection 9 | ? Entity 10 | : never; 11 | -------------------------------------------------------------------------------- /src/entity/entity-representation.type.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryKey } from "../field/field-values/primary-key.type"; 2 | import { AnyEntity } from "./any-entity.type"; 3 | import { EntityData } from "./entity-data/entity-data.type"; 4 | 5 | export type EntityRepresentation = 6 | | PrimaryKey 7 | | EntityData; 8 | -------------------------------------------------------------------------------- /src/entity/entity-type.interface.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../core/berry-orm.class"; 2 | import { PrimaryKey } from "../field/field-values/primary-key.type"; 3 | import { AnyEntity } from "./any-entity.type"; 4 | 5 | export interface EntityType { 6 | new (orm: BerryOrm, primaryKey: PrimaryKey): Entity; 7 | prototype: Entity; 8 | } 9 | -------------------------------------------------------------------------------- /src/field/field-accessors/__test__/base-field.accessor.spec.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../../../core/berry-orm.class"; 2 | import { BaseEntity } from "../../../entity/base-entity.class"; 3 | import { Entity } from "../../../meta/meta-decorators/entity.decorator"; 4 | import { Field } from "../../../meta/meta-decorators/field.decorator"; 5 | import { Primary } from "../../../meta/meta-decorators/primary.decorator"; 6 | 7 | describe("BaseFieldAccessor", () => { 8 | @Entity() 9 | class TestingEntity extends BaseEntity { 10 | @Primary() 11 | @Field() 12 | id!: number; 13 | 14 | @Field() 15 | value!: string; 16 | } 17 | 18 | let orm: BerryOrm; 19 | 20 | beforeEach(() => { 21 | orm = new BerryOrm({ entities: [TestingEntity] }); 22 | }); 23 | 24 | describe(".handleGet()", () => { 25 | it("should throw when the entity has expired", () => { 26 | const entity = orm.em.resolve(TestingEntity, { id: 1, value: "" }); 27 | orm.reset(); 28 | expect(() => { 29 | entity.value; 30 | }).toThrow("Entity version does not match the ORM version: 1/2"); 31 | }); 32 | }); 33 | 34 | describe(".handleSet()", () => { 35 | it("should throw when the entity has expired", () => { 36 | const entity = orm.em.resolve(TestingEntity, { id: 1, value: "" }); 37 | orm.reset(); 38 | expect(() => { 39 | entity.value = ""; 40 | }).toThrow("Entity version does not match the ORM version: 3/4"); 41 | }); 42 | }); 43 | 44 | it("should be enumerable", () => { 45 | const entity = orm.map.get(TestingEntity, 1); 46 | expect(Object.entries(entity)).toEqual([ 47 | ["id", 1], 48 | ["value", undefined], 49 | ]); 50 | entity.value = "value"; 51 | expect(Object.entries(entity)).toEqual([ 52 | ["id", 1], 53 | ["value", "value"], 54 | ]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/field/field-accessors/base-field.accessor.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../../core/berry-orm.class"; 2 | import { AnyEntity } from "../../entity/any-entity.type"; 3 | import { RESOLVED, VERSION } from "../../symbols"; 4 | import { EntityField } from "../field-names/entity-field.type"; 5 | import { FieldAccessDeniedError } from "./field-access-denied.error"; 6 | 7 | export class BaseFieldAccessor< 8 | Entity extends AnyEntity = AnyEntity, 9 | Field extends EntityField = EntityField, 10 | > { 11 | /** 12 | * Exists only after {@link BaseFieldAccessor.apply} has been invoked. 13 | */ 14 | value!: Entity[Field]; 15 | 16 | constructor( 17 | protected orm: BerryOrm, 18 | readonly entity: Entity, 19 | readonly field: Field, 20 | ) {} 21 | 22 | /** 23 | * Initialize {@link BaseFieldAccessor.value} and start the proxy. 24 | */ 25 | apply(): void { 26 | this.value = this.entity[this.field]; 27 | Reflect.defineProperty(this.entity, this.field, { 28 | configurable: true, 29 | get: () => this.handleGet(), 30 | set: (v) => this.handleSet(v), 31 | enumerable: true, 32 | }); 33 | } 34 | 35 | handleGet(): Entity[Field] { 36 | this.checkExpiry(); 37 | return this.value; 38 | } 39 | 40 | handleSet(newValue: Entity[Field]): void { 41 | this.checkExpiry(); 42 | this.value = newValue; 43 | if (this.entity[RESOLVED]) this.orm.eem.emit(this.entity, "update"); 44 | } 45 | 46 | private checkExpiry() { 47 | if (this.entity[VERSION] == this.orm.version) return; 48 | throw new FieldAccessDeniedError( 49 | this.entity, 50 | this.field, 51 | "version-conflict", 52 | `Entity version does not match the ORM version: ${this.entity[VERSION]}/${this.orm.version}`, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/field/field-accessors/common-field.accessor.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { CommonField } from "../field-names/common-field.type"; 3 | import { BaseFieldAccessor } from "./base-field.accessor"; 4 | 5 | export class CommonFieldAccessor< 6 | Entity extends AnyEntity = AnyEntity, 7 | Field extends CommonField = CommonField, 8 | > extends BaseFieldAccessor {} 9 | -------------------------------------------------------------------------------- /src/field/field-accessors/field-access-denied.error.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityField } from "../field-names/entity-field.type"; 3 | 4 | export class FieldAccessDeniedError extends Error { 5 | constructor( 6 | readonly entity: Entity, 7 | readonly field: EntityField, 8 | readonly type: FieldAccessDeniedType, 9 | readonly message: string, 10 | ) { 11 | super(message); 12 | } 13 | } 14 | 15 | type FieldAccessDeniedType = "version-conflict" | "readonly"; 16 | -------------------------------------------------------------------------------- /src/field/field-accessors/primary-field.accessor.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { PrimaryField } from "../field-names/primary-field.type"; 3 | import { PrimaryKey } from "../field-values/primary-key.type"; 4 | import { BaseFieldAccessor } from "./base-field.accessor"; 5 | import { FieldAccessDeniedError } from "./field-access-denied.error"; 6 | 7 | export class PrimaryFieldAccessor< 8 | Entity extends AnyEntity, 9 | > extends BaseFieldAccessor> { 10 | handleSet(newValue: PrimaryKey): void { 11 | if (this.value) 12 | throw new FieldAccessDeniedError( 13 | this.entity, 14 | this.field, 15 | "readonly", 16 | "Primary key fields are readonly", 17 | ); 18 | super.handleSet(newValue); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/field/field-accessors/relation-field-to-many.accessor.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { RelationFieldToMany } from "../field-names/relation-field-to-many.type"; 3 | import { Collection } from "../field-values/collection.class"; 4 | import { BaseFieldAccessor } from "./base-field.accessor"; 5 | import { FieldAccessDeniedError } from "./field-access-denied.error"; 6 | 7 | export class RelationFieldToManyAccessor< 8 | Entity extends AnyEntity = AnyEntity, 9 | Field extends RelationFieldToMany = RelationFieldToMany, 10 | > extends BaseFieldAccessor { 11 | apply(): void { 12 | this.entity[this.field] = new Collection( 13 | this.orm, 14 | this.entity, 15 | this.field, 16 | ) as Entity[Field]; 17 | super.apply(); 18 | } 19 | 20 | handleSet(): void { 21 | throw new FieldAccessDeniedError( 22 | this.entity, 23 | this.field, 24 | "readonly", 25 | "Collection fields are readonly", 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/field/field-accessors/relation-field-to-one.accessor.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { RelationFieldToOne } from "../field-names/relation-field-to-one.type"; 3 | import { BaseFieldAccessor } from "./base-field.accessor"; 4 | 5 | export class RelationFieldToOneAccessor< 6 | Entity extends AnyEntity = AnyEntity, 7 | Field extends RelationFieldToOne = RelationFieldToOne, 8 | > extends BaseFieldAccessor { 9 | handleSet(newValue: Entity[Field]): void { 10 | const currentValue = this.value; 11 | 12 | // end up recurse 13 | if (newValue == currentValue) return; 14 | 15 | super.handleSet(newValue); 16 | 17 | if (newValue) 18 | this.orm.erm.constructRelation(this.entity, this.field, newValue); 19 | else this.orm.erm.destructRelation(this.entity, this.field, currentValue); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/field/field-data/relation-field-data.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityRepresentation } from "../../entity/entity-representation.type"; 3 | import { RelationField } from "../field-names/relation-field.type"; 4 | import { Collection } from "../field-values/collection.class"; 5 | import { EmptyValue } from "../field-values/empty-value.type"; 6 | 7 | export type RelationFieldData< 8 | Entity extends AnyEntity = AnyEntity, 9 | Field extends RelationField = RelationField, 10 | > = Entity[Field] extends AnyEntity 11 | ? EntityRepresentation | EmptyValue 12 | : Entity[Field] extends Collection 13 | ? EntityRepresentation[] 14 | : never; 15 | -------------------------------------------------------------------------------- /src/field/field-discriminator.class.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../core/berry-orm.class"; 2 | import { AnyEntity } from "../entity/any-entity.type"; 3 | import { META } from "../symbols"; 4 | import { CommonField } from "./field-names/common-field.type"; 5 | import { EntityField } from "./field-names/entity-field.type"; 6 | import { PrimaryField } from "./field-names/primary-field.type"; 7 | import { RelationField } from "./field-names/relation-field.type"; 8 | import { RelationFieldToMany } from "./field-names/relation-field-to-many.type"; 9 | import { RelationFieldToOne } from "./field-names/relation-field-to-one.type"; 10 | 11 | export class FieldDiscriminator { 12 | constructor(readonly orm: BerryOrm) {} 13 | 14 | isPrimaryField( 15 | entity: Entity, 16 | field: EntityField, 17 | ): field is PrimaryField { 18 | return entity[META].primary == field; 19 | } 20 | 21 | isCommonField( 22 | entity: Entity, 23 | field: EntityField, 24 | ): field is CommonField { 25 | return ( 26 | !this.isPrimaryField(entity, field) && 27 | !this.isRelationField(entity, field) 28 | ); 29 | } 30 | 31 | isRelationField( 32 | entity: Entity, 33 | field: EntityField, 34 | ): field is RelationField { 35 | return !!entity[META].fields[field].relation; 36 | } 37 | 38 | isRelationFieldToOne( 39 | entity: Entity, 40 | field: EntityField, 41 | ): field is RelationFieldToOne { 42 | return ( 43 | this.isRelationField(entity, field) && 44 | !this.isRelationFieldToMany(entity, field) 45 | ); 46 | } 47 | 48 | isRelationFieldToMany( 49 | entity: Entity, 50 | field: EntityField, 51 | ): field is RelationFieldToMany { 52 | return ( 53 | this.isRelationField(entity, field) && 54 | !!entity[META].fields[field].relation!.multi 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/field/field-names/common-field.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityFieldBase } from "./entity-field-base.type"; 3 | import { RelationField } from "./relation-field.type"; 4 | 5 | export type CommonField = Exclude< 6 | EntityFieldBase, 7 | /** 8 | * Relation fields are not possible to be `string` unless `Entity` is 9 | * `AnyEntity`, if so, `Exclude` will be `never` and cause 10 | * type errors. So it is required to make sure this type return `string` in 11 | * this case. 12 | */ 13 | string extends RelationField ? never : RelationField 14 | >; 15 | -------------------------------------------------------------------------------- /src/field/field-names/entity-field-base.type.ts: -------------------------------------------------------------------------------- 1 | import { UnmatchedKeys } from "../../common/unmatched-keys.type"; 2 | import { AnyEntity } from "../../entity/any-entity.type"; 3 | 4 | export type EntityFieldBase = Extract< 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | UnmatchedKeys, 7 | string 8 | >; 9 | -------------------------------------------------------------------------------- /src/field/field-names/entity-field.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { CommonField } from "./common-field.type"; 3 | import { PrimaryField } from "./primary-field.type"; 4 | import { RelationField } from "./relation-field.type"; 5 | 6 | export type EntityField = 7 | | PrimaryField 8 | | CommonField 9 | | RelationField; 10 | -------------------------------------------------------------------------------- /src/field/field-names/primary-field-possible.type.ts: -------------------------------------------------------------------------------- 1 | import { MatchedKeys } from "../../common/matched-keys.type"; 2 | import { AnyEntity } from "../../entity/any-entity.type"; 3 | import { PrimaryKeyPossible } from "../field-values/primary-key-possible.type"; 4 | import { EntityFieldBase } from "./entity-field-base.type"; 5 | 6 | export type PrimaryFieldPossible = Extract< 7 | EntityFieldBase, 8 | { 9 | [Type in PrimaryKeyPossible]: MatchedKeys; 10 | }[PrimaryKeyPossible] 11 | >; 12 | -------------------------------------------------------------------------------- /src/field/field-names/primary-field.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { BaseEntity } from "../../entity/base-entity.class"; 3 | import { PrimaryFieldPossible } from "./primary-field-possible.type"; 4 | 5 | export type PrimaryField = Extract< 6 | Entity extends BaseEntity ? Primary : never, 7 | PrimaryFieldPossible 8 | >; 9 | -------------------------------------------------------------------------------- /src/field/field-names/relation-field-to-many.type.ts: -------------------------------------------------------------------------------- 1 | import { MatchedKeys } from "../../common/matched-keys.type"; 2 | import { AnyEntity } from "../../entity/any-entity.type"; 3 | import { Collection } from "../field-values/collection.class"; 4 | import { EntityFieldBase } from "./entity-field-base.type"; 5 | 6 | export type RelationFieldToMany = Extract< 7 | EntityFieldBase, 8 | MatchedKeys> 9 | >; 10 | -------------------------------------------------------------------------------- /src/field/field-names/relation-field-to-one.type.ts: -------------------------------------------------------------------------------- 1 | import { MatchedKeys } from "../../common/matched-keys.type"; 2 | import { AnyEntity } from "../../entity/any-entity.type"; 3 | import { EmptyValue } from "../field-values/empty-value.type"; 4 | import { EntityFieldBase } from "./entity-field-base.type"; 5 | 6 | export type RelationFieldToOne = Extract< 7 | EntityFieldBase, 8 | MatchedKeys 9 | >; 10 | -------------------------------------------------------------------------------- /src/field/field-names/relation-field.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../.."; 2 | import { RelationFieldToMany } from "./relation-field-to-many.type"; 3 | import { RelationFieldToOne } from "./relation-field-to-one.type"; 4 | 5 | export type RelationField = 6 | | RelationFieldToOne 7 | | RelationFieldToMany; 8 | -------------------------------------------------------------------------------- /src/field/field-values/collection.class.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../../core/berry-orm.class"; 2 | import { AnyEntity } from "../../entity/any-entity.type"; 3 | 4 | export class Collection extends Set { 5 | constructor( 6 | readonly orm: BerryOrm, 7 | readonly owner: AnyEntity, 8 | readonly field: string, 9 | ) { 10 | super(); 11 | } 12 | 13 | add(entity: Entity): this { 14 | // end up recursion 15 | if (!this.has(entity)) { 16 | super.add(entity); 17 | this.orm.erm.constructRelation(this.owner, this.field, entity); 18 | } 19 | return this; 20 | } 21 | 22 | delete(entity: Entity): boolean { 23 | // end up recursion 24 | if (this.has(entity)) { 25 | super.delete(entity); 26 | this.orm.erm.destructRelation(this.owner, this.field, entity); 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | clear(): void { 33 | this.orm.erm.clearRelations(this.owner, this.field); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/field/field-values/empty-value.type.ts: -------------------------------------------------------------------------------- 1 | export type EmptyValue = null | undefined; 2 | -------------------------------------------------------------------------------- /src/field/field-values/primary-key-possible.type.ts: -------------------------------------------------------------------------------- 1 | export type PrimaryKeyPossible = string | number; 2 | -------------------------------------------------------------------------------- /src/field/field-values/primary-key.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { PrimaryField } from "../field-names/primary-field.type"; 3 | import { PrimaryKeyPossible } from "./primary-key-possible.type"; 4 | 5 | export type PrimaryKey = Extract< 6 | Entity[PrimaryField], 7 | PrimaryKeyPossible 8 | >; 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { MatchedKeys } from "./common/matched-keys.type"; 2 | export { UnmatchedKeys } from "./common/unmatched-keys.type"; 3 | export { BerryOrm } from "./core/berry-orm.class"; 4 | export { EntityManager } from "./core/entity-manager.class"; 5 | export { EntityManagerExportExpansions } from "./core/entity-manager-export-expansions.type"; 6 | export { EntityManagerExportExpansionsEmpty } from "./core/entity-manager-export-expansions-empty.type"; 7 | export { EntityRelationManager } from "./core/entity-relation-manager.class"; 8 | export { IdentityMap } from "./core/identity-map.class"; 9 | export { AnyEntity } from "./entity/any-entity.type"; 10 | export { BaseEntity } from "./entity/base-entity.class"; 11 | export { EntityData } from "./entity/entity-data/entity-data.type"; 12 | export { EntityDataCommon } from "./entity/entity-data/entity-data-common.type"; 13 | export { EntityDataExported } from "./entity/entity-data/entity-data-exported.type"; 14 | export { EntityDataRelational } from "./entity/entity-data/entity-data-relational.type"; 15 | export { EntityFromRelationFieldValue } from "./entity/entity-from-relation-field-value.type"; 16 | export { EntityRepresentation } from "./entity/entity-representation.type"; 17 | export { EntityType } from "./entity/entity-type.interface"; 18 | export { BaseFieldAccessor } from "./field/field-accessors/base-field.accessor"; 19 | export { CommonFieldAccessor } from "./field/field-accessors/common-field.accessor"; 20 | export { FieldAccessDeniedError } from "./field/field-accessors/field-access-denied.error"; 21 | export { PrimaryFieldAccessor } from "./field/field-accessors/primary-field.accessor"; 22 | export { RelationFieldToManyAccessor } from "./field/field-accessors/relation-field-to-many.accessor"; 23 | export { RelationFieldToOneAccessor } from "./field/field-accessors/relation-field-to-one.accessor"; 24 | export { RelationFieldData } from "./field/field-data/relation-field-data.type"; 25 | export { CommonField } from "./field/field-names/common-field.type"; 26 | export { EntityField } from "./field/field-names/entity-field.type"; 27 | export { EntityFieldBase } from "./field/field-names/entity-field-base.type"; 28 | export { PrimaryField } from "./field/field-names/primary-field.type"; 29 | export { PrimaryFieldPossible } from "./field/field-names/primary-field-possible.type"; 30 | export { RelationField } from "./field/field-names/relation-field.type"; 31 | export { RelationFieldToMany } from "./field/field-names/relation-field-to-many.type"; 32 | export { RelationFieldToOne } from "./field/field-names/relation-field-to-one.type"; 33 | export { Collection } from "./field/field-values/collection.class"; 34 | export { EmptyValue } from "./field/field-values/empty-value.type"; 35 | export { PrimaryKey } from "./field/field-values/primary-key.type"; 36 | export { PrimaryKeyPossible } from "./field/field-values/primary-key-possible.type"; 37 | export { Entity } from "./meta/meta-decorators/entity.decorator"; 38 | export { Field } from "./meta/meta-decorators/field.decorator"; 39 | export { Primary } from "./meta/meta-decorators/primary.decorator"; 40 | export { Relation } from "./meta/meta-decorators/relation.decorator"; 41 | export { EntityFieldMeta } from "./meta/meta-objects/entity-field-meta.class"; 42 | export { EntityMeta } from "./meta/meta-objects/entity-meta.class"; 43 | export { EntityRelationMeta } from "./meta/meta-objects/entity-relation-meta.class"; 44 | export { AbstractSerializer } from "./serializer/abstract.serializer"; 45 | export { DateSerializer } from "./serializer/built-in/date.serializer"; 46 | export { Scalar } from "./serializer/scalar.type"; 47 | export { NestedSerializerMap } from "./serializer/serializer-map/nested-serializer-map.type"; 48 | export { NestedSerializerMapEmpty } from "./serializer/serializer-map/nested-serializer-map-empty.type"; 49 | export { SerializerMap } from "./serializer/serializer-map/serializer-map.type"; 50 | export { SerializerMapEmpty } from "./serializer/serializer-map/serializer-map-empty.type"; 51 | export { SerializerType } from "./serializer/serializer-type.interface"; 52 | export { META, RESOLVED } from "./symbols"; 53 | -------------------------------------------------------------------------------- /src/meta/entity-meta.error.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from ".."; 2 | import { EntityType } from "../entity/entity-type.interface"; 3 | 4 | export class EntityMetaError extends Error { 5 | constructor({ 6 | type, 7 | field, 8 | message, 9 | }: { 10 | type: EntityType; 11 | field?: string; 12 | message: string; 13 | }) { 14 | super(); 15 | const prefix = field ? `[${type.name}:${field}]` : `[${type.name}]`; 16 | this.message = `${prefix} - ${message}`; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/__test__/entity.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "../../../entity/base-entity.class"; 2 | import { EntityType } from "../../../entity/entity-type.interface"; 3 | import { META } from "../../../symbols"; 4 | import { EntityMetaError } from "../../entity-meta.error"; 5 | import { EntityMeta } from "../../meta-objects/entity-meta.class"; 6 | import { Entity } from "../entity.decorator"; 7 | 8 | describe("@Entity()", () => { 9 | let cls: EntityType; 10 | 11 | beforeEach(() => { 12 | cls = class TestingEntity extends BaseEntity { 13 | id!: number; 14 | }; 15 | }); 16 | 17 | it("should throw an error when there are no metadata defined", () => { 18 | expect(() => { 19 | Entity()(cls); 20 | }).toThrow(EntityMetaError); 21 | }); 22 | 23 | it("should pass when meta is defined correctly", () => { 24 | const meta = new EntityMeta(cls); 25 | meta.primary = "id"; 26 | cls.prototype[META] = meta; 27 | Entity()(cls); 28 | }); 29 | 30 | it("should throw when applied for multiple times", () => { 31 | const meta = new EntityMeta(cls); 32 | meta.primary = "id"; 33 | cls.prototype[META] = meta; 34 | Entity()(cls); 35 | expect(() => { 36 | Entity()(cls); 37 | }).toThrow(EntityMetaError); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/__test__/field.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "../../../entity/base-entity.class"; 2 | import { EntityType } from "../../../entity/entity-type.interface"; 3 | import { META } from "../../../symbols"; 4 | import { EntityMetaError } from "../../entity-meta.error"; 5 | import { EntityFieldMeta } from "../../meta-objects/entity-field-meta.class"; 6 | import { EntityMeta } from "../../meta-objects/entity-meta.class"; 7 | import { Field } from "../field.decorator"; 8 | 9 | describe("@Field()", () => { 10 | let cls: EntityType; 11 | 12 | beforeEach(() => { 13 | cls = class TestingEntity extends BaseEntity { 14 | id!: number; 15 | }; 16 | }); 17 | 18 | it("should define the metadata of the entity", () => { 19 | Field()(cls.prototype, "id"); 20 | expect(cls.prototype[META]).toBeInstanceOf(EntityMeta); 21 | }); 22 | 23 | it("should define the metadata of the field", () => { 24 | Field()(cls.prototype, "id"); 25 | const meta = cls.prototype[META]?.fields.id; 26 | expect(meta).toBeInstanceOf(EntityFieldMeta); 27 | }); 28 | 29 | it("should throw an error when applied for multiple times", () => { 30 | Field()(cls.prototype, "id"); 31 | expect(() => { 32 | Field()(cls.prototype, "id"); 33 | }).toThrow(EntityMetaError); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/__test__/primary.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "../../../entity/base-entity.class"; 2 | import { EntityType } from "../../../entity/entity-type.interface"; 3 | import { META } from "../../../symbols"; 4 | import { EntityMetaError } from "../../entity-meta.error"; 5 | import { Field } from "../field.decorator"; 6 | import { Primary } from "../primary.decorator"; 7 | 8 | describe("@Primary()", () => { 9 | let cls: EntityType; 10 | 11 | beforeEach(() => { 12 | cls = class TestingEntity extends BaseEntity { 13 | id!: number; 14 | }; 15 | }); 16 | 17 | it("should throw an error when @Field() is not applied", () => { 18 | expect(() => { 19 | Primary()(cls.prototype, "id"); 20 | }).toThrowError(EntityMetaError); 21 | }); 22 | 23 | it("should set the primary field", () => { 24 | Field()(cls.prototype, "id"); 25 | Primary()(cls.prototype, "id"); 26 | expect(cls.prototype[META]?.primary).toBe("id"); 27 | }); 28 | 29 | it("should throw an error when it is applied for more than once", () => { 30 | Field()(cls.prototype, "id"); 31 | Primary()(cls.prototype, "id"); 32 | 33 | expect(() => { 34 | Primary()(cls.prototype, "id"); 35 | }).toThrow(EntityMetaError); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/__test__/relation.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "../../../entity/base-entity.class"; 2 | import { EntityType } from "../../../entity/entity-type.interface"; 3 | import { Collection } from "../../../field/field-values/collection.class"; 4 | import { EntityMetaError } from "../../entity-meta.error"; 5 | import { Field } from "../field.decorator"; 6 | import { Relation } from "../relation.decorator"; 7 | 8 | describe("@Relation()", () => { 9 | let cls: EntityType; 10 | 11 | beforeEach(() => { 12 | cls = class TestingEntity extends BaseEntity { 13 | id!: number; 14 | }; 15 | }); 16 | 17 | it("should throw an error when @Field() is not applied", () => { 18 | expect(() => { 19 | Relation({ target: () => cls, inverse: "" })(cls.prototype, "a"); 20 | }).toThrowError(EntityMetaError); 21 | }); 22 | 23 | it("should define the relation metadata", () => { 24 | Field()(cls.prototype, "a"); 25 | Relation({ target: () => cls, inverse: "" })(cls.prototype, "a"); 26 | }); 27 | 28 | it("should throw an error when applied for multiple times on a field", () => { 29 | Field()(cls.prototype, "a"); 30 | Relation({ target: () => cls, inverse: "" })(cls.prototype, "a"); 31 | 32 | expect(() => { 33 | Relation({ target: () => cls, inverse: "" })(cls.prototype, "a"); 34 | }).toThrow(EntityMetaError); 35 | }); 36 | 37 | describe("Type", () => { 38 | describe("ToOne", () => { 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 40 | class TestingEntity extends BaseEntity { 41 | id!: number; 42 | 43 | @Relation({ target: () => TestingEntity, inverse: "entity" }) 44 | @Field() 45 | entity!: TestingEntity; 46 | } 47 | }); 48 | 49 | describe("ToMany", () => { 50 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 51 | class TestingEntity extends BaseEntity { 52 | id!: number; 53 | 54 | @Relation({ 55 | target: () => TestingEntity, 56 | inverse: "entities", 57 | multi: true, 58 | }) 59 | @Field() 60 | entities!: Collection; 61 | } 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/entity.decorator.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityType } from "../../entity/entity-type.interface"; 3 | import { META } from "../../symbols"; 4 | import { EntityMetaError } from "../entity-meta.error"; 5 | import { EntityMeta } from "../meta-objects/entity-meta.class"; 6 | 7 | export const Entity = 8 | () => 9 | (type: EntityType): void => { 10 | const meta = type.prototype[META] as EntityMeta | undefined; 11 | 12 | if (!meta) 13 | throw new EntityMetaError({ 14 | type, 15 | message: 16 | "@Field() must be applied for at least once in each entity class", 17 | }); 18 | 19 | if (!meta.primary) 20 | throw new EntityMetaError({ 21 | type, 22 | message: "Must have a primary field registered", 23 | }); 24 | 25 | if (meta.completed) 26 | throw new EntityMetaError({ 27 | type, 28 | message: "@Entity() can be applied for only once to each entity", 29 | }); 30 | 31 | meta.completed = true; 32 | }; 33 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/field.decorator.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityType } from "../../entity/entity-type.interface"; 3 | import { EntityField } from "../../field/field-names/entity-field.type"; 4 | import { META } from "../../symbols"; 5 | import { EntityMetaError } from "../entity-meta.error"; 6 | import { EntityFieldMeta } from "../meta-objects/entity-field-meta.class"; 7 | import { EntityMeta } from "../meta-objects/entity-meta.class"; 8 | 9 | export const Field = 10 | () => 11 | ( 12 | prototype: Entity, 13 | field: EntityField, 14 | ): void => { 15 | const meta = (prototype[META] = 16 | prototype[META] ?? 17 | new EntityMeta(prototype.constructor as EntityType)); 18 | 19 | const fieldMeta = new EntityFieldMeta(field); 20 | 21 | if (meta.fields[field]) 22 | throw new EntityMetaError({ 23 | type: meta.type, 24 | field: field, 25 | message: "@Field() can be applied for only once on each field", 26 | }); 27 | 28 | meta.fields[field] = fieldMeta; 29 | }; 30 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/primary.decorator.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityType } from "../../entity/entity-type.interface"; 3 | import { PrimaryField } from "../../field/field-names/primary-field.type"; 4 | import { META } from "../../symbols"; 5 | import { EntityMetaError } from "../entity-meta.error"; 6 | import { EntityMeta } from "../meta-objects/entity-meta.class"; 7 | 8 | export const Primary = 9 | () => 10 | ( 11 | prototype: Entity, 12 | field: PrimaryField, 13 | ): void => { 14 | const meta = prototype[META] as EntityMeta | undefined; 15 | const type = prototype.constructor as EntityType; 16 | if (!meta?.fields[field]) 17 | throw new EntityMetaError({ 18 | type, 19 | field, 20 | message: "@Primary() must be applied after(above) @Field()", 21 | }); 22 | if (meta?.primary) 23 | throw new EntityMetaError({ 24 | type, 25 | field, 26 | message: "@Primary() can be applied for only once in each entity class", 27 | }); 28 | meta.primary = field; 29 | }; 30 | -------------------------------------------------------------------------------- /src/meta/meta-decorators/relation.decorator.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityType } from "../../entity/entity-type.interface"; 3 | import { RelationField } from "../../field/field-names/relation-field.type"; 4 | import { RelationFieldToMany } from "../../field/field-names/relation-field-to-many.type"; 5 | import { RelationFieldToOne } from "../../field/field-names/relation-field-to-one.type"; 6 | import { META } from "../../symbols"; 7 | import { EntityMetaError } from "../entity-meta.error"; 8 | import { EntityMeta } from "../meta-objects/entity-meta.class"; 9 | import { EntityRelationMeta } from "../meta-objects/entity-relation-meta.class"; 10 | 11 | export const Relation = 12 | (options: { 13 | target: () => EntityType; 14 | inverse: RelationField; 15 | multi?: Multi; 16 | }) => 17 | ( 18 | prototype: Entity, 19 | field: Multi extends true 20 | ? RelationFieldToMany 21 | : RelationFieldToOne, 22 | ): void => { 23 | const type = prototype.constructor as EntityType; 24 | const meta = prototype[META] as EntityMeta | undefined; 25 | 26 | if (!meta?.fields[field]) 27 | throw new EntityMetaError({ 28 | type, 29 | field, 30 | message: "@Relation() must be applied after(above) @Field()", 31 | }); 32 | 33 | if (meta?.fields[field].relation) 34 | throw new EntityMetaError({ 35 | type, 36 | field, 37 | message: "@Relation() can be applied for only once on each field", 38 | }); 39 | 40 | const { target, inverse, multi } = options; 41 | (meta.fields[field].relation as EntityRelationMeta) = 42 | new EntityRelationMeta(target, inverse, multi); 43 | }; 44 | -------------------------------------------------------------------------------- /src/meta/meta-objects/entity-field-meta.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityField } from "../../field/field-names/entity-field.type"; 3 | import { EntityRelationMeta } from "./entity-relation-meta.class"; 4 | 5 | export class EntityFieldMeta< 6 | Entity extends AnyEntity = AnyEntity, 7 | Field extends EntityField = EntityField, 8 | > { 9 | relation?: EntityRelationMeta; 10 | constructor(readonly name: Field) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/meta/meta-objects/entity-meta.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityType } from "../../entity/entity-type.interface"; 3 | import { EntityField } from "../../field/field-names/entity-field.type"; 4 | import { PrimaryField } from "../../field/field-names/primary-field.type"; 5 | import { PrimaryFieldPossible } from "../../field/field-names/primary-field-possible.type"; 6 | import { EntityFieldMeta } from "./entity-field-meta.class"; 7 | 8 | /** 9 | * The type parameter `Primary` is a must here, which is used to make the type 10 | * inference of the primary field work. 11 | */ 12 | export class EntityMeta< 13 | Entity extends AnyEntity = AnyEntity, 14 | Primary extends PrimaryFieldPossible = PrimaryField, 15 | > { 16 | primary!: Primary; 17 | fields = {} as Record, EntityFieldMeta>; 18 | completed = false; 19 | 20 | constructor(readonly type: EntityType) {} 21 | } 22 | -------------------------------------------------------------------------------- /src/meta/meta-objects/entity-relation-meta.class.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityType } from "../../entity/entity-type.interface"; 3 | import { RelationField } from "../../field/field-names/relation-field.type"; 4 | 5 | export class EntityRelationMeta { 6 | constructor( 7 | readonly target: () => EntityType, 8 | readonly inverse: RelationField, 9 | readonly multi = false, 10 | ) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/serializer/abstract.serializer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { Scalar } from "./scalar.type"; 4 | import { SerializerType } from "./serializer-type.interface"; 5 | 6 | export abstract class AbstractSerializer< 7 | Internal = unknown, 8 | External extends Scalar | Scalar[] = Scalar | Scalar[], 9 | > { 10 | protected orm; 11 | constructor(...[orm]: ConstructorParameters) { 12 | this.orm = orm; 13 | } 14 | abstract serialize(value: Internal): External; 15 | abstract deserialize(value: External | Internal): Internal; 16 | } 17 | -------------------------------------------------------------------------------- /src/serializer/built-in/date.serializer.ts: -------------------------------------------------------------------------------- 1 | import { AbstractSerializer } from "../abstract.serializer"; 2 | 3 | export class DateSerializer extends AbstractSerializer { 4 | serialize(value: Date): string { 5 | return value.toISOString(); 6 | } 7 | deserialize(value: string | Date): Date { 8 | return new Date(value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/serializer/scalar.type.ts: -------------------------------------------------------------------------------- 1 | export type Scalar = string | number | boolean | null | undefined; 2 | -------------------------------------------------------------------------------- /src/serializer/serializer-map/nested-serializer-map-empty.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { NestedSerializerMap } from "./nested-serializer-map.type"; 3 | 4 | export type NestedSerializerMapEmpty = Partial< 5 | Record, undefined> 6 | >; 7 | -------------------------------------------------------------------------------- /src/serializer/serializer-map/nested-serializer-map.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { EntityFromRelationFieldValue } from "../../entity/entity-from-relation-field-value.type"; 3 | import { RelationField } from "../../field/field-names/relation-field.type"; 4 | import { SerializerMap } from "./serializer-map.type"; 5 | 6 | export type NestedSerializerMap = 7 | SerializerMap & 8 | { 9 | [Field in RelationField]?: NestedSerializerMap< 10 | EntityFromRelationFieldValue 11 | >; 12 | }; 13 | -------------------------------------------------------------------------------- /src/serializer/serializer-map/serializer-map-empty.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { SerializerMap } from "./serializer-map.type"; 3 | 4 | export type SerializerMapEmpty = Partial< 5 | Record, undefined> 6 | >; 7 | -------------------------------------------------------------------------------- /src/serializer/serializer-map/serializer-map.type.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from "../../entity/any-entity.type"; 2 | import { CommonField } from "../../field/field-names/common-field.type"; 3 | import { PrimaryField } from "../../field/field-names/primary-field.type"; 4 | import { AbstractSerializer } from "../abstract.serializer"; 5 | import { SerializerType } from "../serializer-type.interface"; 6 | 7 | export type SerializerMap = { 8 | [Field in CommonField | PrimaryField]?: SerializerType< 9 | AbstractSerializer 10 | >; 11 | }; 12 | -------------------------------------------------------------------------------- /src/serializer/serializer-type.interface.ts: -------------------------------------------------------------------------------- 1 | import { BerryOrm } from "../core/berry-orm.class"; 2 | import { AbstractSerializer } from "./abstract.serializer"; 3 | 4 | export interface SerializerType { 5 | new (orm: BerryOrm): Serializer; 6 | prototype: Serializer; 7 | } 8 | -------------------------------------------------------------------------------- /src/symbols.ts: -------------------------------------------------------------------------------- 1 | /**Build description. */ 2 | const d = (s: string) => `berry-orm:${s}`; 3 | 4 | export const RESOLVED = Symbol(d("resolved")); 5 | export const META = Symbol(d("meta")); 6 | export const VERSION = Symbol(d("version")); 7 | -------------------------------------------------------------------------------- /src/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/__test__"] 4 | } 5 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../lib" 5 | }, 6 | "include": ["."] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "target": "ES2020", 5 | "module": "ES2020", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable", "ScriptHost"], 7 | "strict": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "experimentalDecorators": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "importHelpers": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": "./src/tsconfig.build.json" }], 3 | "files": [] 4 | } 5 | --------------------------------------------------------------------------------