├── .codeclimate.yml
├── .codesandbox
└── ci.json
├── .github
└── workflows
│ └── default.yml
├── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── Cache
│ ├── Cache.test.ts
│ ├── Cache.ts
│ ├── CacheMissingPrimaryColumnValueError.ts
│ ├── cacheDelete.ts
│ ├── cacheRead.ts
│ ├── cacheStabilize.ts
│ ├── cacheSync.ts
│ ├── cacheWrite.ts
│ └── optimizeInstructions.ts
├── Column
│ ├── Column.test.ts
│ ├── Column.ts
│ ├── IndexableColumn.test.ts
│ ├── IndexableColumn.ts
│ ├── PrimaryColumn.test.ts
│ ├── PrimaryColumn.ts
│ ├── UniqueColumn.test.ts
│ ├── UniqueColumn.ts
│ ├── columnGet.ts
│ ├── columnMetadata.ts
│ └── columnSet.ts
├── Datastore
│ ├── Datastore.ts
│ └── SearchStrategyError.ts
├── Entity
│ ├── Entity.test.ts
│ ├── Entity.ts
│ └── entityMetadata.ts
├── Instruction
│ ├── DeleteInstruction.test.ts
│ ├── DeleteInstruction.ts
│ ├── Instruction.ts
│ ├── WriteInstruction.test.ts
│ └── WriteInstruction.ts
├── MemoryDatastore
│ ├── MemoryDatastore.test.ts
│ └── MemoryDatastore.ts
├── Relationship
│ ├── ToMany.test.ts
│ ├── ToMany.ts
│ ├── ToOne.test.ts
│ ├── ToOne.ts
│ ├── addTo.ts
│ ├── relationshipMetadata.ts
│ ├── removeFrom.ts
│ ├── toManyChild.testhelpers.ts
│ ├── toManyGet.ts
│ ├── toManyParent.testhelpers.ts
│ ├── toManySet.ts
│ ├── toOneChild.testhelpers.ts
│ ├── toOneGet.ts
│ ├── toOneParent.testhelpers.ts
│ └── toOneSet.ts
├── Repository
│ ├── ColumnNotFindableError.ts
│ ├── ColumnNotSearchableError.ts
│ ├── EntityNotFoundError.ts
│ ├── Repository.test.ts
│ ├── Repository.ts
│ ├── RepositoryFindError.ts
│ ├── RepositoryLoadError.ts
│ ├── repositoryDelete.ts
│ ├── repositoryFind.ts
│ ├── repositoryLoad.ts
│ ├── repositorySave.ts
│ └── repositorySearch.ts
├── __tests__
│ └── library
│ │ ├── datastores
│ │ └── libraryDatastore.testhelpers.ts
│ │ ├── fixtures
│ │ └── williamShakespeare.testhelpers.ts
│ │ ├── index.test.ts
│ │ └── models
│ │ └── Author.testhelpers.ts
├── index.test.ts
├── index.ts
├── metadata.ts
├── types
│ ├── columnType.test.ts
│ ├── columnType.ts
│ ├── toManyType.ts
│ └── toOneType.ts
└── utils
│ ├── cache.ts
│ ├── columns.test.ts
│ ├── columns.ts
│ ├── datastore.test.ts
│ ├── datastore.ts
│ ├── entities.ts
│ ├── errors.ts
│ ├── hydrate.ts
│ ├── keyGeneration.test.ts
│ ├── keyGeneration.ts
│ ├── metadata.ts
│ ├── relationships.test.ts
│ └── relationships.ts
└── tsconfig.json
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | plugins:
3 | eslint:
4 | enabled: true
5 | fixme:
6 | enabled: true
7 | git-legal:
8 | enabled: true
9 | markdownlint:
10 | enabled: true
11 | checks:
12 | MD033:
13 | enabled: false
14 | MD013:
15 | enabled: false
16 | nodesecurity:
17 | enabled: true
18 | exclude_patterns:
19 | - "**/*.testhelpers.ts"
20 | - "**/*.test.ts"
21 | - "*.js"
22 |
--------------------------------------------------------------------------------
/.codesandbox/ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "sandboxes": ["github/gregbrimble/kv-orm-cf-workers-example"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/default.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push]
3 |
4 | jobs:
5 | build:
6 | name: Test
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 | - name: Setup Node
12 | uses: actions/setup-node@v1
13 | with:
14 | fetch-depth: 1
15 | - name: Install
16 | run: npm install
17 | - name: Lint
18 | run: npm run lint
19 | - name: Test
20 | run: npm run test:coverage
21 | - name: Upload Coverage Report
22 | uses: codecov/codecov-action@v1.0.6
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/git,node,macos,visualstudiocode
3 | # Edit at https://www.gitignore.io/?templates=git,node,macos,visualstudiocode
4 |
5 | ### Git ###
6 | # Created by git for backups. To disable backups in Git:
7 | # $ git config --global mergetool.keepBackup false
8 | *.orig
9 |
10 | # Created by git when using merge tools for conflicts
11 | *.BACKUP.*
12 | *.BASE.*
13 | *.LOCAL.*
14 | *.REMOTE.*
15 | *_BACKUP_*.txt
16 | *_BASE_*.txt
17 | *_LOCAL_*.txt
18 | *_REMOTE_*.txt
19 |
20 | ### macOS ###
21 | # General
22 | .DS_Store
23 | .AppleDouble
24 | .LSOverride
25 |
26 | # Icon must end with two \r
27 | Icon
28 |
29 | # Thumbnails
30 | ._*
31 |
32 | # Files that might appear in the root of a volume
33 | .DocumentRevisions-V100
34 | .fseventsd
35 | .Spotlight-V100
36 | .TemporaryItems
37 | .Trashes
38 | .VolumeIcon.icns
39 | .com.apple.timemachine.donotpresent
40 |
41 | # Directories potentially created on remote AFP share
42 | .AppleDB
43 | .AppleDesktop
44 | Network Trash Folder
45 | Temporary Items
46 | .apdisk
47 |
48 | ### Node ###
49 | # Logs
50 | logs
51 | *.log
52 | npm-debug.log*
53 | yarn-debug.log*
54 | yarn-error.log*
55 | lerna-debug.log*
56 |
57 | # Diagnostic reports (https://nodejs.org/api/report.html)
58 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
59 |
60 | # Runtime data
61 | pids
62 | *.pid
63 | *.seed
64 | *.pid.lock
65 |
66 | # Directory for instrumented libs generated by jscoverage/JSCover
67 | lib-cov
68 |
69 | # Coverage directory used by tools like istanbul
70 | coverage
71 | *.lcov
72 |
73 | # nyc test coverage
74 | .nyc_output
75 |
76 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
77 | .grunt
78 |
79 | # Bower dependency directory (https://bower.io/)
80 | bower_components
81 |
82 | # node-waf configuration
83 | .lock-wscript
84 |
85 | # Compiled binary addons (https://nodejs.org/api/addons.html)
86 | build/Release
87 |
88 | # Dependency directories
89 | node_modules/
90 | jspm_packages/
91 |
92 | # TypeScript v1 declaration files
93 | typings/
94 |
95 | # TypeScript cache
96 | *.tsbuildinfo
97 |
98 | # Optional npm cache directory
99 | .npm
100 |
101 | # Optional eslint cache
102 | .eslintcache
103 |
104 | # Optional REPL history
105 | .node_repl_history
106 |
107 | # Output of 'npm pack'
108 | *.tgz
109 |
110 | # Yarn Integrity file
111 | .yarn-integrity
112 |
113 | # dotenv environment variables file
114 | .env
115 | .env.test
116 |
117 | # parcel-bundler cache (https://parceljs.org/)
118 | .cache
119 |
120 | # next.js build output
121 | .next
122 |
123 | # nuxt.js build output
124 | .nuxt
125 |
126 | # vuepress build output
127 | .vuepress/dist
128 |
129 | # Serverless directories
130 | .serverless/
131 |
132 | # FuseBox cache
133 | .fusebox/
134 |
135 | # DynamoDB Local files
136 | .dynamodb/
137 |
138 | ### VisualStudioCode ###
139 | .vscode/*
140 | !.vscode/settings.json
141 | !.vscode/tasks.json
142 | !.vscode/launch.json
143 | !.vscode/extensions.json
144 |
145 | ### VisualStudioCode Patch ###
146 | # Ignore all local history of files
147 | .history
148 |
149 | # End of https://www.gitignore.io/api/git,node,macos,visualstudiocode
150 |
151 | dist/
152 | core
153 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v13.13.0
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Greg Brimble
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Welcome to @kv-orm/core 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | [kv-orm] is an Node.JS [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) for [key-value datastores](https://en.wikipedia.org/wiki/Key-value_database). **It is currently in beta**.
36 |
37 | ## Author
38 |
39 | 👤 **Greg Brimble**
40 |
41 | - Github: [@GregBrimble](https://github.com/GregBrimble)
42 | - Personal Website: [https://gregbrimble.com/](https://gregbrimble.com/)
43 |
44 | ## 🤝 Contributing
45 |
46 | Contributions, issues and feature requests are welcome! Feel free to check [issues page](https://github.com/kv-orm/core/issues).
47 |
48 | ## 😍 Show your support
49 |
50 | Please consider giving this project a ⭐️ if you use it, or if it provides some inspiration!
51 |
52 | # Supported Datastores
53 |
54 | - In-Memory
55 | - [Cloudflare Workers KV](https://github.com/kv-orm/cf-workers)
56 |
57 | If there is any other datastore that you'd like to see supported, please [create an issue](https://github.com/kv-orm/core/issues/new), or [make a pull request](https://github.com/kv-orm/core/fork).
58 |
59 | # Features
60 |
61 | - Support for multiple key-value datastores in a single application.
62 |
63 | ```typescript
64 | import { MemoryDatastore } from "@kv-orm/core";
65 |
66 | const libraryDatastore = new MemoryDatastore();
67 | const applicationSecrets = new MemoryDatastore();
68 | ```
69 |
70 | See above for the full list of [Supported Datastores](#Supported%20Datastores).
71 |
72 | - Easy construction of typed Entities using [Typescript](https://www.typescriptlang.org/).
73 |
74 | ```typescript
75 | import { Column, Entity } from "@kv-orm/core";
76 |
77 | @Entity({ datastore: libraryDatastore })
78 | class Author {
79 | @Column()
80 | public firstName: string;
81 |
82 | @Column()
83 | public lastName: string;
84 |
85 | // ...
86 | }
87 | ```
88 |
89 | - On-demand, asynchronous, lazy-loading: [kv-orm] won't load properties of an Entity until they're needed, and will do so seamlessly at the time of lookup.
90 |
91 | ```typescript
92 | import { getRepository } from "@kv-orm/core";
93 |
94 | const authorRepository = getRepository(Author);
95 |
96 | let author = await authorRepository.load("william@shakespeare.com"); // 1ms - no properties of the author have been loaded
97 |
98 | console.log(await author.firstName); // 60ms - author.firstName is fetched
99 | ```
100 |
101 | - No unnecessary reads: if a property is already in memory, [kv-orm] won't look it up again unless it needs to.
102 |
103 | ```typescript
104 | let author = await authorRepository.load("william@shakespeare.com");
105 |
106 | console.log(await author.lastName); // 60ms - author.lastName is fetched
107 | console.log(await author.lastName); // 1ms - author.lastName is retrieved from memory (no lookup performed)
108 | ```
109 |
110 | - Indexable and Unique Columns allowing quick lookups for specific entity instances:
111 |
112 | ```typescript
113 | import { UniqueColumn, IndexableColumn } from "@kv-orm/core";
114 |
115 | @Entity({ datastore: libraryDatastore })
116 | class Author {
117 | // ...
118 |
119 | @IndexableColumn()
120 | public birthYear: number;
121 |
122 | @UniqueColumn()
123 | public phoneNumber: string;
124 | // ...
125 | }
126 |
127 | let authors = await authorRepository.search("birthYear", 1564); // An AsyncGenerator yielding authors born in 1564
128 | let author = await authorRepository.find("phoneNumber", "+1234567890"); // A single author with the phone number +1234567890
129 | ```
130 |
131 | - Relationships (\*-to-one & \*-to-many) with backref support, and on-update & on-delete cascading.
132 |
133 | ```typescript
134 | import { ToOne, ToMany } from "@kv-orm/core";
135 |
136 | @Entity({ datastore: libraryDatastore })
137 | class Author {
138 | // ...
139 |
140 | @ToOne({ type: Book, backRef: "author", cascade: true })
141 | public books: Book[];
142 |
143 | // ...
144 | }
145 |
146 | @Entity({ datastore: libraryDatastore })
147 | class Book {
148 | @ToMany({ type: Author, backRef: "books", cascade: true })
149 | public author: Author;
150 |
151 | // ...
152 | }
153 | ```
154 |
155 | # Usage
156 |
157 | ## Install
158 |
159 | ```sh
160 | npm install --save @kv-orm/core
161 | ```
162 |
163 | ## Datastores
164 |
165 | ### `MemoryDatastore`
166 |
167 | `MemoryDatastore` is inbuilt into `@kv-orm/core`. It is a simple in-memory key-value datastore, and can be used for prototyping applications.
168 |
169 | ```typescript
170 | import { MemoryDatastore } from `@kv-orm/core`;
171 |
172 | const libraryDatastore = new MemoryDatastore();
173 | ```
174 |
175 | ### Cloudflare Workers KV
176 |
177 | See [`@kv-orm/cf-workers`](https://github.com/kv-orm/cf-workers) for more information.
178 |
179 | ## Entities
180 |
181 | An Entity is an object which stores data about something e.g. an Author. The Entity decorator takes a datastore to save the Entity instances into.
182 |
183 | Optionally, you can also pass in a `key` to the decorator, to rename the key name in the datastore.
184 |
185 | You can initialize a new instance of the Entity as normal.
186 |
187 | ```typescript
188 | import { Entity } from "@kv-orm/core";
189 |
190 | @Entity({ datastore: libraryDatastore, key: "Author" })
191 | class Author {
192 | // ...
193 | }
194 |
195 | const authorInstance = new Author();
196 | ```
197 |
198 | ## Columns
199 |
200 | Using the `@Column()` decorator on an Entity property is how you mark it as a savable property. You must `await` their value. This is because it might need to asynchronously query the datastore, if it doesn't have the value in memory.
201 |
202 | Like with Entities, you can optionally pass in a `key` to the decorator.
203 |
204 | ```typescript
205 | import {
206 | Column,
207 | PrimaryColumn,
208 | UniqueColumn,
209 | IndexableColumn,
210 | } from "@kv-orm/core";
211 | import { Book } from "./Book";
212 |
213 | @Entity({ datastore: libraryDatastore })
214 | class Author {
215 | @Column({ key: "givenName" })
216 | public firstName: string;
217 |
218 | @Column()
219 | public lastName: string;
220 |
221 | @Column()
222 | public nickName: string | undefined;
223 |
224 | @PrimaryColumn()
225 | public emailAddress: string;
226 |
227 | @IndexableColumn()
228 | public birthYear: number;
229 |
230 | @UniqueColumn()
231 | public phoneNumber: string;
232 |
233 | public someUnsavedProperty: any;
234 |
235 | @ToMany({ type: () => Book, backRef: "author", cascade: true }) // More on this later
236 | public books: Book[] = [];
237 |
238 | public constructor({
239 | firstName,
240 | lastName,
241 | emailAddress,
242 | birthYear,
243 | phoneNumber,
244 | }: {
245 | firstName: string;
246 | lastName: string;
247 | emailAddress: string;
248 | birthYear: number;
249 | phoneNumber: string;
250 | }) {
251 | this.firstName = firstName;
252 | this.lastName = lastName;
253 | this.emailAddress = emailAddress;
254 | this.birthYear = birthYear;
255 | this.phoneNumber = phoneNumber;
256 | }
257 | }
258 |
259 | const williamShakespeare = new Author({
260 | firstName: "William",
261 | lastName: "Shakespeare",
262 | emailAddress: "william@shakespeare.com",
263 | birthYear: 1564,
264 | phoneNumber: "+1234567890",
265 | });
266 | williamShakespeare.nickName = "Bill";
267 | williamShakespeare.someUnsavedProperty = "Won't get saved!";
268 |
269 | // When in an async function, you can fetch the value with `await`
270 | (async () => {
271 | console.log(await author.firstName);
272 | })();
273 | ```
274 |
275 | ### Primary Columns
276 |
277 | Any non-singleton class needs a PrimaryColumn used to differentiate Entity instances. For this reason, **PrimaryColumn values are required and must be unique**.
278 |
279 | ```typescript
280 | @Entity({ datastore: libraryDatastore })
281 | class Author {
282 | // ...
283 |
284 | @PrimaryColumn()
285 | public emailAddress: string;
286 |
287 | // ...
288 | }
289 | ```
290 |
291 | An example of a singleton class where you do not need a PrimaryColumn, might be a global configuration Entity where you store application secrets (e.g. API keys).
292 |
293 | ### Indexable Columns
294 |
295 | An IndexableColumn can be used to mark a property as one which you may wish to later lookup with. For example, in SQL, you might perform the following query: `SELECT * FROM Author WHERE birthYear = 1564`. In [kv-orm], you can lookup Entity instances with a given IndexableColumn value with a repository's [search](#Search) method
296 |
297 | ```typescript
298 | @Entity({ datastore: libraryDatastore })
299 | class Author {
300 | // ...
301 |
302 | @IndexableColumn()
303 | public birthYear: number;
304 |
305 | // ...
306 | }
307 | ```
308 |
309 | IndexableColumn types should be used to store non-unique values.
310 |
311 | ### Unique Columns
312 |
313 | Columns with unique values can be setup with UniqueColumn. This is more efficient that an IndexableColumn, and the [loading mechanism](#Find) is simpler.
314 |
315 | ```typescript
316 | @Entity({ datastore: libraryDatastore })
317 | class Author {
318 | // ...
319 |
320 | @UniqueColumn()
321 | public phoneNumber: number;
322 |
323 | // ...
324 | }
325 | ```
326 |
327 | ### Property Getters/Setters
328 |
329 | If your property is particularly complex (i.e. can't be stored natively in the datastore), you may wish to use a property getter/setter for a Column, to allow you to serialize it before saving in the datastore.
330 |
331 | For example, let's say you have a complex property on `Author`, `somethingComplex`:
332 |
333 | ```typescript
334 | @Entity({ datastore: libraryDatastore })
335 | class Author {
336 | // ...
337 |
338 | @Column()
339 | private _complex: string = ""; // place to store serialized value of somethingComplex
340 |
341 | set somethingComplex(value: any) {
342 | // function serialize(value: any): string
343 | this._complex = serialize(value);
344 | }
345 | get somethingComplex(): any {
346 | // function deserialize(serializedValue: string): any
347 | return (async () => deserialize(await this._complex))();
348 | }
349 |
350 | // ...
351 | }
352 | ```
353 |
354 | ## Repositories
355 |
356 | To actually interact with the datastore, you'll need a Repository.
357 |
358 | ```typescript
359 | import { getRepository } from "@kv-orm/core";
360 |
361 | const authorRepository = getRepository(Author);
362 | ```
363 |
364 | ### Save
365 |
366 | You can then save Entity instances.
367 |
368 | ```typescript
369 | const williamShakespeare = new Author({
370 | firstName: "William",
371 | lastName: "Shakespeare",
372 | emailAddress: "william@shakespeare.com",
373 | birthYear: 1564,
374 | phoneNumber: "+1234567890",
375 | });
376 |
377 | await authorRepository.save(williamShakepseare);
378 | ```
379 |
380 | ### Load
381 |
382 | And subsequently, load them back again. If the Entity has a PrimaryColumn, you can load the specific instance by passing in the PrimaryColumn value.
383 |
384 | ```typescript
385 | const loadedWilliamShakespeare = await authorRepository.load(
386 | "william@shakespeare.com"
387 | );
388 |
389 | console.log(await loadedWilliamShakespeare.nickName); // Bill
390 | ```
391 |
392 | ### Search
393 |
394 | If a property has been set as an IndexableColumn (is non-unique), you can search for instances with a saved value.
395 |
396 | ```typescript
397 | const searchedAuthors = await authorRepository.search("birthYear", 1564);
398 |
399 | for await (const searchedAuthor of searchedAuthors) {
400 | console.log(await searchedAuthor.nickName); // Bill
401 | }
402 | ```
403 |
404 | ### Find
405 |
406 | If a property has been set as a UniqueColumn, you can directly load an instance by a saved value. If no results are found, `null` is returned.
407 |
408 | ```typescript
409 | const foundWilliamShakespeare = await authorRepository.find(
410 | "phoneNumber",
411 | "+1234567890"
412 | );
413 |
414 | console.log(await foundWilliamShakespeare?.nickName); // Bill
415 |
416 | const foundNonexistent = await authorRepository.find(
417 | "phoneNumber",
418 | "+9999999999"
419 | );
420 |
421 | console.log(foundNonexistent); // null
422 | ```
423 |
424 | ## Relationships
425 |
426 | All Relationships must supply the `type` of Entity as a function (to allow circular dependencies) and a `backRef` (the property name on the inverse-side of the Relationship).
427 |
428 | In order to propagate changes automatically on update and on delete, `cascade` can be set to `true`.
429 |
430 | Note: deleting may be disproportionally intensive as it load, and then must edit or delete every related instance.
431 |
432 | ### One To One / Many To One
433 |
434 | For \* to one Relationships, use the ToOne decorator.
435 |
436 | ```typescript
437 | import { Author, authorRepository } from "./Author";
438 |
439 | @Entity({ datastore: libraryDatastore })
440 | class Book {
441 | // ...
442 |
443 | @ToOne({ type: () => Author, backRef: "books", cascade: true })
444 | public author: Author;
445 |
446 | // ...
447 | }
448 |
449 | const bookRepository = getRepository(Book);
450 |
451 | const williamShakespeare = await authorRepository.load(
452 | "william@shakespeare.com"
453 | );
454 |
455 | const hamlet = new Book({ title: "Hamlet", author: williamShakespeare });
456 |
457 | console.log((await hamlet.author) === williamShakespeare); // true
458 |
459 | // And because `cascade` was set to `true`, the Author instance has been updated as well
460 | const books = await williamShakespeare.books;
461 | for await (const book of books) {
462 | console.log(await book.title); // Hamlet
463 | }
464 |
465 | await bookRepository.save(hamlet); // Will also save williamShakespeare
466 | ```
467 |
468 | ### One To Many / Many To Many
469 |
470 | ToMany Relationships are slightly different to how Columns and ToOne Relationships work. Instead of setting its value and awaiting a Promise of its value, ToMany Relationships set an array of values, and await a Promise of an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) which yields its values.
471 |
472 | ```typescript
473 | @Entity({ datastore: libraryDatastore })
474 | class Author {
475 | // ...
476 |
477 | @ToMany({ type: () => Book, backRef: "author", cascade: true }) // More on this later
478 | public books: Book[] = [];
479 |
480 | // ...
481 | }
482 |
483 | // ...
484 |
485 | const hamlet = new Book({ title: "Hamlet" });
486 |
487 | williamShakespeare.books = [hamlet];
488 |
489 | const books = await williamShakespeare.books;
490 | for await (const book of books) {
491 | console.log(await book.title); // Hamlet
492 | }
493 |
494 | // And because `cascade` was set to `true`, the Book instance has been updated as well
495 | console.log((await hamlet.author) === williamShakespeare); // true - because `cascade` is set to true
496 |
497 | await authorRepository.save(williamShakespeare); // Will also save hamlet
498 | ```
499 |
500 | Note: The order of returned Entities by the AsyncGenerator is not guaranteed. In fact, as items are loaded into memory, they will be pushed to the front to ensure tha other Entities are loaded only as needed.
501 |
502 | #### Helpers
503 |
504 | To simplify interacting with `ToMany` Relationships, some helpers are available.
505 |
506 | Note: saving must still be completed afterwards to persist changes.
507 |
508 | ##### `addTo`
509 |
510 | `addTo` simplifies pushing an element to a ToMany relaionship:
511 |
512 | ```typescript
513 | import { addTo } from "@kv-orm/core";
514 |
515 | addTo(williamShakespeare, "books", hamlet);
516 | ```
517 |
518 | ##### `removeFrom`
519 |
520 | `removeFrom` simplifies splicing an element from a ToMany relaionship:
521 |
522 | ```typescript
523 | import { removeFrom } from "@kv-orm/core";
524 |
525 | removeFrom(williamShakespeare, "books", hamlet);
526 | ```
527 |
528 | # Types
529 |
530 | ## `columnType`
531 |
532 | When defining an Entity in TypeScript, you must provide types for the properties. For example, `firstName` and `lastName` are set as `string`s:
533 |
534 | ```typescript
535 | import { Column, Entity } from "@kv-orm/core";
536 |
537 | @Entity({ datastore: libraryDatastore })
538 | class Author {
539 | @Column()
540 | public firstName: string;
541 |
542 | @Column()
543 | public lastName: string;
544 |
545 | // ...
546 | }
547 | ```
548 |
549 | However, when reading these values with kv-orm, in fact a `Promise` is returned. Simply switching these type definitions to `Promise` is not valid either, as writing values is done synchronously.
550 |
551 | Therefore, to improve the developer experience and prevent TypeScript errors such as `'await' has no effect on the type of this expression. ts(80007)` and `Property 'then' does not exist on type 'T'. ts(2339)`, when declaring the Entity, wrap the property types in the `columnType` helper. In the same example:
552 |
553 | ```typescript
554 | import { columnType } from "@kv-orm/core";
555 |
556 | @Entity({ datastore: libraryDatastore })
557 | class Author {
558 | @Column()
559 | public firstName: columnType;
560 |
561 | @Column()
562 | public lastName: columnType;
563 |
564 | // ...
565 | }
566 | ```
567 |
568 | `columnType` is simply an alias: `type columnType = T | Promise`.
569 |
570 | ## `toOneType`
571 |
572 | Similarly, for ToOne Relationships:
573 |
574 | ```typescript
575 | import { toOneType } from "@kv-orm/core";
576 |
577 | @Entity({ datastore: libraryDatastore })
578 | class Book {
579 | // ...
580 |
581 | @ToOne({ type: () => Author, backRef: "books", cascade: true })
582 | public author: toOneType;
583 |
584 | // ...
585 | }
586 | ```
587 |
588 | ## `toManyType`
589 |
590 | And ToMany Relationships:
591 |
592 | ```typescript
593 | import { toManyType } from "@kv-orm/core";
594 |
595 | @Entity({ datastore: libraryDatastore })
596 | class Author {
597 | // ...
598 |
599 | @ToMany({ type: () => Book, backRef: "author", cascade: true })
600 | public books: toManyType;
601 |
602 | // ...
603 | }
604 | ```
605 |
606 | # Development
607 |
608 | ## Clone and Install Dependencies
609 |
610 | ```sh
611 | git clone git@github.com:kv-orm/core.git
612 | npm install
613 | ```
614 |
615 | ## Run tests
616 |
617 | ```sh
618 | npm run lint # 'npm run lint:fix' will automatically fix most problems
619 | npm test
620 | ```
621 |
622 | ## 🚎 Roadmap
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 | ## 📝 License
633 |
634 | Copyright © 2019 [Greg Brimble](https://github.com/GregBrimble).
635 | This project is [MIT](https://github.com/kv-orm/core/blob/master/LICENSE) licensed.
636 |
637 | # FAQs
638 |
639 | ## My Entity keys are getting mangled when they are saved into the datastore
640 |
641 | If you're using a preprocessor that minifies class names, such as Babel, the class constructors names often get shortened. kv-orm will always use this class name, so, either disable minification in the preprocessor, or manually set the `key` value when creating an Entity e.g.
642 |
643 | ```typescript
644 | @Entity({ key: "MyClass" })
645 | class MyClass {
646 | // ...
647 | }
648 | ```
649 |
650 | ## How can I upgrade from the alpha (0.0.X)?
651 |
652 | Thank you for trying out kv-orm in it's alpha period! Thanks to a generous sponsor, I have been able to complete work to elevate [kv-orm] to a more featureful beta. Unfortunately, this has meant a couple of minor breaking changes.
653 |
654 | - [`PrimaryColumn`](#Primary-Column), [`IndexableColumn`](#Indexable-Column) and [`UniqueColumn`](#Unique-Column) have been introduced to deprecate the `isPrimary`, `isIndexable` and `isUnique` options on the `Column` decorator.
655 |
656 | - `Repository`'s `find` method has been renamted to [`search`](#Search), and a different, new function [`find`](#Find) been added.
657 |
658 | This is probably the most confusing and frustrating breaking change (apologies—I will take efforts to make sure this doesn't happen again). With the introduction of `UniqueColumn` as a more specific type of `IndexableColumn`, we needed a way to take advantage of its simpler loading mechanism. Since `UniqueColumn` has unique-values, there should be only ever one instance with a given value (or none), and so `find` seemed a more appropriate verb for its loading. Whereas `IndexableColumn` can have non-unique values, `search` seemed more appropriate a verb when returning an `AsyncGenerator` of instances.
659 |
660 | - [`ToOne`](#To-One) & [`ToMany`](#To-Many) have been formally introduced (renamed from their drafted `ManyToOne` & `ManyToMany` names—the parent's cardinality does not matter).
661 |
662 | - Various bugfixes and improvements. None should have unexpected breaking changes, but if I've missed a use-case, please file a [GitHub Issue](https://github.com/kv-orm/core/issues) and tell me about it.
663 |
664 | [kv-orm]: https://github.com/kv-orm/core
665 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: `ts-jest`,
3 | rootDir: `src`,
4 | coverageDirectory: `../coverage`,
5 | testEnvironment: `node`,
6 | collectCoverageFrom: [
7 | '**/*.{js,ts}',
8 | '!**/node_modules/**',
9 | '!**/*.testhelpers.{js,ts}',
10 | ],
11 | testMatch: [
12 | '**/__tests__/**/*.[jt]s?(x)',
13 | '**/?(*.)+(spec|test).[jt]s?(x)',
14 | '!**/*.testhelpers.ts',
15 | ],
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kv-orm/core",
3 | "version": "0.1.1",
4 | "description": "A Node.js ORM for key-value datastores",
5 | "main": "dist/index",
6 | "types": "dist/index",
7 | "files": [
8 | "dist/**/*"
9 | ],
10 | "scripts": {
11 | "test": "jest",
12 | "test:coverage": "npm run test -- --coverage",
13 | "clean": "rm -rf dist/",
14 | "clean:node": "rm -rf package-lock.json node_modules/",
15 | "build": "tsc",
16 | "lint": "prettier -c 'src/' 'README.md'",
17 | "lint:fix": "prettier --write 'src/' 'README.md'"
18 | },
19 | "publishConfig": {
20 | "registry": "https://npm.pkg.github.com/"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git@github.com:kv-orm/core.git"
25 | },
26 | "keywords": [
27 | "kv",
28 | "key-value",
29 | "orm",
30 | "model",
31 | "nodejs",
32 | "javascript"
33 | ],
34 | "author": {
35 | "name": "Greg Brimble",
36 | "email": "developer@gregbrimble.com",
37 | "url": "https://gregbrimble.com"
38 | },
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/kv-orm/core/issues"
42 | },
43 | "homepage": "https://github.com/kv-orm/core#readme",
44 | "devDependencies": {
45 | "@babel/core": "7.9.0",
46 | "@babel/preset-env": "7.9.5",
47 | "@types/jest": "25.2.1",
48 | "@types/node": "13.13.4",
49 | "babel-jest": "25.3.0",
50 | "codecov": "3.6.5",
51 | "husky": "^4.2.5",
52 | "jest": "25.3.0",
53 | "prettier": "2.0.4",
54 | "ts-jest": "25.3.1",
55 | "typescript": "3.8.3"
56 | },
57 | "dependencies": {
58 | "reflect-metadata": "0.1.13"
59 | },
60 | "husky": {
61 | "hooks": {
62 | "pre-commit": "npm run lint || (npm run lint:fix && npm run lint)",
63 | "pre-push": "npm test"
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Cache/Cache.test.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { Datastore } from "../Datastore/Datastore";
3 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
4 | import { Entity, BaseEntity } from "../Entity/Entity";
5 | import { Column } from "../Column/Column";
6 | import { CacheMissingPrimaryColumnValueError } from "./CacheMissingPrimaryColumnValueError";
7 |
8 | describe(`Cache`, () => {
9 | let cache: Cache;
10 | let datastore: Datastore;
11 | let instance: BaseEntity;
12 |
13 | beforeEach(() => {
14 | cache = new Cache();
15 | datastore = new MemoryDatastore();
16 |
17 | @Entity({ datastore })
18 | class MyEntity {
19 | @Column()
20 | public myProperty = `initial value`;
21 |
22 | @Column({ isPrimary: true })
23 | public id: string;
24 |
25 | constructor(id: string) {
26 | this.id = id;
27 | }
28 | }
29 |
30 | instance = new MyEntity(`abc`);
31 | });
32 |
33 | it(`can be written to, read from, and elements can be deleted`, async () => {
34 | cache.write(instance, () => `key`, `value`);
35 | expect(await cache.read(instance, `key`)).toEqual(`value`);
36 | cache.delete(instance, () => `key`);
37 | expect(await cache.read(instance, `key`)).toBeNull();
38 | });
39 |
40 | describe(`CacheMissingPrimaryColumnValueError`, () => {
41 | it(`is thrown when reading an, as yet, unset PrimaryColumn value`, () => {
42 | expect(() => {
43 | cache.getPrimaryColumnValue({});
44 | }).toThrow(CacheMissingPrimaryColumnValueError);
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/Cache/Cache.ts:
--------------------------------------------------------------------------------
1 | import { Key, Value } from "../Datastore/Datastore";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { Instruction } from "../Instruction/Instruction";
4 | import { cacheWrite } from "./cacheWrite";
5 | import { cacheDelete } from "./cacheDelete";
6 | import { cacheRead } from "./cacheRead";
7 | import { cacheSync } from "./cacheSync";
8 | import { CacheMissingPrimaryColumnValueError } from "./CacheMissingPrimaryColumnValueError";
9 |
10 | export class Cache {
11 | public instructions = new Map();
12 | public data = new Map>();
13 | private primaryColumnValues = new Map();
14 |
15 | public recordInstruction(
16 | instance: BaseEntity,
17 | instruction: Instruction
18 | ): void {
19 | const instructions = this.instructions.get(instance) || [];
20 | instructions.push(instruction);
21 | this.instructions.set(instance, instructions);
22 | }
23 |
24 | public async sync(instance: BaseEntity): Promise {
25 | return cacheSync(this, instance);
26 | }
27 |
28 | public async read(instance: BaseEntity, key: Key): Promise {
29 | return cacheRead(this, instance, key);
30 | }
31 |
32 | public write(
33 | instance: BaseEntity,
34 | keyGenerator: () => Key,
35 | value: Value
36 | ): void {
37 | return cacheWrite(this, instance, keyGenerator, value);
38 | }
39 |
40 | public delete(instance: BaseEntity, keyGenerator: () => Key): void {
41 | return cacheDelete(this, instance, keyGenerator);
42 | }
43 |
44 | public getPrimaryColumnValue(
45 | instance: BaseEntity,
46 | { failSilently } = { failSilently: false }
47 | ): Value {
48 | const value = this.primaryColumnValues.get(instance);
49 | if (value === undefined && !failSilently)
50 | throw new CacheMissingPrimaryColumnValueError(instance);
51 | return value;
52 | }
53 |
54 | public setPrimaryColumnValue(instance: BaseEntity, value: Value): void {
55 | this.primaryColumnValues.set(instance, value);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Cache/CacheMissingPrimaryColumnValueError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { getConstructor } from "../utils/entities";
4 |
5 | export class CacheMissingPrimaryColumnValueError extends KVORMError {
6 | constructor(instance: BaseEntity) {
7 | super(
8 | `Could not find the value of the PrimaryColumn in the cache for an Entity, ${
9 | getConstructor(instance).name
10 | }`
11 | );
12 | this.name = `CacheMissingPrimaryColumnValueError`;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Cache/cacheDelete.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { Key } from "../Datastore/Datastore";
4 | import { DeleteInstruction } from "../Instruction/DeleteInstruction";
5 |
6 | export const cacheDelete = (
7 | cache: Cache,
8 | instance: BaseEntity,
9 | keyGenerator: () => Key
10 | ): void =>
11 | cache.recordInstruction(instance, new DeleteInstruction(keyGenerator));
12 |
--------------------------------------------------------------------------------
/src/Cache/cacheRead.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { Key, Value } from "../Datastore/Datastore";
4 | import { getConstructor } from "../utils/entities";
5 | import { getDatastore } from "../utils/datastore";
6 | import { cacheStabilize } from "./cacheStabilize";
7 |
8 | export const cacheRead = async (
9 | cache: Cache,
10 | instance: BaseEntity,
11 | key: Key
12 | ): Promise => {
13 | await cacheStabilize(cache, instance);
14 | const data = cache.data.get(instance) || new Map();
15 | let value = data.get(key) || null;
16 | if (value !== null) return value;
17 |
18 | const constructor = getConstructor(instance);
19 | const datastore = getDatastore(constructor);
20 | value = await datastore.read(key);
21 | data.set(key, value);
22 | cache.data.set(instance, data);
23 | return value;
24 | };
25 |
--------------------------------------------------------------------------------
/src/Cache/cacheStabilize.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { optimizeInstructions } from "./optimizeInstructions";
4 | import { Value, Key } from "../Datastore/Datastore";
5 |
6 | export const cacheStabilize = (
7 | cache: Cache,
8 | instance: BaseEntity
9 | ): Promise => {
10 | const instructions = optimizeInstructions(cache, instance);
11 |
12 | if (instructions.length === 0) return Promise.resolve(false);
13 |
14 | const data = cache.data.get(instance) || new Map();
15 |
16 | for (const instruction of instructions) {
17 | data.set(instruction.key, instruction.value);
18 | }
19 |
20 | cache.data.set(instance, data);
21 | return Promise.resolve(true);
22 | };
23 |
--------------------------------------------------------------------------------
/src/Cache/cacheSync.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { optimizeInstructions } from "./optimizeInstructions";
4 | import { getDatastore } from "../utils/datastore";
5 | import { getConstructor } from "../utils/entities";
6 |
7 | export const cacheSync = async (
8 | cache: Cache,
9 | instance: BaseEntity
10 | ): Promise => {
11 | const constructor = getConstructor(instance);
12 | const datastore = getDatastore(constructor);
13 | const instructions = optimizeInstructions(cache, instance);
14 |
15 | if (instructions.length === 0) return Promise.resolve(false);
16 |
17 | for (const instruction of instructions) {
18 | await instruction.perform(datastore);
19 | }
20 |
21 | cache.instructions.set(instance, []);
22 | return Promise.resolve(true);
23 | };
24 |
--------------------------------------------------------------------------------
/src/Cache/cacheWrite.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { Key, Value } from "../Datastore/Datastore";
4 | import { WriteInstruction } from "../Instruction/WriteInstruction";
5 |
6 | export const cacheWrite = (
7 | cache: Cache,
8 | instance: BaseEntity,
9 | keyGenerator: () => Key,
10 | value: Value
11 | ): void =>
12 | cache.recordInstruction(instance, new WriteInstruction(keyGenerator, value));
13 |
--------------------------------------------------------------------------------
/src/Cache/optimizeInstructions.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "./Cache";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import { Instruction } from "../Instruction/Instruction";
4 |
5 | export const optimizeInstructions = (
6 | cache: Cache,
7 | instance: BaseEntity
8 | ): Instruction[] => {
9 | let instructions = cache.instructions.get(instance) || [];
10 | instructions = [...instructions].reverse();
11 | const optimalInstructions = [];
12 | const seenKeys = new Set();
13 | for (const instruction of instructions) {
14 | const key = instruction.key;
15 | if (!seenKeys.has(key)) {
16 | optimalInstructions.push(instruction);
17 | seenKeys.add(key);
18 | }
19 | }
20 | optimalInstructions.reverse();
21 | cache.instructions.set(instance, optimalInstructions);
22 | return optimalInstructions;
23 | };
24 |
--------------------------------------------------------------------------------
/src/Column/Column.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity, BaseEntity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { Datastore } from "../Datastore/Datastore";
4 | import { Column } from "../Column/Column";
5 | import { PrimaryColumn } from "../Column/PrimaryColumn";
6 | import { MetadataSetupError } from "../utils/metadata";
7 |
8 | describe(`Column`, () => {
9 | let datastore: Datastore;
10 | let instance: BaseEntity;
11 | let otherInstance: BaseEntity;
12 |
13 | beforeEach(() => {
14 | datastore = new MemoryDatastore();
15 |
16 | @Entity({ datastore, key: `MyEntity` })
17 | class MyEntity {
18 | @Column({ key: `myProperty` })
19 | public myProperty = `initial value`;
20 |
21 | @PrimaryColumn()
22 | public id: string;
23 |
24 | constructor(id: string) {
25 | this.id = id;
26 | }
27 | }
28 |
29 | instance = new MyEntity(`abc`);
30 | otherInstance = new MyEntity(`def`);
31 | });
32 |
33 | it(`can be initialized with a default value`, async () => {
34 | expect(await instance.myProperty).toEqual(`initial value`);
35 | });
36 |
37 | it(`can be written to, and subsequently read from`, async () => {
38 | instance.myProperty = `new value`;
39 | expect(await instance.myProperty).toEqual(`new value`);
40 | expect(await otherInstance.myProperty).toEqual(`initial value`);
41 | });
42 |
43 | it(`can write and read an array value`, async () => {
44 | @Entity({ datastore })
45 | class EntityWithArrayColumn {
46 | @Column()
47 | public arrayColumn: number[];
48 |
49 | constructor(arrayValues: number[]) {
50 | this.arrayColumn = arrayValues;
51 | }
52 | }
53 |
54 | const values = [1, 2, 3, 4, 5];
55 | instance = new EntityWithArrayColumn(values);
56 |
57 | expect(await instance.arrayColumn).toEqual(values);
58 | });
59 |
60 | describe(`MetadataSetupError`, () => {
61 | it(`is thrown with a duplicate key`, () => {
62 | expect(() => {
63 | @Entity({ datastore, key: `MyOtherEntity` })
64 | class MyOtherEntity {
65 | @Column({ key: `myDuplicatedProperty` })
66 | public myProperty1 = `initial value`;
67 |
68 | @Column({ key: `myDuplicatedProperty` })
69 | public myProperty2 = `other initial value`;
70 | }
71 | }).toThrow(MetadataSetupError);
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/src/Column/Column.ts:
--------------------------------------------------------------------------------
1 | import "../metadata";
2 |
3 | import { Key } from "../Datastore/Datastore";
4 | import { BaseEntity, PropertyValue } from "../Entity/Entity";
5 | import { setColumnMetadata, getColumnMetadatas } from "../utils/columns";
6 | import { getConstructor } from "../utils/entities";
7 | import { columnGet } from "./columnGet";
8 | import { columnSet } from "./columnSet";
9 | import { createColumnMetadata } from "./columnMetadata";
10 | import { assertKeyNotInUse, getPropertyMetadatas } from "../utils/metadata";
11 |
12 | export const COLUMN_KEY = Symbol(`Column`);
13 |
14 | interface ColumnOptions {
15 | key?: Key;
16 | isPrimary?: boolean;
17 | isIndexable?: boolean;
18 | isUnique?: boolean;
19 | }
20 |
21 | export const Column = (options: ColumnOptions = {}) => {
22 | return (instance: T, property: keyof T): void => {
23 | const columnMetadata = createColumnMetadata({
24 | options,
25 | property,
26 | });
27 |
28 | const constructor = getConstructor(instance);
29 | assertKeyNotInUse(constructor, columnMetadata, {
30 | getMetadatas: getPropertyMetadatas,
31 | });
32 | setColumnMetadata(constructor, columnMetadata);
33 |
34 | Reflect.defineProperty(instance, property, {
35 | enumerable: true,
36 | configurable: true,
37 | get: async function get(this: BaseEntity) {
38 | return await columnGet(this, columnMetadata);
39 | },
40 | set: function set(this: BaseEntity, value: PropertyValue) {
41 | columnSet(this, columnMetadata, value);
42 | },
43 | });
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/src/Column/IndexableColumn.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity, BaseEntity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "./PrimaryColumn";
4 | import { IndexableColumn } from "./IndexableColumn";
5 |
6 | describe("UniqueColumn", () => {
7 | let instance: BaseEntity;
8 | beforeEach(() => {
9 | @Entity({ datastore: new MemoryDatastore() })
10 | class MyEntity {
11 | @PrimaryColumn()
12 | public id: string;
13 | @IndexableColumn()
14 | public indexableColumn: string;
15 | constructor(id: string, indexableColumn: string) {
16 | this.id = id;
17 | this.indexableColumn = indexableColumn;
18 | }
19 | }
20 | instance = new MyEntity("123", "indexableValue");
21 | });
22 | it("can be setup", async () => {
23 | expect(await instance.indexableColumn).toEqual("indexableValue");
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/Column/IndexableColumn.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "./Column";
2 | import { Key } from "../Datastore/Datastore";
3 |
4 | interface IndexableColumnOptions {
5 | key?: Key;
6 | }
7 |
8 | export const IndexableColumn = (options: IndexableColumnOptions = {}) =>
9 | Column({ ...options, isIndexable: true });
10 |
--------------------------------------------------------------------------------
/src/Column/PrimaryColumn.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity, BaseEntity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "./PrimaryColumn";
4 | import { ReadOnlyError } from "../utils/errors";
5 |
6 | describe("PrimaryColumn", () => {
7 | let instance: BaseEntity;
8 | beforeEach(() => {
9 | @Entity({ datastore: new MemoryDatastore() })
10 | class MyEntity {
11 | @PrimaryColumn()
12 | public id: string;
13 | constructor(id: string) {
14 | this.id = id;
15 | }
16 | }
17 | instance = new MyEntity("123");
18 | });
19 | it("can be setup", async () => {
20 | expect(await instance.id).toEqual("123");
21 | });
22 | describe(`ReadOnlyError`, () => {
23 | it(`is thrown when attempting to write to a PrimaryColumn twice`, () => {
24 | expect(() => {
25 | instance.id = `zzz`;
26 | }).toThrow(ReadOnlyError);
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/Column/PrimaryColumn.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "./Column";
2 | import { Key } from "../Datastore/Datastore";
3 |
4 | interface PrimaryColumnOptions {
5 | key?: Key;
6 | }
7 |
8 | export const PrimaryColumn = (options: PrimaryColumnOptions = {}) =>
9 | Column({ ...options, isPrimary: true });
10 |
--------------------------------------------------------------------------------
/src/Column/UniqueColumn.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity, BaseEntity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "./PrimaryColumn";
4 | import { UniqueColumn } from "./UniqueColumn";
5 |
6 | describe("UniqueColumn", () => {
7 | let instance: BaseEntity;
8 | beforeEach(() => {
9 | @Entity({ datastore: new MemoryDatastore() })
10 | class MyEntity {
11 | @PrimaryColumn()
12 | public id: string;
13 | @UniqueColumn()
14 | public uniqueProperty: string;
15 | constructor(id: string, uniqueProperty: string) {
16 | this.id = id;
17 | this.uniqueProperty = uniqueProperty;
18 | }
19 | }
20 | instance = new MyEntity("123", "uniqueValue");
21 | });
22 | it("can be setup", async () => {
23 | expect(await instance.uniqueProperty).toEqual("uniqueValue");
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/Column/UniqueColumn.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "./Column";
2 | import { Key } from "../Datastore/Datastore";
3 |
4 | interface UniqueColumnOptions {
5 | key?: Key;
6 | }
7 |
8 | export const UniqueColumn = (options: UniqueColumnOptions = {}) =>
9 | Column({ ...options, isIndexable: true, isUnique: true });
10 |
--------------------------------------------------------------------------------
/src/Column/columnGet.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { ColumnMetadata } from "./columnMetadata";
3 | import { Value } from "../Datastore/Datastore";
4 | import { getConstructorDatastoreCache } from "../utils/entities";
5 | import { generatePropertyKey } from "../utils/keyGeneration";
6 |
7 | export const columnGet = async (
8 | instance: BaseEntity,
9 | columnMetadata: ColumnMetadata
10 | ): Promise => {
11 | const { cache } = getConstructorDatastoreCache(instance);
12 |
13 | const key = generatePropertyKey(instance, columnMetadata);
14 |
15 | return await cache.read(instance, key);
16 | };
17 |
--------------------------------------------------------------------------------
/src/Column/columnMetadata.ts:
--------------------------------------------------------------------------------
1 | import { Key } from "../Datastore/Datastore";
2 | import { PropertyKey } from "../Entity/Entity";
3 | import { Metadata } from "../utils/metadata";
4 |
5 | export interface ColumnMetadata extends Metadata {
6 | key: Key;
7 | property: PropertyKey;
8 | isPrimary?: boolean;
9 | isIndexable?: boolean;
10 | isUnique?: boolean;
11 | }
12 |
13 | export const createColumnMetadata = ({
14 | options: { key, isIndexable, isPrimary, isUnique },
15 | property,
16 | }: {
17 | options: {
18 | key?: Key;
19 | isIndexable?: boolean;
20 | isPrimary?: boolean;
21 | isUnique?: boolean;
22 | };
23 | property: PropertyKey;
24 | }): ColumnMetadata => ({
25 | key: key || property.toString(),
26 | property,
27 | isIndexable: !!isPrimary || !!isIndexable,
28 | isPrimary: !!isPrimary,
29 | isUnique: !!isPrimary || !!isUnique,
30 | });
31 |
--------------------------------------------------------------------------------
/src/Column/columnSet.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { ColumnMetadata } from "./columnMetadata";
3 | import { Key, Value } from "../Datastore/Datastore";
4 | import { getConstructorDatastoreCache } from "../utils/entities";
5 | import {
6 | generatePropertyKey,
7 | generateIndexablePropertyKey,
8 | generateRelationshipKey,
9 | } from "../utils/keyGeneration";
10 | import { getPrimaryColumnValue, setPrimaryColumnValue } from "../utils/columns";
11 | import { ReadOnlyError } from "../utils/errors";
12 |
13 | export const columnSet = (
14 | instance: BaseEntity,
15 | columnMetadata: ColumnMetadata,
16 | value: Value
17 | ): void => {
18 | const { constructor, cache } = getConstructorDatastoreCache(instance);
19 |
20 | if (columnMetadata.isPrimary) {
21 | if (getPrimaryColumnValue(instance, { failSilently: true }) !== undefined) {
22 | throw new ReadOnlyError(
23 | constructor,
24 | columnMetadata.property,
25 | value,
26 | `PrimaryColumn Value has already been set`
27 | );
28 | }
29 | setPrimaryColumnValue(instance, value);
30 | }
31 |
32 | if (columnMetadata.isIndexable) {
33 | const indexableKeyGenerator = (): Key =>
34 | generateIndexablePropertyKey(instance, columnMetadata, value);
35 | const primaryColumnValue = generateRelationshipKey(instance);
36 | cache.write(instance, indexableKeyGenerator, primaryColumnValue);
37 | }
38 |
39 | const keyGenerator = (): Key => generatePropertyKey(instance, columnMetadata);
40 | cache.write(instance, keyGenerator, value);
41 | };
42 |
--------------------------------------------------------------------------------
/src/Datastore/Datastore.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "../Cache/Cache";
2 | import { SearchStrategyError } from "./SearchStrategyError";
3 | import { EntityConstructor } from "../Entity/Entity";
4 |
5 | export type Value = any;
6 | export type Key = string;
7 |
8 | interface DatastoreOptions {
9 | keySeparator?: string;
10 | cache?: Cache;
11 | }
12 |
13 | export type Cursor = string;
14 |
15 | export enum SearchStrategy {
16 | prefix,
17 | }
18 |
19 | export interface SearchOptions {
20 | term: Key;
21 | strategy: SearchStrategy;
22 | first?: number;
23 | after?: Cursor;
24 | }
25 |
26 | export interface SearchResult {
27 | keys: Key[];
28 | hasNextPage: boolean;
29 | cursor: Cursor;
30 | }
31 |
32 | export abstract class Datastore {
33 | public abstract searchStrategies: SearchStrategy[];
34 | public readonly keySeparator: Key;
35 | public readonly cache: Cache;
36 | public entityConstructors: EntityConstructor[] = [];
37 |
38 | protected abstract _read(key: Key): Promise | Value;
39 | protected abstract _write(key: Key, value: Value): Promise;
40 | protected abstract _delete(key: Key): Promise;
41 | protected abstract _search(options: SearchOptions): Promise;
42 |
43 | public read(key: Key): Promise {
44 | return this._read(key);
45 | }
46 |
47 | public write(key: Key, value: Value): Promise {
48 | return this._write(key, value);
49 | }
50 |
51 | public delete(key: Key): Promise {
52 | return this._delete(key);
53 | }
54 |
55 | public search(options: SearchOptions): Promise {
56 | this.assertSearchStrategyIsValid(options.strategy);
57 | return this._search(options);
58 | }
59 |
60 | protected assertSearchStrategyIsValid = (
61 | searchStrategy: SearchStrategy
62 | ): void => {
63 | if (!(searchStrategy in this.searchStrategies))
64 | throw new SearchStrategyError(
65 | searchStrategy,
66 | `Search Strategy is not implemented on this type of Datastore.`
67 | );
68 | };
69 |
70 | public constructor({
71 | keySeparator = `:`,
72 | cache = new Cache(),
73 | }: DatastoreOptions = {}) {
74 | this.keySeparator = keySeparator;
75 | this.cache = cache;
76 | }
77 |
78 | public registerEntity(constructor: EntityConstructor): void {
79 | this.entityConstructors.push(constructor);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Datastore/SearchStrategyError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { SearchStrategy } from "./Datastore";
3 |
4 | export class SearchStrategyError extends KVORMError {
5 | constructor(searchStrategy: SearchStrategy, message = `Unknown Error`) {
6 | super(
7 | `Error using Search Strategy, ${SearchStrategy[searchStrategy]}: ${message}`
8 | );
9 | this.name = `SearchStrategyError`;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Entity/Entity.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "./Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { Datastore } from "../Datastore/Datastore";
4 | import { MetadataSetupError } from "../utils/metadata";
5 |
6 | describe(`Entity`, () => {
7 | let datastore: Datastore;
8 |
9 | beforeEach(() => {
10 | datastore = new MemoryDatastore();
11 | });
12 |
13 | it(`can be initialized with a MemoryDatastore`, () => {
14 | @Entity({ datastore })
15 | class X {}
16 |
17 | const instance = new X();
18 |
19 | expect(instance).toBeInstanceOf(X);
20 | expect(instance.constructor).toBe(X);
21 | });
22 |
23 | describe(`MetadataSetupError`, () => {
24 | it(`is thrown when registering two Entities with the same Key`, () => {
25 | expect(() => {
26 | @Entity({ datastore })
27 | class X {}
28 |
29 | @Entity({ datastore, key: `X` })
30 | class Y {}
31 | }).toThrow(MetadataSetupError);
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/Entity/Entity.ts:
--------------------------------------------------------------------------------
1 | import "../metadata";
2 |
3 | import { Datastore, Key } from "../Datastore/Datastore";
4 | import { createEntityMetadata, EntityMetadata } from "./entityMetadata";
5 | import { setEntityMetadata, getEntityMetadata } from "../utils/entities";
6 | import { assertKeyNotInUse } from "../utils/metadata";
7 |
8 | export const ENTITY_KEY = Symbol(`Entity`);
9 |
10 | export type PropertyValue = any;
11 | export type PropertyKey = string | number | symbol;
12 |
13 | export type BaseEntity = Record;
14 |
15 | export type EntityConstructor = {
16 | new (...args: any[]): T;
17 | };
18 |
19 | interface EntityOptions {
20 | key?: Key;
21 | datastore: Datastore;
22 | }
23 |
24 | export function Entity({
25 | datastore,
26 | key,
27 | }: EntityOptions): (constructor: EntityConstructor) => any {
28 | return function (
29 | constructor: EntityConstructor
30 | ): EntityConstructor {
31 | const entityMetadata: EntityMetadata = createEntityMetadata({
32 | options: { datastore, key },
33 | constructor,
34 | });
35 |
36 | assertKeyNotInUse(constructor, entityMetadata, {
37 | getMetadatas: () => datastore.entityConstructors.map(getEntityMetadata),
38 | });
39 | datastore.registerEntity(constructor);
40 | setEntityMetadata(constructor, entityMetadata);
41 |
42 | return constructor;
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/Entity/entityMetadata.ts:
--------------------------------------------------------------------------------
1 | import { Datastore, Key } from "../Datastore/Datastore";
2 | import { BaseEntity, EntityConstructor } from "./Entity";
3 | import { Metadata } from "../utils/metadata";
4 |
5 | export interface EntityMetadata extends Metadata {
6 | datastore: Datastore;
7 | key: Key;
8 | }
9 |
10 | export const createEntityMetadata = ({
11 | options: { datastore, key },
12 | constructor,
13 | }: {
14 | options: { datastore: Datastore; key?: Key };
15 | constructor: EntityConstructor;
16 | }): EntityMetadata => ({
17 | datastore,
18 | key: key || constructor.name,
19 | });
20 |
--------------------------------------------------------------------------------
/src/Instruction/DeleteInstruction.test.ts:
--------------------------------------------------------------------------------
1 | import { DeleteInstruction } from "./DeleteInstruction";
2 | import { Key } from "../Datastore/Datastore";
3 |
4 | describe(`DeleteInstruction`, () => {
5 | let instruction: DeleteInstruction;
6 |
7 | beforeEach(() => {
8 | instruction = new DeleteInstruction((): Key => `key`);
9 | });
10 |
11 | it(`should generate keys`, async () => {
12 | expect(instruction.key).toEqual(`key`);
13 | });
14 |
15 | it(`should have a null value`, () => {
16 | expect(instruction.value).toBeNull();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/Instruction/DeleteInstruction.ts:
--------------------------------------------------------------------------------
1 | import { Key } from "../Datastore/Datastore";
2 | import { WriteInstruction } from "./WriteInstruction";
3 |
4 | export class DeleteInstruction extends WriteInstruction {
5 | public constructor(keyGenerator: () => Key) {
6 | super(keyGenerator, null);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Instruction/Instruction.ts:
--------------------------------------------------------------------------------
1 | import { Datastore, Key, Value } from "../Datastore/Datastore";
2 |
3 | export abstract class Instruction {
4 | protected abstract keyGenerator: () => Key;
5 | public abstract value: Value;
6 |
7 | public abstract async perform(datastore: Datastore): Promise;
8 |
9 | public get key(): Key {
10 | return this.keyGenerator();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Instruction/WriteInstruction.test.ts:
--------------------------------------------------------------------------------
1 | import { WriteInstruction } from "./WriteInstruction";
2 | import { Key } from "../Datastore/Datastore";
3 |
4 | describe(`WriteInstruction`, () => {
5 | let instruction: WriteInstruction;
6 |
7 | beforeEach(() => {
8 | instruction = new WriteInstruction((): Key => `key`, `value`);
9 | });
10 |
11 | it(`should generate keys`, async () => {
12 | expect(instruction.key).toEqual(`key`);
13 | });
14 |
15 | it(`should hold values`, () => {
16 | expect(instruction.value).toEqual(`value`);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/Instruction/WriteInstruction.ts:
--------------------------------------------------------------------------------
1 | import { Instruction } from "./Instruction";
2 | import { Datastore, Key, Value } from "../Datastore/Datastore";
3 |
4 | export class WriteInstruction extends Instruction {
5 | protected keyGenerator: () => Key;
6 | public value: Value;
7 |
8 | public constructor(keyGenerator: () => Key, value: Value) {
9 | super();
10 | this.keyGenerator = keyGenerator;
11 | this.value = value;
12 | }
13 |
14 | public async perform(datastore: Datastore): Promise {
15 | await datastore.write(this.key, this.value);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/MemoryDatastore/MemoryDatastore.test.ts:
--------------------------------------------------------------------------------
1 | import { MemoryDatastore } from "./MemoryDatastore";
2 | import { Datastore } from "../Datastore/Datastore";
3 | import { SearchStrategy } from "../Datastore/Datastore";
4 |
5 | const readWriteWorks = async (datastore: Datastore): Promise => {
6 | await datastore.write(`key`, `value`);
7 | return (await datastore.read(`key`)) === `value`;
8 | };
9 |
10 | describe(`MemoryDatastore`, () => {
11 | let datastore: Datastore;
12 |
13 | beforeEach(() => {
14 | datastore = new MemoryDatastore();
15 | });
16 |
17 | it(`can be initialized`, () => {
18 | expect(datastore).toBeInstanceOf(Datastore);
19 | expect(datastore).toBeInstanceOf(MemoryDatastore);
20 | });
21 |
22 | it(`can be written to, and subsequently read from`, async () => {
23 | expect(await readWriteWorks(datastore)).toBeTruthy();
24 | });
25 |
26 | it(`can delete key-values`, async () => {
27 | expect(await readWriteWorks(datastore)).toBeTruthy();
28 | await datastore.delete(`key`);
29 | expect(await datastore.read(`key`)).toBeNull();
30 | });
31 |
32 | describe(`search`, () => {
33 | const defaultSearchTerms = {
34 | term: `key`,
35 | strategy: SearchStrategy.prefix,
36 | };
37 |
38 | beforeEach(async () => {
39 | for (let i = 0; i < 2003; i++) {
40 | await datastore.write(`key${i}`, `value${i}`);
41 | }
42 | });
43 |
44 | it(`has a default limit of keys returned`, async () => {
45 | const results = await datastore.search(defaultSearchTerms);
46 | expect(results.keys.length).toBe(1000);
47 | expect(results.cursor).toBe(`999`);
48 | expect(results.hasNextPage).toBeTruthy();
49 | });
50 |
51 | it(`has limits the number of keys returned`, async () => {
52 | let results = await datastore.search({
53 | ...defaultSearchTerms,
54 | first: 2000,
55 | });
56 | expect(results.keys.length).toBe(1000);
57 | expect(results.cursor).toBe(`999`);
58 | expect(results.hasNextPage).toBeTruthy();
59 |
60 | results = await datastore.search({
61 | ...defaultSearchTerms,
62 | first: -99,
63 | });
64 | expect(results.keys.length).toBe(0);
65 | expect(results.cursor).toBe(`-1`);
66 | expect(results.hasNextPage).toBeTruthy();
67 | });
68 |
69 | it(`can paginate`, async () => {
70 | const results = await datastore.search({
71 | ...defaultSearchTerms,
72 | after: `3`,
73 | });
74 | expect(results.keys).not.toContain(`key3`);
75 | expect(results.keys).toContain(`key4`);
76 | expect(results.keys).toContain(`key1003`);
77 | expect(results.keys).not.toContain(`key1004`);
78 | expect(results.keys.length).toBe(1000);
79 | expect(results.cursor).toBe(`1003`);
80 | expect(results.hasNextPage).toBeTruthy();
81 | });
82 |
83 | it(`hits the end of pagination correctly`, async () => {
84 | const results = await datastore.search({
85 | ...defaultSearchTerms,
86 | after: `1999`,
87 | });
88 | expect(results.keys).toContain(`key2000`);
89 | expect(results.keys).toContain(`key2001`);
90 | expect(results.keys).toContain(`key2002`);
91 | expect(results.keys.length).toBe(3);
92 | expect(results.cursor).toBe(`2002`);
93 | expect(results.hasNextPage).toBeFalsy();
94 | });
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/src/MemoryDatastore/MemoryDatastore.ts:
--------------------------------------------------------------------------------
1 | import { Datastore, Value, Key } from "../Datastore/Datastore";
2 | import {
3 | SearchStrategy,
4 | SearchOptions,
5 | SearchResult,
6 | } from "../Datastore/Datastore";
7 |
8 | export class MemoryDatastore extends Datastore {
9 | private SEARCH_FIRST_LIMIT = 1000;
10 | private SEARCH_FIRST_DEFAULT = 1000;
11 | private data: Map = new Map(); // "remembers the original insertion order of the keys", https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
12 |
13 | public searchStrategies = [SearchStrategy.prefix];
14 |
15 | _read(key: Key): Promise {
16 | return Promise.resolve(this.data.get(key) || null);
17 | }
18 |
19 | _write(key: Key, value: Value): Promise {
20 | this.data.set(key, value);
21 | return Promise.resolve();
22 | }
23 |
24 | _delete(key: Key): Promise {
25 | this.data.delete(key);
26 | return Promise.resolve();
27 | }
28 |
29 | _search({
30 | term,
31 | first = this.SEARCH_FIRST_DEFAULT,
32 | after = `-1`,
33 | }: SearchOptions): Promise {
34 | if (first > this.SEARCH_FIRST_LIMIT) first = this.SEARCH_FIRST_LIMIT;
35 | if (first < 0) first = 0;
36 |
37 | let keys = Array.from(this.data.keys()).filter((key: Key) =>
38 | key.startsWith(term)
39 | );
40 |
41 | keys = keys.slice(+after + 1);
42 |
43 | const hasNextPage = keys.length > first;
44 |
45 | keys = keys.slice(0, first);
46 |
47 | const cursor = (+after + (hasNextPage ? first : keys.length)).toString();
48 |
49 | return Promise.resolve({
50 | keys,
51 | hasNextPage,
52 | cursor,
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Relationship/ToMany.test.ts:
--------------------------------------------------------------------------------
1 | import { Datastore, Value, SearchStrategy } from "../Datastore/Datastore";
2 | import { BaseEntity, Entity, EntityConstructor } from "../Entity/Entity";
3 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
4 | import { ToMany } from "./ToMany";
5 | import { Column } from "../Column/Column";
6 | import { getRepository, Repository } from "../Repository/Repository";
7 | import { SearchResult } from "../Datastore/Datastore";
8 | import { SearchStrategyError } from "../Datastore/SearchStrategyError";
9 | import { ToManyChild } from "./toManyChild.testhelpers";
10 | import { ToManyParent } from "./toManyParent.testhelpers";
11 | import { ToOneChild } from "./toOneChild.testhelpers";
12 | import { ToOneParent } from "./toOneParent.testhelpers";
13 |
14 | describe(`ToMany`, () => {
15 | let childInstance: BaseEntity;
16 | let childEntityConstructor: EntityConstructor;
17 | let otherChildInstance: BaseEntity;
18 | let parentInstance: BaseEntity;
19 |
20 | let childRepository: Repository;
21 | let parentRepository: Repository;
22 |
23 | beforeEach(async () => {
24 | childRepository = getRepository(ToOneChild);
25 | parentRepository = getRepository(ToOneParent);
26 |
27 | childInstance = new ToOneChild("child");
28 | otherChildInstance = new ToOneChild("child2");
29 | await childRepository.save(childInstance);
30 | parentInstance = new ToOneParent("parent");
31 | });
32 |
33 | // it(`can save and load a relationship to a non-singleton entity`, async () => {
34 | // parentInstance.myProperty = [childInstance, otherChildInstance];
35 | // await parentRepository.save(parentInstance);
36 |
37 | // const loadedRelations = await parentInstance.myProperty;
38 |
39 | // const expectedIDs = [`child`, "child2"];
40 | // let i = 0;
41 | // for await (const loadedRelation of loadedRelations) {
42 | // expect(await loadedRelation.id).toEqual(expectedIDs[i]);
43 | // i++;
44 | // }
45 | // expect(i).toBe(2);
46 | // });
47 |
48 | // it(`can save and load a relationship to a singleton entity`, async () => {
49 | // singletonParentInstance.myProperty = [singletonInstance, singletonInstance];
50 | // await singletonParentRepository.save(singletonParentInstance);
51 |
52 | // const loadedRelations = await singletonParentInstance.myProperty;
53 |
54 | // let i = 0;
55 | // for await (const loadedRelation of loadedRelations) {
56 | // expect(await loadedRelation.constantProperty).toEqual(`Never change!`);
57 | // i++;
58 | // }
59 | // expect(i).toBe(2);
60 | // });
61 |
62 | describe(`SearchStrategyError`, () => {
63 | class UselessDatastore extends Datastore {
64 | public searchStrategies = [];
65 |
66 | protected _read(): Promise {
67 | throw new Error(`Method not implemented.`);
68 | }
69 |
70 | protected _write(): Promise {
71 | throw new Error(`Method not implemented.`);
72 | }
73 |
74 | protected _delete(): Promise {
75 | throw new Error(`Method not implemented.`);
76 | }
77 |
78 | protected _search(): Promise {
79 | throw new Error(`Method not implemented.`);
80 | }
81 | }
82 |
83 | const uselessDatastore = new UselessDatastore();
84 |
85 | @Entity({ datastore: uselessDatastore })
86 | class UselessEntity {
87 | @ToMany({ type: () => childEntityConstructor, backRef: "" })
88 | relations = undefined;
89 | }
90 |
91 | const instance = new UselessEntity();
92 |
93 | it(`is thrown when using an unsupported Datastore`, async () => {
94 | await expect(
95 | (async (): Promise => {
96 | const relations = ((await instance.relations) as unknown) as {
97 | next: () => Promise;
98 | };
99 | await relations.next();
100 | })()
101 | ).rejects.toThrow(SearchStrategyError);
102 | });
103 |
104 | it(`is thrown wnen trying to manually search the datastore`, async () => {
105 | await expect(
106 | (async (): Promise => {
107 | await uselessDatastore.search({
108 | strategy: SearchStrategy.prefix,
109 | term: `test`,
110 | });
111 | })()
112 | ).rejects.toThrow(SearchStrategyError);
113 | });
114 | });
115 | });
116 |
117 | describe("backRefs on ToMany", () => {
118 | it("updates ToManyChild", async () => {
119 | const toManyChild = new ToManyChild("child");
120 | const toManyParent = new ToManyParent("parent");
121 | toManyParent.toManyChild = [toManyChild];
122 | await getRepository(ToManyParent).save(toManyParent);
123 |
124 | const relationChildInstances = await toManyParent.toManyChild;
125 | let i = 0;
126 | for await (const relationInstance of relationChildInstances) {
127 | expect(relationInstance).toBe(toManyChild);
128 | i++;
129 | }
130 | expect(i).toBe(1);
131 |
132 | const relationParentInstances = await toManyChild.toManyParent;
133 | i = 0;
134 | for await (const relationInstance of relationParentInstances) {
135 | expect(relationInstance).toBe(toManyParent);
136 | i++;
137 | }
138 | expect(i).toBe(1);
139 | });
140 |
141 | it("updates ToManyParent", async () => {
142 | const toManyChild = new ToManyChild("child");
143 | const toManyParent = new ToManyParent("parent");
144 | toManyChild.toManyParent = [toManyParent];
145 |
146 | const relationParentInstances = await toManyChild.toManyParent;
147 | let i = 0;
148 | for await (const relationInstance of relationParentInstances) {
149 | expect(relationInstance).toBe(toManyParent);
150 | i++;
151 | }
152 | expect(i).toBe(1);
153 |
154 | const relationChildInstances = await toManyParent.toManyChild;
155 | i = 0;
156 | for await (const relationInstance of relationChildInstances) {
157 | expect(relationInstance).toBe(toManyChild);
158 | i++;
159 | }
160 | expect(i).toBe(1);
161 | });
162 |
163 | it("updates ToOneChild", async () => {
164 | const toOneChild = new ToOneChild("child");
165 | const toManyParent = new ToManyParent("parent");
166 | toManyParent.toOneChild = toOneChild;
167 | expect(await toManyParent.toOneChild).toBe(toOneChild);
168 |
169 | const relationInstances = await toOneChild.toManyParent;
170 | let i = 0;
171 | for await (const relationInstance of relationInstances) {
172 | expect(relationInstance).toBe(toManyParent);
173 | i++;
174 | }
175 | expect(i).toBe(1);
176 | });
177 |
178 | it("updates ToOneParent", async () => {
179 | const toManyChild = new ToManyChild("child");
180 | const toOneParent = new ToOneParent("parent");
181 | toOneParent.toManyChild = [toManyChild];
182 | expect(await toManyChild.toOneParent).toBe(toOneParent);
183 |
184 | const relationInstances = await toOneParent.toManyChild;
185 | let i = 0;
186 | for await (const relationInstance of relationInstances) {
187 | expect(relationInstance).toBe(toManyChild);
188 | i++;
189 | }
190 | expect(i).toBe(1);
191 | });
192 | });
193 |
--------------------------------------------------------------------------------
/src/Relationship/ToMany.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, EntityConstructor } from "../Entity/Entity";
2 | import { PropertyKey } from "../Entity/Entity";
3 | import { Key } from "../Datastore/Datastore";
4 | import { getConstructor } from "../utils/entities";
5 | import { toManySet } from "./toManySet";
6 | import { createToManyRelationshipMetadata } from "./relationshipMetadata";
7 | import { setToManyRelationshipMetadata } from "../utils/relationships";
8 | import { toManyGet } from "./toManyGet";
9 | import { assertKeyNotInUse, getPropertyMetadatas } from "../utils/metadata";
10 |
11 | interface ToManyOptions {
12 | key?: Key;
13 | type: () => EntityConstructor;
14 | cascade?: boolean;
15 | backRef: PropertyKey;
16 | }
17 |
18 | export function ToMany(options: ToManyOptions) {
19 | return (instance: BaseEntity, property: PropertyKey): void => {
20 | const toManyRelationshipMetadata = createToManyRelationshipMetadata({
21 | options,
22 | property,
23 | });
24 |
25 | const constructor = getConstructor(instance);
26 | assertKeyNotInUse(constructor, toManyRelationshipMetadata, {
27 | getMetadatas: getPropertyMetadatas,
28 | });
29 | setToManyRelationshipMetadata(constructor, toManyRelationshipMetadata);
30 |
31 | Reflect.defineProperty(instance, property, {
32 | enumerable: true,
33 | configurable: true,
34 | get: function get(this: BaseEntity) {
35 | return toManyGet(this, toManyRelationshipMetadata);
36 | },
37 | set: function set(this: BaseEntity, values: BaseEntity[]) {
38 | if (values) toManySet(this, toManyRelationshipMetadata, values);
39 | },
40 | });
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/Relationship/ToOne.test.ts:
--------------------------------------------------------------------------------
1 | import { Datastore } from "../Datastore/Datastore";
2 | import { BaseEntity, Entity } from "../Entity/Entity";
3 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
4 | import { ToOne } from "./ToOne";
5 | import { Column } from "../Column/Column";
6 | import { getRepository, Repository } from "../Repository/Repository";
7 | import { ToOneChild } from "./toOneChild.testhelpers";
8 | import { ToOneParent } from "./toOneParent.testhelpers";
9 | import { ToManyParent } from "./toManyParent.testhelpers";
10 | import { ToManyChild } from "./toManyChild.testhelpers";
11 |
12 | describe(`ToOne`, () => {
13 | let childInstance: BaseEntity;
14 | let parentInstance: BaseEntity;
15 |
16 | let childRepository: Repository;
17 | let parentRepository: Repository;
18 |
19 | beforeEach(async () => {
20 | childRepository = getRepository(ToOneChild);
21 | parentRepository = getRepository(ToOneParent);
22 |
23 | childInstance = new ToOneChild("child");
24 | await childRepository.save(childInstance);
25 | parentInstance = new ToOneParent("parent");
26 | });
27 |
28 | it("returns the existing instance on get", async () => {
29 | parentInstance.myProperty = childInstance;
30 | expect(await parentInstance.myProperty).toBe(childInstance);
31 | });
32 |
33 | it(`can save and load a relationship to a non-singleton entity`, async () => {
34 | parentInstance.myProperty = childInstance;
35 | await parentRepository.save(parentInstance);
36 |
37 | const loadedRelation = await parentInstance.myProperty;
38 | expect(await loadedRelation.id).toEqual(await childInstance.id);
39 | });
40 | });
41 |
42 | describe("backRefs on ToOne", () => {
43 | it("updates ToOneChild", async () => {
44 | const toOneChild = new ToOneChild("child");
45 | const toOneParent = new ToOneParent("parent");
46 | toOneParent.toOneChild = toOneChild;
47 | expect(await toOneParent.toOneChild).toBe(toOneChild);
48 | expect(await toOneChild.toOneParent).toBe(toOneParent);
49 | });
50 |
51 | it("updates ToOneParent", async () => {
52 | const toOneChild = new ToOneChild("child");
53 | const toOneParent = new ToOneParent("parent");
54 | toOneChild.toOneParent = toOneParent;
55 | expect(await toOneChild.toOneParent).toBe(toOneParent);
56 | expect(await toOneParent.toOneChild).toBe(toOneChild);
57 | });
58 |
59 | it("updates ToManyChild", async () => {
60 | const toManyChild = new ToManyChild("child");
61 | const toOneParent = new ToOneParent("parent");
62 | toOneParent.toManyChild = [toManyChild];
63 | expect(await toManyChild.toOneParent).toBe(toOneParent);
64 |
65 | const relationInstances = await toOneParent.toManyChild;
66 | let i = 0;
67 | for await (const relationInstance of relationInstances) {
68 | expect(relationInstance).toBe(toManyChild);
69 | i++;
70 | }
71 | expect(i).toBe(1);
72 | });
73 |
74 | it("updates ToManyParent", async () => {
75 | const toOneChild = new ToOneChild("child");
76 | const toManyParent = new ToManyParent("parent");
77 | toManyParent.toOneChild = toOneChild;
78 | expect(await toManyParent.toOneChild).toBe(toOneChild);
79 |
80 | const relationInstances = await toOneChild.toManyParent;
81 | let i = 0;
82 | for await (const relationInstance of relationInstances) {
83 | expect(relationInstance).toBe(toManyParent);
84 | i++;
85 | }
86 | expect(i).toBe(1);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/src/Relationship/ToOne.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, EntityConstructor } from "../Entity/Entity";
2 | import { PropertyKey } from "../Entity/Entity";
3 | import { Key } from "../Datastore/Datastore";
4 | import { getConstructor } from "../utils/entities";
5 | import { toOneSet } from "./toOneSet";
6 | import {
7 | createToOneRelationshipMetadata,
8 | RelationshipOptions,
9 | } from "./relationshipMetadata";
10 | import { setToOneRelationshipMetadata } from "../utils/relationships";
11 | import { toOneGet } from "./toOneGet";
12 | import { assertKeyNotInUse, getPropertyMetadatas } from "../utils/metadata";
13 |
14 | interface ToOneOptions {
15 | key?: Key;
16 | type: () => EntityConstructor;
17 | cascade?: boolean;
18 | backRef: PropertyKey;
19 | }
20 | export function ToOne(options: ToOneOptions) {
21 | return (instance: BaseEntity, property: PropertyKey): void => {
22 | const toOneRelationshipMetadata = createToOneRelationshipMetadata({
23 | options,
24 | property,
25 | });
26 |
27 | const constructor = getConstructor(instance);
28 | assertKeyNotInUse(constructor, toOneRelationshipMetadata, {
29 | getMetadatas: getPropertyMetadatas,
30 | });
31 | setToOneRelationshipMetadata(constructor, toOneRelationshipMetadata);
32 |
33 | Reflect.defineProperty(instance, property, {
34 | enumerable: true,
35 | configurable: true,
36 | get: async function get(this: BaseEntity) {
37 | return toOneGet(this, toOneRelationshipMetadata);
38 | },
39 | set: function set(this: BaseEntity, value: BaseEntity) {
40 | if (value) toOneSet(this, toOneRelationshipMetadata, value);
41 | },
42 | });
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/Relationship/addTo.ts:
--------------------------------------------------------------------------------
1 | import { getConstructorDatastoreCache } from "../utils/entities";
2 | import { BaseEntity } from "../Entity/Entity";
3 | import {
4 | getToManyRelationshipMetadata,
5 | setToManyRelationshipMetadata,
6 | } from "../utils/relationships";
7 | import { Key } from "../Datastore/Datastore";
8 | import {
9 | generateManyRelationshipKey,
10 | generateRelationshipKey,
11 | } from "../utils/keyGeneration";
12 |
13 | export const addTo = (
14 | instance: BaseEntity,
15 | property: PropertyKey,
16 | value: BaseEntity
17 | ) => {
18 | const { constructor, cache } = getConstructorDatastoreCache(instance);
19 | const toManyRelationshipMetadata = getToManyRelationshipMetadata(
20 | constructor,
21 | property
22 | );
23 |
24 | const keyGenerator = (): Key =>
25 | generateManyRelationshipKey(instance, toManyRelationshipMetadata, value);
26 | cache.write(instance, keyGenerator, generateRelationshipKey(value));
27 |
28 | const cachedRelationshipInstances = [
29 | ...(toManyRelationshipMetadata.instances.get(instance) || []),
30 | value,
31 | ];
32 | toManyRelationshipMetadata.instances.set(
33 | instance,
34 | cachedRelationshipInstances
35 | );
36 | setToManyRelationshipMetadata(constructor, toManyRelationshipMetadata);
37 | };
38 |
--------------------------------------------------------------------------------
/src/Relationship/relationshipMetadata.ts:
--------------------------------------------------------------------------------
1 | import { Key } from "../Datastore/Datastore";
2 | import { PropertyKey, BaseEntity } from "../Entity/Entity";
3 | import { EntityConstructor } from "../Entity/Entity";
4 | import { Metadata } from "../utils/metadata";
5 |
6 | export const TOONE_RELATIONSHIP_KEY = Symbol(`ToOneRelationship`);
7 | export const TOMANY_RELATIONSHIP_KEY = Symbol(`ToManyRelationship`);
8 |
9 | export interface RelationshipMetadata extends Metadata {
10 | key: Key;
11 | property: PropertyKey;
12 | type: () => EntityConstructor;
13 | cascade: { onUpdate: boolean; onDelete: boolean };
14 | backRef: PropertyKey;
15 | cardinality: "ToOne" | "ToMany";
16 | }
17 |
18 | export interface ToOneRelationshipMetadata extends RelationshipMetadata {
19 | instance: Map;
20 | cardinality: "ToOne";
21 | }
22 |
23 | export interface ToManyRelationshipMetadata extends RelationshipMetadata {
24 | instances: Map;
25 | cardinality: "ToMany";
26 | }
27 |
28 | export interface RelationshipOptions {
29 | key?: Key;
30 | type: () => EntityConstructor;
31 | cascade?: boolean;
32 | backRef: PropertyKey;
33 | }
34 |
35 | export const createToOneRelationshipMetadata = ({
36 | options: { key, type, cascade, backRef },
37 | property,
38 | }: {
39 | options: RelationshipOptions;
40 | property: PropertyKey;
41 | }): ToOneRelationshipMetadata => ({
42 | key: key || property.toString(),
43 | property,
44 | type,
45 | cascade: cascade
46 | ? { onUpdate: true, onDelete: true }
47 | : { onUpdate: false, onDelete: false },
48 | backRef,
49 | instance: new Map(),
50 | cardinality: "ToOne",
51 | });
52 |
53 | export const createToManyRelationshipMetadata = ({
54 | options: { key, type, cascade, backRef },
55 | property,
56 | }: {
57 | options: RelationshipOptions;
58 | property: PropertyKey;
59 | }): ToManyRelationshipMetadata => ({
60 | key: key || property.toString(),
61 | property,
62 | type,
63 | cascade: cascade
64 | ? { onUpdate: true, onDelete: true }
65 | : { onUpdate: false, onDelete: false },
66 | backRef,
67 | instances: new Map(),
68 | cardinality: "ToMany",
69 | });
70 |
--------------------------------------------------------------------------------
/src/Relationship/removeFrom.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, PropertyKey } from "../Entity/Entity";
2 | import {
3 | getToManyRelationshipMetadata,
4 | setToManyRelationshipMetadata,
5 | } from "../utils/relationships";
6 | import { getConstructorDatastoreCache } from "../utils/entities";
7 | import { Key } from "../Datastore/Datastore";
8 | import { generateManyRelationshipKey } from "../utils/keyGeneration";
9 |
10 | export const removeFrom = (
11 | instance: BaseEntity,
12 | property: PropertyKey,
13 | value: BaseEntity
14 | ) => {
15 | const { constructor, cache } = getConstructorDatastoreCache(instance);
16 | const toManyRelationshipMetadata = getToManyRelationshipMetadata(
17 | constructor,
18 | property
19 | );
20 |
21 | const keyGenerator = (): Key =>
22 | generateManyRelationshipKey(instance, toManyRelationshipMetadata, value);
23 | cache.delete(instance, keyGenerator);
24 |
25 | const cachedRelationshipInstances =
26 | toManyRelationshipMetadata.instances.get(instance) || [];
27 | const indexOfInstance = cachedRelationshipInstances.indexOf(value);
28 | if (indexOfInstance !== -1) {
29 | cachedRelationshipInstances.splice(indexOfInstance, 1);
30 | toManyRelationshipMetadata.instances.set(
31 | instance,
32 | cachedRelationshipInstances
33 | );
34 | setToManyRelationshipMetadata(constructor, toManyRelationshipMetadata);
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/Relationship/toManyChild.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "../Column/PrimaryColumn";
4 | import { ToOne } from "./ToOne";
5 | import { ToOneParent } from "./toOneParent.testhelpers";
6 | import { ToManyParent } from "./toManyParent.testhelpers";
7 | import { toManyType } from "../types/toManyType";
8 | import { toOneType } from "../types/toOneType";
9 | import { ToMany } from "./ToMany";
10 |
11 | const datastore = new MemoryDatastore();
12 |
13 | @Entity({ datastore })
14 | class ToManyChild {
15 | @PrimaryColumn()
16 | public id: string;
17 |
18 | @ToOne({ type: () => ToOneParent, backRef: "toManyChild", cascade: true })
19 | public toOneParent?: toOneType = undefined;
20 |
21 | @ToMany({ type: () => ToManyParent, backRef: "toManyChild", cascade: true })
22 | public toManyParent: toManyType = [];
23 |
24 | constructor(id: string) {
25 | this.id = id;
26 | }
27 | }
28 |
29 | export { ToManyChild };
30 |
--------------------------------------------------------------------------------
/src/Relationship/toManyGet.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { getConstructor } from "../utils/entities";
3 | import { getDatastore } from "../utils/datastore";
4 | import { generateManyRelationshipSearchKey } from "../utils/keyGeneration";
5 | import { ToManyRelationshipMetadata } from "./relationshipMetadata";
6 | import { hydrateMany, getHydrator } from "../utils/hydrate";
7 | import {
8 | arrayToAsyncGenerator,
9 | makeCachingGenerator,
10 | combineAsyncGenerators,
11 | } from "../utils/relationships";
12 | import { getPrimaryColumnValue } from "../utils/columns";
13 |
14 | export const toManyGet = (
15 | instance: BaseEntity,
16 | toManyRelationshipMetadata: ToManyRelationshipMetadata
17 | ): AsyncGenerator => {
18 | const cachedRelationshipInstances =
19 | toManyRelationshipMetadata.instances.get(instance) || [];
20 | const primaryColumnValueInstances = cachedRelationshipInstances.map(
21 | (instance) => getPrimaryColumnValue(instance)
22 | );
23 |
24 | const constructor = getConstructor(instance);
25 | const datastore = getDatastore(constructor);
26 | const hydrator = getHydrator(toManyRelationshipMetadata.type());
27 |
28 | const searchKey = generateManyRelationshipSearchKey(
29 | instance,
30 | toManyRelationshipMetadata
31 | );
32 |
33 | const cachingGenerator = makeCachingGenerator(
34 | instance,
35 | toManyRelationshipMetadata
36 | );
37 |
38 | return combineAsyncGenerators(
39 | arrayToAsyncGenerator(cachedRelationshipInstances),
40 | cachingGenerator(
41 | hydrateMany(datastore, searchKey, hydrator, {
42 | skip: (identifier) => primaryColumnValueInstances.includes(identifier),
43 | })
44 | )
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/Relationship/toManyParent.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "../Column/PrimaryColumn";
4 | import { ToOne } from "./ToOne";
5 | import { ToMany } from "./ToMany";
6 | import { toManyType } from "../types/toManyType";
7 | import { toOneType } from "../types/toOneType";
8 | import { ToOneChild } from "./toOneChild.testhelpers";
9 | import { ToManyChild } from "./toManyChild.testhelpers";
10 |
11 | const datastore = new MemoryDatastore();
12 |
13 | @Entity({ datastore })
14 | class ToManyParent {
15 | @PrimaryColumn()
16 | public id: string;
17 |
18 | @ToOne({ type: () => ToOneChild, backRef: "toManyParent", cascade: true })
19 | public toOneChild?: toOneType = undefined;
20 |
21 | @ToMany({ type: () => ToManyChild, backRef: "toManyParent", cascade: true })
22 | public toManyChild: toManyType = [];
23 |
24 | constructor(id: string) {
25 | this.id = id;
26 | }
27 | }
28 |
29 | export { ToManyParent };
30 |
--------------------------------------------------------------------------------
/src/Relationship/toManySet.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { Key } from "../Datastore/Datastore";
3 | import { getConstructorDatastoreCache } from "../utils/entities";
4 | import {
5 | generateManyRelationshipKey,
6 | generateRelationshipKey,
7 | } from "../utils/keyGeneration";
8 | import {
9 | ToManyRelationshipMetadata,
10 | ToOneRelationshipMetadata,
11 | } from "./relationshipMetadata";
12 | import {
13 | setToManyRelationshipMetadata,
14 | getRelationshipMetadata,
15 | } from "../utils/relationships";
16 | import { toOneSet } from "./toOneSet";
17 | import { addTo } from "./addTo";
18 |
19 | export const toManySet = async (
20 | instance: BaseEntity,
21 | toManyRelationshipMetadata: ToManyRelationshipMetadata,
22 | values: BaseEntity[],
23 | { skipBackRef }: { skipBackRef: boolean } = { skipBackRef: false }
24 | ): Promise => {
25 | const { constructor, cache } = getConstructorDatastoreCache(instance);
26 |
27 | for (const value of values) {
28 | const keyGenerator = (): Key =>
29 | generateManyRelationshipKey(instance, toManyRelationshipMetadata, value);
30 | cache.write(instance, keyGenerator, generateRelationshipKey(value));
31 | }
32 |
33 | toManyRelationshipMetadata.instances.set(instance, values);
34 | setToManyRelationshipMetadata(constructor, toManyRelationshipMetadata);
35 |
36 | if (!skipBackRef) {
37 | const relationshipConstructor = toManyRelationshipMetadata.type();
38 | const relationshipRelationshipMetadata = getRelationshipMetadata(
39 | relationshipConstructor,
40 | toManyRelationshipMetadata.backRef
41 | );
42 |
43 | switch (relationshipRelationshipMetadata.cardinality) {
44 | case "ToOne":
45 | for (const value of values) {
46 | toOneSet(
47 | value,
48 | relationshipRelationshipMetadata as ToOneRelationshipMetadata,
49 | instance,
50 | {
51 | skipBackRef: true,
52 | }
53 | );
54 | }
55 | break;
56 | case "ToMany":
57 | for (const value of values) {
58 | addTo(value, toManyRelationshipMetadata.backRef, instance);
59 | }
60 | break;
61 | }
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/src/Relationship/toOneChild.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "../Column/PrimaryColumn";
4 | import { ToOne } from "./ToOne";
5 | import { ToOneParent } from "./toOneParent.testhelpers";
6 | import { ToManyParent } from "./toManyParent.testhelpers";
7 | import { toManyType } from "../types/toManyType";
8 | import { toOneType } from "../types/toOneType";
9 | import { ToMany } from "./ToMany";
10 |
11 | const datastore = new MemoryDatastore();
12 |
13 | @Entity({ datastore })
14 | class ToOneChild {
15 | @PrimaryColumn()
16 | public id: string;
17 |
18 | @ToOne({ type: () => ToOneParent, backRef: "toOneChild", cascade: true })
19 | public toOneParent?: toOneType = undefined;
20 |
21 | @ToMany({ type: () => ToManyParent, backRef: "toOneChild", cascade: true })
22 | public toManyParent: toManyType = [];
23 |
24 | constructor(id: string) {
25 | this.id = id;
26 | }
27 | }
28 |
29 | export { ToOneChild };
30 |
--------------------------------------------------------------------------------
/src/Relationship/toOneGet.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { getConstructorDatastoreCache } from "../utils/entities";
3 | import { generateOneRelationshipKey } from "../utils/keyGeneration";
4 | import { ToOneRelationshipMetadata } from "./relationshipMetadata";
5 | import { getHydrator } from "../utils/hydrate";
6 | import { setToOneRelationshipMetadata } from "../utils/relationships";
7 |
8 | export const toOneGet = async (
9 | instance: BaseEntity,
10 | toOneRelationshipMetadata: ToOneRelationshipMetadata
11 | ): Promise => {
12 | const cachedRelationshipInstance = toOneRelationshipMetadata.instance.get(
13 | instance
14 | );
15 | if (cachedRelationshipInstance) return cachedRelationshipInstance;
16 |
17 | const { constructor, cache } = getConstructorDatastoreCache(instance);
18 | const hydrator = getHydrator(toOneRelationshipMetadata.type());
19 |
20 | const key = generateOneRelationshipKey(instance, toOneRelationshipMetadata);
21 | const identifier = await cache.read(instance, key);
22 | const relationshipInstance = await hydrator(identifier);
23 |
24 | toOneRelationshipMetadata.instance.set(instance, relationshipInstance);
25 | setToOneRelationshipMetadata(constructor, toOneRelationshipMetadata);
26 |
27 | return relationshipInstance;
28 | };
29 |
--------------------------------------------------------------------------------
/src/Relationship/toOneParent.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../Entity/Entity";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { PrimaryColumn } from "../Column/PrimaryColumn";
4 | import { ToOne } from "./ToOne";
5 | import { ToMany } from "./ToMany";
6 | import { toManyType } from "../types/toManyType";
7 | import { toOneType } from "../types/toOneType";
8 | import { ToOneChild } from "./toOneChild.testhelpers";
9 | import { ToManyChild } from "./toManyChild.testhelpers";
10 |
11 | const datastore = new MemoryDatastore();
12 |
13 | @Entity({ datastore })
14 | class ToOneParent {
15 | @PrimaryColumn()
16 | public id: string;
17 |
18 | @ToOne({ type: () => ToOneChild, backRef: "toOneParent", cascade: true })
19 | public toOneChild?: toOneType = undefined;
20 |
21 | @ToMany({ type: () => ToManyChild, backRef: "toOneParent", cascade: true })
22 | public toManyChild: toManyType = [];
23 |
24 | constructor(id: string) {
25 | this.id = id;
26 | }
27 | }
28 |
29 | export { ToOneParent };
30 |
--------------------------------------------------------------------------------
/src/Relationship/toOneSet.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { Key } from "../Datastore/Datastore";
3 | import {
4 | getConstructorDatastoreCache,
5 | getConstructor,
6 | } from "../utils/entities";
7 | import {
8 | generateRelationshipKey,
9 | generateOneRelationshipKey,
10 | } from "../utils/keyGeneration";
11 | import {
12 | ToOneRelationshipMetadata,
13 | ToManyRelationshipMetadata,
14 | } from "./relationshipMetadata";
15 | import {
16 | setToOneRelationshipMetadata,
17 | getToOneRelationshipMetadata,
18 | getRelationshipMetadata,
19 | } from "../utils/relationships";
20 | import { addTo } from "./addTo";
21 |
22 | export const toOneSet = (
23 | instance: BaseEntity,
24 | toOneRelationshipMetadata: ToOneRelationshipMetadata,
25 | value: BaseEntity,
26 | { skipBackRef }: { skipBackRef: boolean } = { skipBackRef: false }
27 | ): void => {
28 | const { constructor, cache } = getConstructorDatastoreCache(instance);
29 |
30 | const keyGenerator = (): Key =>
31 | generateOneRelationshipKey(instance, toOneRelationshipMetadata);
32 | cache.write(instance, keyGenerator, generateRelationshipKey(value));
33 |
34 | toOneRelationshipMetadata.instance.set(instance, value);
35 | setToOneRelationshipMetadata(constructor, toOneRelationshipMetadata);
36 |
37 | if (!skipBackRef) {
38 | const relationshipConstructor = toOneRelationshipMetadata.type();
39 | const relationshipRelationshipMetadata = getRelationshipMetadata(
40 | relationshipConstructor,
41 | toOneRelationshipMetadata.backRef
42 | );
43 |
44 | switch (relationshipRelationshipMetadata.cardinality) {
45 | case "ToOne":
46 | toOneSet(
47 | value,
48 | relationshipRelationshipMetadata as ToOneRelationshipMetadata,
49 | instance,
50 | {
51 | skipBackRef: true,
52 | }
53 | );
54 | break;
55 | case "ToMany":
56 | addTo(value, toOneRelationshipMetadata.backRef, instance);
57 | break;
58 | }
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/src/Repository/ColumnNotFindableError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { EntityConstructor } from "../Entity/Entity";
3 | import { ColumnMetadata } from "../Column/columnMetadata";
4 |
5 | export class ColumnNotFindableError extends KVORMError {
6 | constructor(
7 | constructor: EntityConstructor,
8 | columnMetadata: ColumnMetadata,
9 | message = `Unknown Error`
10 | ) {
11 | super(
12 | `The Column, ${columnMetadata.property.toString()}, on Entity, ${
13 | constructor.name
14 | } cannot be used to find: ${message}`
15 | );
16 | this.name = `ColumnNotFindableError`;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Repository/ColumnNotSearchableError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { EntityConstructor } from "../Entity/Entity";
3 | import { ColumnMetadata } from "../Column/columnMetadata";
4 |
5 | export class ColumnNotSearchableError extends KVORMError {
6 | constructor(
7 | constructor: EntityConstructor,
8 | columnMetadata: ColumnMetadata,
9 | message = `Unknown Error`
10 | ) {
11 | super(
12 | `The Column, ${columnMetadata.property.toString()}, on Entity, ${
13 | constructor.name
14 | } cannot be searched with: ${message}`
15 | );
16 | this.name = `ColumnNotSearchableError`;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Repository/EntityNotFoundError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { Value } from "../Datastore/Datastore";
3 | import { EntityConstructor } from "../Entity/Entity";
4 |
5 | export class EntityNotFoundError extends KVORMError {
6 | constructor(constructor: EntityConstructor, identifier?: Value) {
7 | super(
8 | `Could not find an Entity, ${constructor.name}, with PrimaryColumn identifier, ${identifier} in Datastore`
9 | );
10 | this.name = `EntityNotFoundError`;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Repository/Repository.test.ts:
--------------------------------------------------------------------------------
1 | import { Datastore } from "../Datastore/Datastore";
2 | import { BaseEntity, Entity } from "../Entity/Entity";
3 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
4 | import { Column } from "../Column/Column";
5 | import { Repository, getRepository } from "./Repository";
6 | import { RepositoryLoadError } from "./RepositoryLoadError";
7 | import { ColumnLookupError } from "../utils/errors";
8 | import { EntityNotFoundError } from "./EntityNotFoundError";
9 | import { ColumnNotSearchableError } from "./ColumnNotSearchableError";
10 | import { ColumnNotFindableError } from "./ColumnNotFindableError";
11 | import { RepositoryFindError } from "./RepositoryFindError";
12 |
13 | describe(`Repository`, () => {
14 | let datastore: Datastore;
15 | let singletonInstance: BaseEntity;
16 | let singletonRepository: Repository;
17 | let complexInstance: BaseEntity;
18 | let complexRepository: Repository;
19 |
20 | beforeEach(() => {
21 | datastore = new MemoryDatastore();
22 |
23 | @Entity({ datastore, key: `SingletonEntity` })
24 | class SingletonEntity {
25 | @Column({ key: `myProperty`, isIndexable: true })
26 | public myProperty = `initial value`;
27 | }
28 |
29 | @Entity({ datastore, key: `ComplexEntity` })
30 | class ComplexEntity {
31 | @Column({ key: `myProperty` })
32 | public myProperty = `initial value`;
33 |
34 | @Column({ key: `primaryProperty`, isPrimary: true })
35 | public primaryProperty: number;
36 |
37 | @Column({
38 | key: `indexableUniqueProperty`,
39 | isIndexable: true,
40 | isUnique: true,
41 | })
42 | public indexableUniqueProperty: string;
43 |
44 | @Column({
45 | key: `indexableProperty`,
46 | isIndexable: true,
47 | })
48 | public indexableProperty: string;
49 |
50 | @Column()
51 | public arrayProperty: number[] = [];
52 |
53 | constructor(
54 | primaryProperty: number,
55 | indexableUniqueProperty: string,
56 | indexableProperty: string
57 | ) {
58 | this.primaryProperty = primaryProperty;
59 | this.indexableUniqueProperty = indexableUniqueProperty;
60 | this.indexableProperty = indexableProperty;
61 | }
62 | }
63 |
64 | singletonRepository = getRepository(SingletonEntity);
65 | singletonInstance = new SingletonEntity();
66 |
67 | complexRepository = getRepository(ComplexEntity);
68 | complexInstance = new ComplexEntity(12345, `abc@xyz.com`, `blue`);
69 | });
70 |
71 | it(`can be initialized with a default value`, async () => {
72 | expect(await datastore.read(`SingletonEntity:myProperty`)).toBeNull();
73 | expect(await singletonRepository.save(singletonInstance)).toBeTruthy();
74 | expect(await datastore.read(`SingletonEntity:myProperty`)).toEqual(
75 | `initial value`
76 | );
77 | expect(await singletonRepository.save(singletonInstance)).toBeFalsy();
78 |
79 | expect(await complexRepository.save(complexInstance)).toBeTruthy();
80 | expect(await datastore.read(`ComplexEntity:myProperty`)).toBeNull();
81 | expect(await datastore.read(`ComplexEntity:12345:myProperty`)).toEqual(
82 | `initial value`
83 | );
84 | expect(await datastore.read(`ComplexEntity:12345:primaryProperty`)).toEqual(
85 | 12345
86 | );
87 | expect(
88 | await datastore.read(`ComplexEntity:12345:indexableUniqueProperty`)
89 | ).toEqual(`abc@xyz.com`);
90 | expect(
91 | await datastore.read(`ComplexEntity:indexableUniqueProperty:abc@xyz.com`)
92 | ).toEqual(12345);
93 | expect(
94 | await datastore.read(`ComplexEntity:indexableProperty:blue:12345`)
95 | ).toEqual(12345);
96 | });
97 |
98 | it(`can be written to, and subsequently read from`, async () => {
99 | singletonInstance.myProperty = `new value`;
100 | expect(await singletonRepository.save(singletonInstance)).toBeTruthy();
101 | expect(await datastore.read(`SingletonEntity:myProperty`)).toEqual(
102 | `new value`
103 | );
104 | expect(await singletonInstance.myProperty).toEqual(`new value`);
105 |
106 | complexInstance.myProperty = `new value`;
107 | expect(await singletonRepository.save(complexInstance)).toBeTruthy();
108 | expect(await datastore.read(`ComplexEntity:12345:myProperty`)).toEqual(
109 | `new value`
110 | );
111 | expect(await complexInstance.myProperty).toEqual(`new value`);
112 | });
113 |
114 | it(`can load an instance`, async () => {
115 | await singletonRepository.save(singletonInstance);
116 | let loadedInstance = await singletonRepository.load();
117 | expect(await loadedInstance.myProperty).toEqual(`initial value`);
118 |
119 | await complexRepository.save(complexInstance);
120 | loadedInstance = await complexRepository.load(12345);
121 | expect(await loadedInstance.myProperty).toEqual(`initial value`);
122 |
123 | singletonInstance.myProperty = `new value`;
124 | await singletonRepository.save(singletonInstance);
125 | loadedInstance = await singletonRepository.load();
126 | expect(await loadedInstance.myProperty).toEqual(`new value`);
127 |
128 | complexInstance.myProperty = `new value`;
129 | await complexRepository.save(complexInstance);
130 | loadedInstance = await complexRepository.load(12345);
131 | expect(await loadedInstance.myProperty).toEqual(`new value`);
132 | });
133 |
134 | it(`can find an instance`, async () => {
135 | await complexRepository.save(complexInstance);
136 | const loadedInstance = await complexRepository.load(12345);
137 | const foundInstance = (await complexRepository.find(
138 | `indexableUniqueProperty`,
139 | `abc@xyz.com`
140 | )) as BaseEntity;
141 | expect(await foundInstance.myProperty).toEqual(
142 | await loadedInstance.myProperty
143 | );
144 | expect(await foundInstance.primaryProperty).toEqual(
145 | await loadedInstance.primaryProperty
146 | );
147 | expect(await foundInstance.indexableUniqueProperty).toEqual(
148 | await loadedInstance.indexableUniqueProperty
149 | );
150 | });
151 |
152 | it("throws an error when finding a non UniqueColumn", () => {
153 | expect(
154 | complexRepository.find("indexableProperty", "")
155 | ).rejects.toThrowError();
156 | });
157 |
158 | it(`returns null when finding for a non-existent instance`, async () => {
159 | await complexRepository.save(complexInstance);
160 | const foundInstance = await complexRepository.find(
161 | `indexableUniqueProperty`,
162 | `non-existent@email.com`
163 | );
164 | expect(foundInstance).toBeNull();
165 | });
166 |
167 | it(`can save and load a Column with an array type`, async () => {
168 | const values = [1, 2, 3, 4, 5];
169 | complexInstance.arrayProperty = values;
170 | expect(await complexInstance.arrayProperty).toEqual(values);
171 | await complexRepository.save(complexInstance);
172 |
173 | const loadedInstance = await complexRepository.load(12345);
174 | expect(await loadedInstance.arrayProperty).toEqual(values);
175 | });
176 |
177 | it("can search", async () => {
178 | await complexRepository.save(complexInstance);
179 | const searchResults = await complexRepository.search(
180 | "indexableProperty",
181 | "blue"
182 | );
183 | const expectedIDs = [12345];
184 | let i = 0;
185 | for await (const searchResult of searchResults) {
186 | expect(await searchResult.primaryProperty).toEqual(expectedIDs[i]);
187 | i++;
188 | }
189 | expect(i).toBe(expectedIDs.length);
190 | });
191 |
192 | describe(`RepositoryLoadError`, () => {
193 | it(`is thrown when loading a singleton Entity with an identifier`, async () => {
194 | await expect(
195 | (async (): Promise => {
196 | await singletonRepository.load(12345);
197 | })()
198 | ).rejects.toThrow(RepositoryLoadError);
199 | });
200 | it(`is thrown when loading a non-singleton Entity without an identifier`, async () => {
201 | await expect(
202 | (async (): Promise => {
203 | await complexRepository.load();
204 | })()
205 | ).rejects.toThrow(RepositoryLoadError);
206 | });
207 | });
208 |
209 | describe("RepositoryFindError", () => {
210 | it(`is thrown when loading a singleton Entity without an identifier`, async () => {
211 | await expect(
212 | (async (): Promise => {
213 | await singletonRepository.find("myProperty", "someValue");
214 | })()
215 | ).rejects.toThrow(RepositoryFindError);
216 | });
217 | });
218 |
219 | describe(`ColumnLookupError`, () => {
220 | it(`is thrown when searching a non-existent property`, async () => {
221 | await expect(
222 | (async (): Promise => {
223 | await complexRepository.find(`fakeProperty`, 1);
224 | })()
225 | ).rejects.toThrow(ColumnLookupError);
226 | });
227 | });
228 |
229 | describe(`ColumnNotFindableError`, () => {
230 | it(`is thrown when searching a non IndexableColumn`, async () => {
231 | await expect(
232 | (async (): Promise => {
233 | await complexRepository.find(`myProperty`, `value`);
234 | })()
235 | ).rejects.toThrow(ColumnNotFindableError);
236 | });
237 | });
238 |
239 | describe(`ColumnNotFindableError`, () => {
240 | it(`is thrown when searching a non IndexableColumn`, async () => {
241 | await expect(
242 | (async (): Promise => {
243 | await complexRepository.search(`myProperty`, `value`);
244 | })()
245 | ).rejects.toThrow(ColumnNotSearchableError);
246 | });
247 | });
248 |
249 | describe(`EntityNotFoundError`, () => {
250 | it(`is thrown when loading a non-existent non-singleton entity`, async () => {
251 | await expect(
252 | (async (): Promise => {
253 | await complexRepository.load(`99999`);
254 | })()
255 | ).rejects.toThrow(EntityNotFoundError);
256 | });
257 |
258 | it(`is [not?] thrown when loading a non-existent singleton entity`, async () => {
259 | // TODO: Should we throw an EntityNotFoundError here?
260 | // await expect(
261 | // (async (): Promise => {
262 | // await singletonRepository.load()
263 | // })()
264 | // ).rejects.toThrow(EntityNotFoundError)
265 | });
266 | });
267 | });
268 |
--------------------------------------------------------------------------------
/src/Repository/Repository.ts:
--------------------------------------------------------------------------------
1 | import "../metadata";
2 |
3 | import { BaseEntity, EntityConstructor } from "../Entity/Entity";
4 | import { PropertyKey } from "../Entity/Entity";
5 | import { Value } from "../Datastore/Datastore";
6 | import { repositoryLoad } from "./repositoryLoad";
7 | import { repositorySearch } from "./repositorySearch";
8 | import { repositoryFind } from "./repositoryFind";
9 | import { repositorySave } from "./repositorySave";
10 |
11 | export interface Repository {
12 | load(identifier?: Value): Promise;
13 | save(entity: BaseEntity): Promise;
14 | find(property: PropertyKey, identifier: Value): Promise;
15 | search(
16 | property: PropertyKey,
17 | identifier: Value
18 | ): Promise>;
19 | }
20 |
21 | export const getRepository = (
22 | constructor: EntityConstructor
23 | ): Repository => {
24 | return {
25 | async load(identifier?: Value): Promise {
26 | return await repositoryLoad(constructor, identifier);
27 | },
28 | async save(instance: BaseEntity): Promise {
29 | return await repositorySave(instance);
30 | },
31 | async find(property: PropertyKey, identifier: Value): Promise {
32 | return await repositoryFind(constructor, property, identifier);
33 | },
34 | search(
35 | property: PropertyKey,
36 | identifier: Value
37 | ): Promise> {
38 | return repositorySearch(constructor, property, identifier);
39 | },
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/src/Repository/RepositoryFindError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { EntityConstructor } from "../Entity/Entity";
3 |
4 | export class RepositoryFindError extends KVORMError {
5 | constructor(constructor: EntityConstructor, message = `Unknown Error`) {
6 | super(`Could not find Entity, ${constructor.name}: ${message}`);
7 | this.name = `RepositoryFindError`;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Repository/RepositoryLoadError.ts:
--------------------------------------------------------------------------------
1 | import { KVORMError } from "../utils/errors";
2 | import { EntityConstructor } from "../Entity/Entity";
3 |
4 | export class RepositoryLoadError extends KVORMError {
5 | constructor(constructor: EntityConstructor, message = `Unknown Error`) {
6 | super(`Could not load Entity, ${constructor.name}: ${message}`);
7 | this.name = `RepositoryLoadError`;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Repository/repositoryDelete.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { getConstructorDatastoreCache } from "../utils/entities";
3 | import { getColumnMetadatas } from "../utils/columns";
4 | import {
5 | getToOneRelationshipMetadatas,
6 | getToManyRelationshipMetadatas,
7 | getRelationshipMetadata,
8 | } from "../utils/relationships";
9 | import { columnSet } from "../Column/columnSet";
10 | import { repositorySave } from "./repositorySave";
11 | import { toOneGet } from "../Relationship/toOneGet";
12 | import { toManyGet } from "../Relationship/toManyGet";
13 | import { removeFrom } from "../Relationship/removeFrom";
14 |
15 | export const repositoryDelete = async (
16 | instance: BaseEntity
17 | ): Promise => {
18 | const { constructor } = getConstructorDatastoreCache(instance);
19 | const columnMetadatas = getColumnMetadatas(constructor);
20 |
21 | for (const columnMetadata of columnMetadatas) {
22 | columnSet(instance, columnMetadata, null);
23 | }
24 |
25 | const toOneRelationshipMetadatas = getToOneRelationshipMetadatas(constructor);
26 | const toManyRelationshipMetadatas = getToManyRelationshipMetadatas(
27 | constructor
28 | );
29 |
30 | let updatedRelation = false;
31 |
32 | for (const toOneRelationshipMetadata of toOneRelationshipMetadatas) {
33 | if (toOneRelationshipMetadata.cascade.onDelete) {
34 | const relationInstance = await toOneGet(
35 | instance,
36 | toOneRelationshipMetadata
37 | );
38 | const relationshipConstructor = toOneRelationshipMetadata.type();
39 | const relationshipRelationshipMetadata = getRelationshipMetadata(
40 | relationshipConstructor,
41 | toOneRelationshipMetadata.backRef
42 | );
43 |
44 | switch (relationshipRelationshipMetadata.cardinality) {
45 | case "ToOne":
46 | updatedRelation =
47 | (await repositoryDelete(relationInstance)) || updatedRelation;
48 | break;
49 | case "ToMany":
50 | // TODO: updatedRelation =
51 | removeFrom(
52 | relationInstance,
53 | toOneRelationshipMetadata.backRef,
54 | instance
55 | );
56 | break;
57 | }
58 | }
59 | }
60 |
61 | for (const toManyRelationshipMetadata of toManyRelationshipMetadatas) {
62 | if (toManyRelationshipMetadata.cascade.onDelete) {
63 | const relationInstances = toManyGet(instance, toManyRelationshipMetadata);
64 | const relationshipConstructor = toManyRelationshipMetadata.type();
65 | const relationshipRelationshipMetadata = getRelationshipMetadata(
66 | relationshipConstructor,
67 | toManyRelationshipMetadata.backRef
68 | );
69 |
70 | switch (relationshipRelationshipMetadata.cardinality) {
71 | case "ToOne":
72 | for await (const relationInstance of relationInstances) {
73 | updatedRelation =
74 | (await repositoryDelete(relationInstance)) || updatedRelation;
75 | }
76 | break;
77 | case "ToMany":
78 | for await (const relationInstance of relationInstances) {
79 | // TODO: updatedRelation =
80 | removeFrom(
81 | relationInstance,
82 | toManyRelationshipMetadata.backRef,
83 | instance
84 | );
85 | }
86 | break;
87 | }
88 | }
89 | }
90 |
91 | return repositorySave(instance) || updatedRelation;
92 | };
93 |
--------------------------------------------------------------------------------
/src/Repository/repositoryFind.ts:
--------------------------------------------------------------------------------
1 | import { EntityConstructor, BaseEntity } from "../Entity/Entity";
2 | import { Value } from "../Datastore/Datastore";
3 | import { getColumnMetadata, getPrimaryColumnMetadata } from "../utils/columns";
4 | import { PropertyKey } from "../Entity/Entity";
5 | import { getUniqueSearchKey } from "../utils/keyGeneration";
6 | import { repositoryLoad } from "./repositoryLoad";
7 | import { getDatastore } from "../utils/datastore";
8 | import { ColumnNotFindableError } from "./ColumnNotFindableError";
9 | import { RepositoryFindError } from "./RepositoryFindError";
10 |
11 | const assertNotSingleton = (constructor: EntityConstructor) => {
12 | const primaryColumnMetadata = getPrimaryColumnMetadata(constructor);
13 |
14 | if (primaryColumnMetadata === undefined)
15 | throw new RepositoryFindError(
16 | constructor,
17 | `Entity is a singleton, so cannot perform a find with it's repository. Try simply loading with repository.load() instead.`
18 | );
19 | };
20 |
21 | export const repositoryFind = async (
22 | constructor: EntityConstructor,
23 | property: PropertyKey,
24 | identifier: Value
25 | ): Promise => {
26 | const datastore = getDatastore(constructor);
27 | const columnMetadata = getColumnMetadata(constructor, property);
28 | assertNotSingleton(constructor);
29 |
30 | if (!columnMetadata.isIndexable)
31 | throw new ColumnNotFindableError(
32 | constructor,
33 | columnMetadata,
34 | `Column is not set as isIndexable`
35 | );
36 |
37 | if (!columnMetadata.isUnique)
38 | throw new ColumnNotFindableError(
39 | constructor,
40 | columnMetadata,
41 | `Column is not set as isUnique`
42 | );
43 |
44 | const key = getUniqueSearchKey(constructor, columnMetadata, identifier);
45 | const primaryIdentifier = await datastore.read(key);
46 |
47 | if (primaryIdentifier !== null) {
48 | return await repositoryLoad(constructor, primaryIdentifier);
49 | } else {
50 | return null;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/src/Repository/repositoryLoad.ts:
--------------------------------------------------------------------------------
1 | import { EntityConstructor, BaseEntity } from "../Entity/Entity";
2 | import { Value, Datastore } from "../Datastore/Datastore";
3 | import { createEmptyInstance } from "../utils/entities";
4 | import {
5 | getPrimaryColumnMetadata,
6 | setPrimaryColumnValue,
7 | } from "../utils/columns";
8 | import { RepositoryLoadError } from "./RepositoryLoadError";
9 | import { getDatastore } from "../utils/datastore";
10 | import { generatePropertyKey } from "../utils/keyGeneration";
11 | import { EntityNotFoundError } from "./EntityNotFoundError";
12 | import { ColumnMetadata } from "../Column/columnMetadata";
13 |
14 | const assertIdentifierValid = (
15 | constructor: EntityConstructor,
16 | primaryColumnMetadata: ColumnMetadata | undefined,
17 | identifier: Value
18 | ): void => {
19 | if (primaryColumnMetadata === undefined && identifier) {
20 | throw new RepositoryLoadError(
21 | constructor,
22 | `Entity is a singleton, so cannot load with an identifier.`
23 | );
24 | } else if (primaryColumnMetadata !== undefined && !identifier) {
25 | throw new RepositoryLoadError(
26 | constructor,
27 | `Entity is not a singleton, and so requires an identifier to load with.`
28 | );
29 | }
30 | };
31 |
32 | const loadNonSingleton = async (
33 | datastore: Datastore,
34 | constructor: EntityConstructor,
35 | instance: BaseEntity,
36 | primaryColumnMetadata: ColumnMetadata,
37 | identifier: Value
38 | ): Promise => {
39 | setPrimaryColumnValue(instance, identifier);
40 | const key = generatePropertyKey(instance, primaryColumnMetadata);
41 | const loadedIdentifier = await datastore.read(key);
42 | if (String(loadedIdentifier) !== identifier.toString())
43 | throw new EntityNotFoundError(constructor, identifier);
44 |
45 | setPrimaryColumnValue(instance, loadedIdentifier);
46 | };
47 |
48 | export const repositoryLoad = async (
49 | constructor: EntityConstructor,
50 | identifier?: Value
51 | ): Promise => {
52 | const datastore = getDatastore(constructor);
53 | const instance = createEmptyInstance(constructor);
54 | const primaryColumnMetadata = getPrimaryColumnMetadata(constructor);
55 |
56 | assertIdentifierValid(constructor, primaryColumnMetadata, identifier);
57 |
58 | if (primaryColumnMetadata !== undefined && identifier !== undefined) {
59 | await loadNonSingleton(
60 | datastore,
61 | constructor,
62 | instance,
63 | primaryColumnMetadata,
64 | identifier
65 | );
66 | } else {
67 | // Entity is a singleton
68 | // Should we still somehow check if it has been saved before and throw a notfound error?
69 | }
70 |
71 | return instance;
72 | };
73 |
--------------------------------------------------------------------------------
/src/Repository/repositorySave.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity } from "../Entity/Entity";
2 | import { getConstructorDatastoreCache } from "../utils/entities";
3 | import { getRepository } from "./Repository";
4 | import {
5 | getToOneRelationshipMetadatas,
6 | getToManyRelationshipMetadatas,
7 | } from "../utils/relationships";
8 | import { RelationshipMetadata } from "../Relationship/relationshipMetadata";
9 |
10 | export const repositorySave = async (
11 | instance: BaseEntity,
12 | {
13 | skipRelationshipMetadatas,
14 | }: { skipRelationshipMetadatas: RelationshipMetadata[] } = {
15 | skipRelationshipMetadatas: [],
16 | }
17 | ): Promise => {
18 | const { constructor, cache } = getConstructorDatastoreCache(instance);
19 |
20 | const toOneRelationshipMetadatas = getToOneRelationshipMetadatas(constructor);
21 | const toManyRelationshipMetadatas = getToManyRelationshipMetadatas(
22 | constructor
23 | );
24 |
25 | let updatedRelation = false;
26 |
27 | for (const toOneRelationshipMetadata of toOneRelationshipMetadatas) {
28 | if (
29 | toOneRelationshipMetadata.cascade.onUpdate &&
30 | !skipRelationshipMetadatas.includes(toOneRelationshipMetadata)
31 | ) {
32 | skipRelationshipMetadatas.push(toOneRelationshipMetadata);
33 | const cachedRelationInstance = toOneRelationshipMetadata.instance.get(
34 | instance
35 | );
36 | if (cachedRelationInstance) {
37 | updatedRelation =
38 | (await repositorySave(cachedRelationInstance, {
39 | skipRelationshipMetadatas,
40 | })) || updatedRelation;
41 | }
42 | }
43 | }
44 |
45 | for (const toManyRelationshipMetadata of toManyRelationshipMetadatas) {
46 | if (
47 | toManyRelationshipMetadata.cascade.onUpdate &&
48 | !skipRelationshipMetadatas.push(toManyRelationshipMetadata)
49 | ) {
50 | skipRelationshipMetadatas.push(toManyRelationshipMetadata);
51 | const cachedRelationInstances =
52 | toManyRelationshipMetadata.instances.get(instance) || [];
53 | for (const cachedRelationInstance of cachedRelationInstances) {
54 | updatedRelation =
55 | (await repositorySave(cachedRelationInstance, {
56 | skipRelationshipMetadatas,
57 | })) || updatedRelation;
58 | }
59 | }
60 | }
61 |
62 | return cache.sync(instance) || updatedRelation;
63 | };
64 |
--------------------------------------------------------------------------------
/src/Repository/repositorySearch.ts:
--------------------------------------------------------------------------------
1 | import { EntityConstructor, BaseEntity } from "../Entity/Entity";
2 | import { Value } from "../Datastore/Datastore";
3 | import { getColumnMetadata } from "../utils/columns";
4 | import { PropertyKey } from "../Entity/Entity";
5 | import { getIndexableSearchKey } from "../utils/keyGeneration";
6 | import { getDatastore } from "../utils/datastore";
7 | import { ColumnNotSearchableError } from "./ColumnNotSearchableError";
8 | import { getHydrator, hydrateMany } from "../utils/hydrate";
9 |
10 | export const repositorySearch = async (
11 | constructor: EntityConstructor,
12 | property: PropertyKey,
13 | identifier: Value
14 | ): Promise> => {
15 | const datastore = getDatastore(constructor);
16 | const columnMetadata = getColumnMetadata(constructor, property);
17 |
18 | if (!columnMetadata.isIndexable)
19 | throw new ColumnNotSearchableError(
20 | constructor,
21 | columnMetadata,
22 | `Column is not set as isIndexable`
23 | );
24 |
25 | const searchKey = getIndexableSearchKey(
26 | constructor,
27 | columnMetadata,
28 | identifier
29 | );
30 | const hydrator = getHydrator(constructor);
31 | return hydrateMany(datastore, searchKey, hydrator);
32 | };
33 |
--------------------------------------------------------------------------------
/src/__tests__/library/datastores/libraryDatastore.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { MemoryDatastore } from "../../../MemoryDatastore/MemoryDatastore";
2 |
3 | export const libraryDatastore = new MemoryDatastore();
4 |
--------------------------------------------------------------------------------
/src/__tests__/library/fixtures/williamShakespeare.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { Author } from "../models/Author.testhelpers";
2 |
3 | const williamShakespeare = new Author({
4 | firstName: `William`,
5 | lastName: `Shakespeare`,
6 | emailAddress: `william@shakespeare.com`,
7 | phoneNumber: `+1234567890`,
8 | birthYear: 1564,
9 | });
10 |
11 | williamShakespeare.nickName = `Bill`;
12 | williamShakespeare.someUnsavedProperty = `Won't get saved!`;
13 |
14 | export { williamShakespeare };
15 |
--------------------------------------------------------------------------------
/src/__tests__/library/index.test.ts:
--------------------------------------------------------------------------------
1 | import { Author } from "./models/Author.testhelpers";
2 | import { williamShakespeare } from "./fixtures/williamShakespeare.testhelpers";
3 | import { getRepository } from "../../Repository/Repository";
4 | import { libraryDatastore } from "./datastores/libraryDatastore.testhelpers";
5 |
6 | const repository = getRepository(Author);
7 |
8 | describe(`library`, () => {
9 | describe(`README.md example`, () => {
10 | beforeEach(() => {
11 | (async () => await repository.save(williamShakespeare))();
12 | });
13 |
14 | it(`async await read firstName`, async () => {
15 | expect(await williamShakespeare.firstName).toEqual(`William`);
16 | });
17 |
18 | it(`property getters/setters`, async () => {
19 | const date = new Date(`2020-01-02T03:04:05Z`);
20 | williamShakespeare.somethingComplex = date;
21 | expect(await williamShakespeare.somethingComplex).toEqual(date);
22 | });
23 |
24 | it(`repository saves/loads`, async () => {
25 | const loadedWilliamShakespeare = await repository.load(
26 | `william@shakespeare.com`
27 | );
28 | expect(await loadedWilliamShakespeare.nickName).toEqual(`Bill`);
29 | });
30 |
31 | it(`repository finds`, async () => {
32 | const foundWilliamShakespeare = await repository.find(
33 | `phoneNumber`,
34 | `+1234567890`
35 | );
36 |
37 | expect(foundWilliamShakespeare).not.toBeNull();
38 | expect(await foundWilliamShakespeare?.nickName).toEqual(`Bill`);
39 |
40 | const foundNonexistent = await repository.find(
41 | `phoneNumber`,
42 | `+9999999999`
43 | );
44 |
45 | expect(foundNonexistent).toBeNull();
46 | });
47 |
48 | it(`repository searches`, async () => {
49 | const searchedAuthors = await repository.search(`birthYear`, 1564);
50 |
51 | let i = 0;
52 | for await (const searchedAuthor of searchedAuthors) {
53 | expect(await searchedAuthor.nickName).toEqual("Bill");
54 | i++;
55 | }
56 |
57 | expect(i).toBe(1);
58 | });
59 | });
60 |
61 | describe(`the datastore`, () => {
62 | it(`is saved as expected`, () => {
63 | expect((libraryDatastore as any).data).toMatchInlineSnapshot(`
64 | Map {
65 | "Author:william@shakespeare.com:_complex" => "2020-01-02T03:04:05.000Z",
66 | "Author:william@shakespeare.com:givenName" => "William",
67 | "Author:william@shakespeare.com:lastName" => "Shakespeare",
68 | "Author:emailAddress:william@shakespeare.com" => "william@shakespeare.com",
69 | "Author:william@shakespeare.com:emailAddress" => "william@shakespeare.com",
70 | "Author:birthYear:1564:william@shakespeare.com" => "william@shakespeare.com",
71 | "Author:william@shakespeare.com:birthYear" => 1564,
72 | "Author:phoneNumber:+1234567890" => "william@shakespeare.com",
73 | "Author:william@shakespeare.com:phoneNumber" => "+1234567890",
74 | "Author:william@shakespeare.com:nickName" => "Bill",
75 | }
76 | `);
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/__tests__/library/models/Author.testhelpers.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../../../Entity/Entity";
2 | import { Column } from "../../../Column/Column";
3 | import { PrimaryColumn } from "../../../Column/PrimaryColumn";
4 | import { UniqueColumn } from "../../../Column/UniqueColumn";
5 | import { IndexableColumn } from "../../../Column/IndexableColumn";
6 | import { columnType } from "../../../types/columnType";
7 | import { libraryDatastore } from "../datastores/libraryDatastore.testhelpers";
8 |
9 | const serialize = (value: columnType): string => {
10 | if (!(value instanceof Date)) throw new Error(`Value must be a date`);
11 | return value.toISOString();
12 | };
13 |
14 | const deserialize = (value: string): any => new Date(value);
15 |
16 | @Entity({ datastore: libraryDatastore })
17 | export class Author {
18 | @Column({ key: `givenName` })
19 | public firstName: columnType;
20 |
21 | @Column()
22 | public lastName: columnType;
23 |
24 | @Column()
25 | public nickName?: columnType;
26 |
27 | @PrimaryColumn()
28 | public emailAddress: columnType;
29 |
30 | @IndexableColumn()
31 | public birthYear: columnType;
32 |
33 | @UniqueColumn()
34 | public phoneNumber: columnType;
35 |
36 | public someUnsavedProperty: any;
37 |
38 | @Column()
39 | private _complex: columnType = ``;
40 |
41 | set somethingComplex(value: columnType) {
42 | this._complex = serialize(value);
43 | }
44 |
45 | get somethingComplex(): columnType {
46 | return (async () => deserialize(await this._complex))();
47 | }
48 |
49 | public constructor({
50 | firstName,
51 | lastName,
52 | emailAddress,
53 | birthYear,
54 | phoneNumber,
55 | }: {
56 | firstName: string;
57 | lastName: string;
58 | emailAddress: string;
59 | birthYear: number;
60 | phoneNumber: string;
61 | }) {
62 | this.firstName = firstName;
63 | this.lastName = lastName;
64 | this.emailAddress = emailAddress;
65 | this.birthYear = birthYear;
66 | this.phoneNumber = phoneNumber;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | describe(`the universe`, () => {
2 | it(`can do math`, () => {
3 | expect(1 + 1).toEqual(2);
4 | });
5 | });
6 |
7 | describe("exports", () => {
8 | it("includes everything", () => {
9 | const everything = require("./index");
10 | expect(Object.keys(everything)).toMatchInlineSnapshot(`
11 | Array [
12 | "Datastore",
13 | "SearchStrategy",
14 | "MemoryDatastore",
15 | "Entity",
16 | "Column",
17 | "PrimaryColumn",
18 | "UniqueColumn",
19 | "IndexableColumn",
20 | "ToOne",
21 | "ToMany",
22 | "addTo",
23 | "removeFrom",
24 | "getRepository",
25 | ]
26 | `);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Datastore, Key, Value } from "./Datastore/Datastore";
2 | export {
3 | SearchOptions,
4 | SearchResult,
5 | SearchStrategy,
6 | } from "./Datastore/Datastore";
7 |
8 | export { MemoryDatastore } from "./MemoryDatastore/MemoryDatastore";
9 | export { Entity } from "./Entity/Entity";
10 | export { Column } from "./Column/Column";
11 | export { PrimaryColumn } from "./Column/PrimaryColumn";
12 | export { UniqueColumn } from "./Column/UniqueColumn";
13 | export { IndexableColumn } from "./Column/IndexableColumn";
14 | export { columnType } from "./types/columnType";
15 | export { ToOne } from "./Relationship/ToOne";
16 | export { toOneType } from "./types/toOneType";
17 | export { ToMany } from "./Relationship/ToMany";
18 | export { toManyType } from "./types/toManyType";
19 | export { addTo } from "./Relationship/addTo";
20 | export { removeFrom } from "./Relationship/removeFrom";
21 | export { getRepository } from "./Repository/Repository";
22 |
--------------------------------------------------------------------------------
/src/metadata.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 |
--------------------------------------------------------------------------------
/src/types/columnType.test.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "../Column/Column";
2 | import { Entity } from "../Entity/Entity";
3 | import { Datastore } from "../Datastore/Datastore";
4 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
5 | import { columnType } from "./columnType";
6 |
7 | describe(`columnType`, () => {
8 | let datastore: Datastore;
9 |
10 | beforeEach(() => {
11 | datastore = new MemoryDatastore();
12 | });
13 |
14 | it(`helps`, async () => {
15 | @Entity({ datastore })
16 | class WithColumnType {
17 | @Column()
18 | id: columnType;
19 |
20 | constructor(id: number) {
21 | this.id = id;
22 | }
23 | }
24 |
25 | @Entity({ datastore })
26 | class WithoutColumnType {
27 | @Column()
28 | id: number;
29 |
30 | constructor(id: number) {
31 | this.id = id;
32 | }
33 | }
34 |
35 | const instanceWithColumnType = new WithColumnType(123);
36 | const instanceWithoutColumnType = new WithoutColumnType(123);
37 |
38 | await instanceWithColumnType.id;
39 | await instanceWithoutColumnType.id; // 'await' has no effect on the type of this expression.ts(80007)
40 |
41 | instanceWithColumnType.id = 234;
42 | instanceWithoutColumnType.id = 234;
43 | });
44 | });
45 |
46 | type toManyType =
47 | | T[]
48 | | {
49 | push(value: T): void;
50 | };
51 |
--------------------------------------------------------------------------------
/src/types/columnType.ts:
--------------------------------------------------------------------------------
1 | export type columnType = T | Promise;
2 |
--------------------------------------------------------------------------------
/src/types/toManyType.ts:
--------------------------------------------------------------------------------
1 | export type toManyType = T[] | AsyncGenerator;
2 |
--------------------------------------------------------------------------------
/src/types/toOneType.ts:
--------------------------------------------------------------------------------
1 | export type toOneType = T | Promise;
2 |
--------------------------------------------------------------------------------
/src/utils/cache.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "../Cache/Cache";
2 | import { Datastore } from "../Datastore/Datastore";
3 |
4 | export const getCache = (datastore: Datastore): Cache => datastore.cache;
5 |
--------------------------------------------------------------------------------
/src/utils/columns.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../Entity/Entity";
2 | import { Datastore } from "../Datastore/Datastore";
3 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
4 | import { PrimaryColumnMissingError } from "./errors";
5 | import { getPrimaryColumnValue } from "./columns";
6 |
7 | describe(`columns`, () => {
8 | let datastore: Datastore;
9 | beforeEach(() => {
10 | datastore = new MemoryDatastore();
11 | });
12 | describe(`PrimaryColumnMissingError`, () => {
13 | it(`is thrown when it does not exist`, () => {
14 | @Entity({ datastore })
15 | class MyEntity {}
16 |
17 | const instance = new MyEntity();
18 |
19 | expect(() => {
20 | getPrimaryColumnValue(instance);
21 | }).toThrow(PrimaryColumnMissingError);
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/utils/columns.ts:
--------------------------------------------------------------------------------
1 | import "../metadata";
2 |
3 | import { EntityConstructor, BaseEntity, PropertyKey } from "../Entity/Entity";
4 | import { COLUMN_KEY } from "../Column/Column";
5 | import { ColumnMetadata } from "../Column/columnMetadata";
6 | import { ColumnLookupError, PrimaryColumnMissingError } from "./errors";
7 | import { getConstructorDatastoreCache } from "./entities";
8 | import { Value } from "../Datastore/Datastore";
9 | import { getMetadatas, getMetadata, setMetadata } from "./metadata";
10 |
11 | export const getColumnMetadatas = (
12 | constructor: EntityConstructor
13 | ): ColumnMetadata[] =>
14 | getMetadatas(COLUMN_KEY, constructor) as ColumnMetadata[];
15 |
16 | export const getColumnMetadata = (
17 | constructor: EntityConstructor,
18 | property: PropertyKey
19 | ): ColumnMetadata => {
20 | const columnMetadatas = getColumnMetadatas(constructor);
21 | return getMetadata(columnMetadatas, property, () => {
22 | throw new ColumnLookupError(
23 | constructor,
24 | property,
25 | `Could not find Column. Has it been defined yet?`
26 | );
27 | }) as ColumnMetadata;
28 | };
29 |
30 | export const setColumnMetadata = (
31 | constructor: EntityConstructor,
32 | columnMetadata: ColumnMetadata
33 | ): void => setMetadata(COLUMN_KEY, constructor, columnMetadata);
34 |
35 | export const getPrimaryColumnMetadata = (
36 | constructor: EntityConstructor
37 | ): ColumnMetadata | undefined => {
38 | return getColumnMetadatas(constructor).find(({ isPrimary }) => isPrimary);
39 | };
40 |
41 | const assertHasPrimaryColumn = (constructor: EntityConstructor): void => {
42 | if (getPrimaryColumnMetadata(constructor) === undefined)
43 | throw new PrimaryColumnMissingError(constructor);
44 | };
45 |
46 | export const getPrimaryColumnValue = (
47 | instance: BaseEntity,
48 | { failSilently } = { failSilently: false }
49 | ): Value => {
50 | const { constructor, cache } = getConstructorDatastoreCache(instance);
51 | assertHasPrimaryColumn(constructor);
52 | return cache.getPrimaryColumnValue(instance, { failSilently });
53 | };
54 |
55 | export const setPrimaryColumnValue = (
56 | instance: BaseEntity,
57 | value: Value
58 | ): void => {
59 | const { constructor, cache } = getConstructorDatastoreCache(instance);
60 | assertHasPrimaryColumn(constructor);
61 | return cache.setPrimaryColumnValue(instance, value);
62 | };
63 |
--------------------------------------------------------------------------------
/src/utils/datastore.test.ts:
--------------------------------------------------------------------------------
1 | import { Datastore, SearchStrategy } from "../Datastore/Datastore";
2 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
3 | import { paginateSearch } from "./datastore";
4 |
5 | describe(`paginateSearch`, () => {
6 | let datastore: Datastore;
7 |
8 | beforeEach(async () => {
9 | datastore = new MemoryDatastore();
10 |
11 | for (let i = 0; i < 9999; i++) {
12 | await datastore.write(`KEY:${i}`, `VALUE${i}`);
13 | }
14 | });
15 |
16 | it(`does go beyond the first page`, async () => {
17 | const results = await paginateSearch(datastore, {
18 | strategy: SearchStrategy.prefix,
19 | term: `KEY:`,
20 | });
21 | expect(results.keys.length).toBe(9999);
22 | expect(results.keys).toContain(`KEY:0`);
23 | expect(results.keys).toContain(`KEY:1`);
24 | expect(results.keys).toContain(`KEY:9998`);
25 | expect(results.cursor).toEqual(`9998`);
26 | expect(results.hasNextPage).toBeFalsy();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/utils/datastore.ts:
--------------------------------------------------------------------------------
1 | import { EntityConstructor } from "../Entity/Entity";
2 | import {
3 | Datastore,
4 | SearchOptions,
5 | SearchResult,
6 | Key,
7 | SearchStrategy,
8 | } from "../Datastore/Datastore";
9 | import { getEntityMetadata } from "./entities";
10 | import { SearchStrategyError } from "../Datastore/SearchStrategyError";
11 |
12 | export const getDatastore = (constructor: EntityConstructor): Datastore =>
13 | getEntityMetadata(constructor).datastore;
14 |
15 | export const paginateSearch = async (
16 | datastore: Datastore,
17 | options: SearchOptions
18 | ): Promise => {
19 | let results: SearchResult = await datastore.search(options);
20 |
21 | while (results.hasNextPage) {
22 | const result = await datastore.search({
23 | ...options,
24 | after: results.cursor,
25 | });
26 | const { keys, cursor, hasNextPage } = result;
27 | results = {
28 | keys: [...results.keys, ...keys],
29 | cursor,
30 | hasNextPage,
31 | };
32 | }
33 |
34 | return results;
35 | };
36 |
37 | export async function* keysFromSearch(
38 | datastore: Datastore,
39 | options: SearchOptions
40 | ): AsyncGenerator {
41 | let cursor;
42 | let hasNextPage = true;
43 | while (hasNextPage) {
44 | const searchResults: SearchResult = await datastore.search({
45 | ...options,
46 | after: cursor,
47 | });
48 | const { keys: queue } = searchResults;
49 | cursor = searchResults.cursor;
50 | hasNextPage = searchResults.hasNextPage;
51 |
52 | while (queue.length > 0) {
53 | const key = queue.shift();
54 | if (key !== undefined) yield key;
55 | }
56 | }
57 | }
58 |
59 | export const pickSearchStrategy = (datastore: Datastore): SearchStrategy => {
60 | let strategy;
61 | if (datastore.searchStrategies.indexOf(SearchStrategy.prefix) !== -1) {
62 | strategy = SearchStrategy.prefix;
63 | }
64 |
65 | if (strategy === undefined) {
66 | throw new SearchStrategyError(
67 | SearchStrategy.prefix,
68 | `Datastore does not support searching`
69 | );
70 | }
71 | return strategy;
72 | };
73 |
--------------------------------------------------------------------------------
/src/utils/entities.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from "../Cache/Cache";
2 | import { EntityConstructor, BaseEntity, ENTITY_KEY } from "../Entity/Entity";
3 | import { EntityLookupError } from "./errors";
4 | import { EntityMetadata } from "../Entity/entityMetadata";
5 | import { getDatastore } from "./datastore";
6 | import { getCache } from "./cache";
7 | import { Datastore } from "../Datastore/Datastore";
8 |
9 | export const getEntityMetadata = (
10 | constructor: EntityConstructor
11 | ): EntityMetadata => {
12 | const entityMetadata = Reflect.getMetadata(
13 | ENTITY_KEY,
14 | constructor
15 | ) as EntityMetadata;
16 |
17 | if (entityMetadata === undefined)
18 | throw new EntityLookupError(
19 | constructor,
20 | `Could not find metadata on Entity. Has it been defined yet?`
21 | );
22 |
23 | return entityMetadata;
24 | };
25 |
26 | export const setEntityMetadata = (
27 | constructor: EntityConstructor,
28 | entityMetadata: EntityMetadata
29 | ): void => Reflect.defineMetadata(ENTITY_KEY, entityMetadata, constructor);
30 |
31 | export const getConstructor = (instance: BaseEntity): EntityConstructor =>
32 | instance.constructor as EntityConstructor;
33 |
34 | export const createEmptyInstance = (
35 | constructor: EntityConstructor
36 | ): T => Object.create(constructor.prototype);
37 |
38 | export const getConstructorDatastoreCache = (
39 | instance: BaseEntity
40 | ): { constructor: EntityConstructor; datastore: Datastore; cache: Cache } => {
41 | const constructor = getConstructor(instance);
42 | const datastore = getDatastore(constructor);
43 | const cache = getCache(datastore);
44 |
45 | return {
46 | constructor,
47 | datastore,
48 | cache,
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------
1 | import { EntityConstructor, PropertyKey } from "../Entity/Entity";
2 | import { Key, Value } from "../Datastore/Datastore";
3 |
4 | export class KVORMError extends Error {
5 | constructor(message: string) {
6 | super(`kv-orm Error: ${message}`);
7 | this.name = `kv-orm Error`;
8 | }
9 | }
10 |
11 | export class SetupError extends KVORMError {
12 | constructor(message: string) {
13 | super(message);
14 | this.name = `SetupError`;
15 | }
16 | }
17 |
18 | export class MetadataError extends KVORMError {
19 | constructor(message: string) {
20 | super(message);
21 | this.name = `MetadataError`;
22 | }
23 | }
24 |
25 | export class ReadOnlyError extends KVORMError {
26 | constructor(
27 | constructor: EntityConstructor,
28 | property: PropertyKey,
29 | value: Value,
30 | message = `Unknown Error`
31 | ) {
32 | super(
33 | `Could not write Value, ${value}, to Column, ${property.toString()}, on Entity, ${
34 | constructor.name
35 | }: ${message}`
36 | );
37 | this.name = `ReadOnlyError`;
38 | }
39 | }
40 |
41 | export class EntityLookupError extends MetadataError {
42 | constructor(constructor: EntityConstructor, message = `Unknown Error`) {
43 | super(`Error looking up Entity, ${constructor.name}: ${message}`);
44 | this.name = `EntityLookupError`;
45 | }
46 | }
47 |
48 | export class ColumnLookupError extends MetadataError {
49 | constructor(
50 | constructor: EntityConstructor,
51 | property: PropertyKey,
52 | message = `Unknown Error`
53 | ) {
54 | super(
55 | `Error looking up Column, ${property.toString()}, on Entity, ${
56 | constructor.name
57 | }: ${message}`
58 | );
59 | this.name = `ColumnLookupError`;
60 | }
61 | }
62 |
63 | export class RelationshipLookupError extends MetadataError {
64 | constructor(
65 | constructor: EntityConstructor,
66 | property: PropertyKey,
67 | message = `Unknown Error`
68 | ) {
69 | super(
70 | `Error looking up Relationship, ${property.toString()}, on Entity, ${
71 | constructor.name
72 | }: ${message}`
73 | );
74 | this.name = `RelationshipLookupError`;
75 | }
76 | }
77 |
78 | export class PrimaryColumnMissingError extends MetadataError {
79 | constructor(
80 | constructor: EntityConstructor,
81 | message = `PrimaryColumn Missing`
82 | ) {
83 | super(`PrimaryColumn not found on Entity, ${constructor.name}: ${message}`);
84 | this.name = `PrimaryColumnMissingError`;
85 | }
86 | }
87 |
88 | export class InvalidKeyError extends MetadataError {
89 | constructor(key: Key, message = `Unknown Error`) {
90 | super(`Key, ${key}, is invalid: ${message}`);
91 | this.name = `InvalidKeyError`;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/utils/hydrate.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, EntityConstructor } from "../Entity/Entity";
2 | import { repositoryLoad } from "../Repository/repositoryLoad";
3 | import { Value, Datastore, Key } from "../Datastore/Datastore";
4 | import { getPrimaryColumnMetadata } from "./columns";
5 | import { pickSearchStrategy, keysFromSearch } from "./datastore";
6 | import { extractValueFromSearchKey } from "./keyGeneration";
7 |
8 | export type hydrator = (identifier: Value) => Promise;
9 |
10 | export const getHydrator = (constructor: EntityConstructor): hydrator => {
11 | return async (identifier: Value): Promise => {
12 | const primaryColumn = getPrimaryColumnMetadata(constructor);
13 | if (primaryColumn !== undefined) {
14 | return await repositoryLoad(constructor, identifier);
15 | } else {
16 | return await repositoryLoad(constructor);
17 | }
18 | };
19 | };
20 |
21 | export async function* hydrateMany(
22 | datastore: Datastore,
23 | searchKey: Key,
24 | hydrator: hydrator,
25 | { skip }: { skip: (primaryColumnValue: Key) => boolean } = {
26 | skip: () => false,
27 | }
28 | ): AsyncGenerator {
29 | const searchStrategy = pickSearchStrategy(datastore);
30 |
31 | const keyGenerator = keysFromSearch(datastore, {
32 | strategy: searchStrategy,
33 | term: searchKey,
34 | });
35 |
36 | for await (const value of keyGenerator) {
37 | const primaryColumnValue = extractValueFromSearchKey(
38 | datastore,
39 | value,
40 | searchKey
41 | );
42 | !skip(primaryColumnValue) && (yield await hydrator(primaryColumnValue));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/keyGeneration.test.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "../Column/Column";
2 | import { ColumnMetadata } from "../Column/columnMetadata";
3 | import {
4 | generatePropertyKey,
5 | generateIndexablePropertyKey,
6 | generateOneRelationshipKey,
7 | generateManyRelationshipKey,
8 | assertKeysDoNotContainSeparator,
9 | } from "./keyGeneration";
10 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
11 | import { Datastore } from "../Datastore/Datastore";
12 | import {
13 | getPrimaryColumnMetadata,
14 | getColumnMetadatas,
15 | getColumnMetadata,
16 | } from "./columns";
17 | import { Entity, EntityConstructor, BaseEntity } from "../Entity/Entity";
18 | import {
19 | EntityLookupError,
20 | InvalidKeyError,
21 | RelationshipLookupError,
22 | } from "./errors";
23 | import { ToOne } from "../Relationship/ToOne";
24 | import { getToOneRelationshipMetadata } from "./relationships";
25 |
26 | describe(`keyGeneration`, () => {
27 | let datastore: Datastore;
28 | let singletonEntityConstructor: EntityConstructor;
29 | let singletonEntityWithCustomKeysConstructor: EntityConstructor;
30 | let complexEntityConstructor: EntityConstructor;
31 | let complexEntityWithCustomKeysConstructor: EntityConstructor;
32 |
33 | beforeEach(() => {
34 | datastore = new MemoryDatastore();
35 |
36 | @Entity({ datastore })
37 | class SingletonEntity {
38 | @Column()
39 | public myProperty = `initial value`;
40 |
41 | @ToOne({ type: () => SingletonEntity, backRef: "" })
42 | public relationshipProperty: undefined;
43 | }
44 |
45 | singletonEntityConstructor = SingletonEntity;
46 |
47 | @Entity({ datastore, key: `CustomSingletonEntityKey` })
48 | class SingletonEntityWithCustomKeys {
49 | @Column({ key: `CustomPropertyKey` })
50 | public myProperty = `initial value`;
51 |
52 | @ToOne({
53 | type: () => SingletonEntityWithCustomKeys,
54 | key: `CustomRelationshipKey`,
55 | backRef: "",
56 | })
57 | public relationshipProperty: undefined;
58 | }
59 |
60 | singletonEntityWithCustomKeysConstructor = SingletonEntityWithCustomKeys;
61 |
62 | @Entity({ datastore })
63 | class ComplexEntity {
64 | @Column({ isPrimary: true })
65 | public id = 12345;
66 |
67 | @Column()
68 | public myProperty = `initial value`;
69 |
70 | @Column({ isIndexable: true, isUnique: true })
71 | public indexable = `abc@xyz.com`;
72 |
73 | @ToOne({ type: () => ComplexEntity, backRef: "" })
74 | public relationshipProperty: undefined;
75 | }
76 |
77 | complexEntityConstructor = ComplexEntity;
78 |
79 | @Entity({ datastore, key: `CustomComplexEntityKey` })
80 | class ComplexEntityWithCustomKeys {
81 | @Column({ isPrimary: true, key: `CustomPrimaryKey` })
82 | public id = 12345;
83 |
84 | @Column({ key: `CustomPropertyKey` })
85 | public myProperty = `initial value`;
86 |
87 | @Column({ isIndexable: true, isUnique: true, key: `CustomIndexableKey` })
88 | public indexable = `abc@xyz.com`;
89 |
90 | @ToOne({
91 | type: () => ComplexEntityWithCustomKeys,
92 | key: `CustomRelationshipKey`,
93 | backRef: "",
94 | })
95 | public relationshipProperty: undefined;
96 | }
97 |
98 | complexEntityWithCustomKeysConstructor = ComplexEntityWithCustomKeys;
99 | });
100 |
101 | describe(`generatePropertyKey`, () => {
102 | it(`generates a key for a singleton with default keys`, async () => {
103 | const instance = new singletonEntityConstructor();
104 | const columnMetadata = getColumnMetadata(
105 | singletonEntityConstructor,
106 | `myProperty`
107 | );
108 | expect(generatePropertyKey(instance, columnMetadata)).toEqual(
109 | `SingletonEntity:myProperty`
110 | );
111 | });
112 | it(`generates a key for a singleton with custom keys`, async () => {
113 | const instance = new singletonEntityWithCustomKeysConstructor();
114 | const columnMetadata = getColumnMetadata(
115 | singletonEntityWithCustomKeysConstructor,
116 | `myProperty`
117 | );
118 | expect(generatePropertyKey(instance, columnMetadata)).toEqual(
119 | `CustomSingletonEntityKey:CustomPropertyKey`
120 | );
121 | });
122 | it(`generates a key with default keys`, async () => {
123 | const instance = new complexEntityConstructor();
124 | const primaryColumnMetadata = getPrimaryColumnMetadata(
125 | complexEntityConstructor
126 | ) as ColumnMetadata;
127 | const otherColumnMetadata = getColumnMetadata(
128 | complexEntityConstructor,
129 | `myProperty`
130 | );
131 | const indexableColumnMetadata = getColumnMetadata(
132 | complexEntityConstructor,
133 | `indexable`
134 | );
135 |
136 | expect(generatePropertyKey(instance, primaryColumnMetadata)).toEqual(
137 | `ComplexEntity:12345:id`
138 | );
139 | expect(generatePropertyKey(instance, otherColumnMetadata)).toEqual(
140 | `ComplexEntity:12345:myProperty`
141 | );
142 | expect(generatePropertyKey(instance, indexableColumnMetadata)).toEqual(
143 | `ComplexEntity:12345:indexable`
144 | );
145 | });
146 | it(`generates a key with custom keys`, async () => {
147 | const instance = new complexEntityWithCustomKeysConstructor();
148 | const primaryColumnMetadata = getPrimaryColumnMetadata(
149 | complexEntityWithCustomKeysConstructor
150 | ) as ColumnMetadata;
151 | const otherColumnMetadata = getColumnMetadata(
152 | complexEntityWithCustomKeysConstructor,
153 | `myProperty`
154 | );
155 | const indexableColumnMetadata = getColumnMetadata(
156 | complexEntityWithCustomKeysConstructor,
157 | `indexable`
158 | );
159 |
160 | expect(generatePropertyKey(instance, primaryColumnMetadata)).toEqual(
161 | `CustomComplexEntityKey:12345:CustomPrimaryKey`
162 | );
163 | expect(generatePropertyKey(instance, otherColumnMetadata)).toEqual(
164 | `CustomComplexEntityKey:12345:CustomPropertyKey`
165 | );
166 | expect(generatePropertyKey(instance, indexableColumnMetadata)).toEqual(
167 | `CustomComplexEntityKey:12345:CustomIndexableKey`
168 | );
169 | });
170 | it(`throws an error for invalid Entities`, async () => {
171 | await expect(
172 | (async (): Promise => {
173 | // No @Entity() decorator
174 | class MyInvalidEntity {
175 | @Column()
176 | public myProperty = `initial value`;
177 | }
178 |
179 | const instance = new MyInvalidEntity();
180 | const columnMetadata = getColumnMetadatas(MyInvalidEntity)[0];
181 | generatePropertyKey(instance, columnMetadata);
182 | })()
183 | ).rejects.toThrow(EntityLookupError);
184 | });
185 | });
186 | describe(`generateIndexablePropertyKey`, () => {
187 | it(`generates a key`, () => {
188 | const instance = new complexEntityConstructor();
189 | const indexableColumnMetadata = getColumnMetadata(
190 | complexEntityConstructor,
191 | `indexable`
192 | );
193 | expect(
194 | generateIndexablePropertyKey(
195 | instance,
196 | indexableColumnMetadata,
197 | `abc@xyz.com`
198 | )
199 | ).toEqual(`ComplexEntity:indexable:abc@xyz.com`);
200 | });
201 | it(`generates a key with custom keys`, () => {
202 | const instance = new complexEntityWithCustomKeysConstructor();
203 | const indexableColumnMetadata = getColumnMetadata(
204 | complexEntityWithCustomKeysConstructor,
205 | `indexable`
206 | );
207 | expect(
208 | generateIndexablePropertyKey(
209 | instance,
210 | indexableColumnMetadata,
211 | `abc@xyz.com`
212 | )
213 | ).toEqual(`CustomComplexEntityKey:CustomIndexableKey:abc@xyz.com`);
214 | });
215 | });
216 | describe(`generateOneRelationshipKey`, () => {
217 | it(`generates a key for a singleton`, async () => {
218 | const instance = new singletonEntityConstructor();
219 | const relationshipMetadata = getToOneRelationshipMetadata(
220 | singletonEntityConstructor,
221 | `relationshipProperty`
222 | );
223 | expect(
224 | generateOneRelationshipKey(instance, relationshipMetadata)
225 | ).toEqual(generatePropertyKey(instance, relationshipMetadata));
226 | });
227 | it(`generates a key for a singleton with custom keys`, async () => {
228 | const instance = new singletonEntityWithCustomKeysConstructor();
229 | const relationshipMetadata = getToOneRelationshipMetadata(
230 | singletonEntityWithCustomKeysConstructor,
231 | `relationshipProperty`
232 | );
233 | expect(
234 | generateOneRelationshipKey(instance, relationshipMetadata)
235 | ).toEqual(generatePropertyKey(instance, relationshipMetadata));
236 | });
237 | it(`generates a key with default keys`, async () => {
238 | const instance = new complexEntityConstructor();
239 | const otherRelationshipMetadata = getToOneRelationshipMetadata(
240 | complexEntityConstructor,
241 | `relationshipProperty`
242 | );
243 |
244 | expect(generatePropertyKey(instance, otherRelationshipMetadata)).toEqual(
245 | generatePropertyKey(instance, otherRelationshipMetadata)
246 | );
247 | });
248 | it(`generates a key with custom keys`, async () => {
249 | const instance = new complexEntityWithCustomKeysConstructor();
250 | const otherRelationshipMetadata = getToOneRelationshipMetadata(
251 | complexEntityWithCustomKeysConstructor,
252 | `relationshipProperty`
253 | );
254 |
255 | expect(generatePropertyKey(instance, otherRelationshipMetadata)).toEqual(
256 | generatePropertyKey(instance, otherRelationshipMetadata)
257 | );
258 | });
259 | });
260 | // describe(`generateManyRelationshipKey`, () => {
261 | // let relationshipInstance: BaseEntity;
262 |
263 | // beforeEach(() => {
264 | // relationshipInstance = new complexEntityConstructor();
265 | // });
266 |
267 | // it(`generates a key for a singleton`, async () => {
268 | // const instance = new singletonEntityConstructor();
269 | // const relationshipMetadata = getToOneRelationshipMetadata(
270 | // singletonEntityConstructor,
271 | // `relationshipProperty`
272 | // );
273 | // expect(
274 | // generateManyRelationshipKey(
275 | // instance,
276 | // relationshipMetadata,
277 | // relationshipInstance
278 | // )
279 | // ).toEqual(`SingletonEntity:relationshipProperty:12345`);
280 | // });
281 | // it(`generates a key for a singleton with custom keys`, async () => {
282 | // const instance = new singletonEntityWithCustomKeysConstructor();
283 | // const relationshipMetadata = getToOneRelationshipMetadata(
284 | // singletonEntityWithCustomKeysConstructor,
285 | // `relationshipProperty`
286 | // );
287 | // expect(
288 | // generateManyRelationshipKey(
289 | // instance,
290 | // relationshipMetadata,
291 | // relationshipInstance
292 | // )
293 | // ).toEqual(`CustomSingletonEntityKey:CustomRelationshipKey:12345`);
294 | // });
295 | // it(`generates a key with default keys`, async () => {
296 | // const instance = new complexEntityConstructor();
297 | // const otherRelationshipMetadata = getToOneRelationshipMetadata(
298 | // complexEntityConstructor,
299 | // `relationshipProperty`
300 | // );
301 |
302 | // expect(
303 | // generateManyRelationshipKey(
304 | // instance,
305 | // otherRelationshipMetadata,
306 | // relationshipInstance
307 | // )
308 | // ).toEqual(`ComplexEntity:12345:relationshipProperty:12345`);
309 | // });
310 | // it(`generates a key with custom keys`, async () => {
311 | // const instance = new complexEntityWithCustomKeysConstructor();
312 | // const otherRelationshipMetadata = getToOneRelationshipMetadata(
313 | // complexEntityWithCustomKeysConstructor,
314 | // `relationshipProperty`
315 | // );
316 |
317 | // expect(
318 | // generateManyRelationshipKey(
319 | // instance,
320 | // otherRelationshipMetadata,
321 | // relationshipInstance
322 | // )
323 | // ).toEqual(`CustomComplexEntityKey:12345:CustomRelationshipKey:12345`);
324 | // });
325 | // });
326 |
327 | describe(`InvalidKeyError`, () => {
328 | it(`is thrown when generating a key with the key separator in it`, () => {
329 | expect(() => {
330 | assertKeysDoNotContainSeparator(datastore, [`test:bad:key`, `:`]);
331 | }).toThrow(InvalidKeyError);
332 | });
333 | });
334 | });
335 |
--------------------------------------------------------------------------------
/src/utils/keyGeneration.ts:
--------------------------------------------------------------------------------
1 | import "../metadata";
2 |
3 | import { Key, Value, Datastore } from "../Datastore/Datastore";
4 | import { BaseEntity, EntityConstructor } from "../Entity/Entity";
5 | import { ColumnMetadata } from "../Column/columnMetadata";
6 | import { getPrimaryColumnMetadata, getPrimaryColumnValue } from "./columns";
7 | import { getDatastore } from "./datastore";
8 | import { getConstructor, getEntityMetadata } from "./entities";
9 | import { InvalidKeyError } from "./errors";
10 | import { Metadata } from "./metadata";
11 | import {
12 | ToOneRelationshipMetadata,
13 | ToManyRelationshipMetadata,
14 | } from "../Relationship/relationshipMetadata";
15 |
16 | export const assertKeysDoNotContainSeparator = (
17 | datastore: Datastore,
18 | keys: Key[]
19 | ): void => {
20 | for (const key of keys) {
21 | if (key.toString().includes(datastore.keySeparator))
22 | throw new InvalidKeyError(key, `Key contains Datastore's Key Separator`);
23 | }
24 | };
25 |
26 | const getEntityKey = (constructor: EntityConstructor): Key => {
27 | const datastore = getDatastore(constructor);
28 | const entityMetadata = getEntityMetadata(constructor);
29 | assertKeysDoNotContainSeparator(datastore, [entityMetadata.key]);
30 | return entityMetadata.key;
31 | };
32 |
33 | // Author:UUID-HERE:name
34 | // or, if singleton, ApplicationConfiguration:password
35 | export const generatePropertyKey = (
36 | instance: BaseEntity,
37 | metadata: Metadata
38 | ): Key => {
39 | const constructor = getConstructor(instance);
40 | const datastore = getDatastore(constructor);
41 | const keys = [getEntityKey(constructor)];
42 | const primaryColumnMetadata = getPrimaryColumnMetadata(constructor);
43 |
44 | if (primaryColumnMetadata) {
45 | keys.push(getPrimaryColumnValue(instance));
46 | }
47 |
48 | keys.push(metadata.key);
49 |
50 | assertKeysDoNotContainSeparator(datastore, keys);
51 | return keys.join(datastore.keySeparator);
52 | };
53 |
54 | // Author:email:abc@xyz.com:UUID-HERE
55 | // or, if isUnique, Author:email:abc@xyz.com
56 | export const generateIndexablePropertyKey = (
57 | instance: BaseEntity,
58 | columnMetadata: ColumnMetadata,
59 | value: Value
60 | ): Key => {
61 | const constructor = getConstructor(instance);
62 | const datastore = getDatastore(constructor);
63 |
64 | const keys = [getEntityKey(constructor), columnMetadata.key, value];
65 |
66 | if (!columnMetadata.isUnique) {
67 | keys.push(generateRelationshipKey(instance));
68 | }
69 |
70 | assertKeysDoNotContainSeparator(datastore, keys);
71 | return keys.join(datastore.keySeparator);
72 | };
73 |
74 | // Author:email:abc@xyz.com
75 | export const getUniqueSearchKey = (
76 | constructor: EntityConstructor,
77 | columnMetadata: ColumnMetadata,
78 | value: Value
79 | ) => {
80 | const datastore = getDatastore(constructor);
81 |
82 | const keys = [getEntityKey(constructor), columnMetadata.key, value];
83 |
84 | assertKeysDoNotContainSeparator(datastore, keys);
85 | return keys.join(datastore.keySeparator);
86 | };
87 |
88 | export const getIndexableSearchKey = (
89 | constructor: EntityConstructor,
90 | columnMetadata: ColumnMetadata,
91 | value: Value
92 | ) => {
93 | const datastore = getDatastore(constructor);
94 |
95 | return (
96 | getUniqueSearchKey(constructor, columnMetadata, value) +
97 | datastore.keySeparator
98 | );
99 | };
100 |
101 | // UUID-HERE
102 | // or, if singleton, ApplicationConfiguration
103 | export const generateRelationshipKey = (instance: BaseEntity): Key => {
104 | const constructor = getConstructor(instance);
105 | const datastore = getDatastore(constructor);
106 | const primaryColumnMetadata = getPrimaryColumnMetadata(constructor);
107 |
108 | let key;
109 | if (primaryColumnMetadata) {
110 | key = getPrimaryColumnValue(instance);
111 | } else {
112 | key = getEntityKey(constructor);
113 | }
114 |
115 | assertKeysDoNotContainSeparator(datastore, [key]);
116 | return key;
117 | };
118 |
119 | // Author:UUID-HERE:passport
120 | export const generateOneRelationshipKey = (
121 | instance: BaseEntity,
122 | toOneRelationshipMetadata: ToOneRelationshipMetadata
123 | ): Key => generatePropertyKey(instance, toOneRelationshipMetadata);
124 |
125 | // Author:UUID-HERE:books:UUID-HERE
126 | export const generateManyRelationshipKey = (
127 | instance: BaseEntity,
128 | toManyRelationshipMetadata: ToManyRelationshipMetadata,
129 | relationshipInstance: BaseEntity
130 | ): Key => {
131 | const constructor = getConstructor(instance);
132 | const datastore = getDatastore(constructor);
133 | const keys = [
134 | generatePropertyKey(instance, toManyRelationshipMetadata),
135 | generateRelationshipKey(relationshipInstance),
136 | ];
137 | return keys.join(datastore.keySeparator);
138 | };
139 |
140 | export const generateManyRelationshipSearchKey = (
141 | instance: BaseEntity,
142 | toManyRelationshipMetadata: ToManyRelationshipMetadata
143 | ): Key => {
144 | const constructor = getConstructor(instance);
145 | const datastore = getDatastore(constructor);
146 |
147 | return (
148 | generatePropertyKey(instance, toManyRelationshipMetadata) +
149 | datastore.keySeparator
150 | );
151 | };
152 |
153 | export const extractValueFromSearchKey = (
154 | datastore: Datastore,
155 | key: Key,
156 | searchKey: Key
157 | ): Key => key.split(searchKey)[1].split(datastore.keySeparator)[0];
158 |
--------------------------------------------------------------------------------
/src/utils/metadata.ts:
--------------------------------------------------------------------------------
1 | import "../metadata";
2 |
3 | import { EntityConstructor, BaseEntity } from "../Entity/Entity";
4 | import { SetupError } from "./errors";
5 | import { Key } from "../Datastore/Datastore";
6 | import { getColumnMetadatas } from "./columns";
7 | import {
8 | getToOneRelationshipMetadatas,
9 | getToManyRelationshipMetadatas,
10 | } from "./relationships";
11 |
12 | export interface Metadata {
13 | key: Key;
14 | property?: PropertyKey; // TODO: Clean-up. Separate Relationship, Column and Entity Metadatas properly
15 | }
16 |
17 | export class MetadataSetupError extends SetupError {
18 | constructor(
19 | constructor: BaseEntity,
20 | metadata: Metadata,
21 | message = `Unknown Error`
22 | ) {
23 | super(
24 | `Could not setup the Column, ${metadata.key}, on Entity, ${constructor.name}: ${message}`
25 | );
26 | this.name = `MetadataSetupError`;
27 | }
28 | }
29 |
30 | export const assertKeyNotInUse = (
31 | constructor: EntityConstructor,
32 | metadata: Metadata,
33 | {
34 | getMetadatas,
35 | }: {
36 | getMetadatas: (constructor: EntityConstructor) => Metadata[];
37 | }
38 | ): void => {
39 | const metadatas = getMetadatas(constructor);
40 | const keysInUse = metadatas.map((metadata) => metadata.key);
41 |
42 | if (keysInUse.indexOf(metadata.key) !== -1)
43 | throw new MetadataSetupError(
44 | constructor,
45 | metadata,
46 | `Key is already in use`
47 | );
48 | };
49 |
50 | export const getMetadatas = (
51 | key: symbol,
52 | constructor: EntityConstructor
53 | ): Metadata[] => Reflect.getMetadata(key, constructor) || [];
54 |
55 | const setMetadatas = (
56 | key: symbol,
57 | constructor: EntityConstructor,
58 | metadatas: Metadata[]
59 | ): void => Reflect.defineMetadata(key, metadatas, constructor);
60 |
61 | export const getMetadata = (
62 | metadatas: Metadata[],
63 | property: PropertyKey,
64 | throwError: () => void
65 | ): Metadata => {
66 | const metadata = metadatas.find(({ property: p }) => p === property);
67 | if (metadata === undefined) throwError();
68 |
69 | return metadata as Metadata;
70 | };
71 |
72 | export const setMetadata = (
73 | key: symbol,
74 | constructor: EntityConstructor,
75 | metadata: Metadata
76 | ): void => {
77 | const metadatas = getMetadatas(key, constructor);
78 | metadatas.push(metadata);
79 | setMetadatas(key, constructor, metadatas);
80 | };
81 |
82 | export const getPropertyMetadatas = (constructor: EntityConstructor) => [
83 | ...getColumnMetadatas(constructor),
84 | ...getToOneRelationshipMetadatas(constructor),
85 | ...getToManyRelationshipMetadatas(constructor),
86 | ];
87 |
--------------------------------------------------------------------------------
/src/utils/relationships.test.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../Entity/Entity";
2 | import { Datastore } from "../Datastore/Datastore";
3 | import { MemoryDatastore } from "../MemoryDatastore/MemoryDatastore";
4 | import { getToOneRelationshipMetadata } from "./relationships";
5 | import { RelationshipLookupError } from "./errors";
6 |
7 | describe(`relationships`, () => {
8 | let datastore: Datastore;
9 | beforeEach(() => {
10 | datastore = new MemoryDatastore();
11 | });
12 | describe(`RelationshipLookupError`, () => {
13 | it(`is thrown when it does not exist`, () => {
14 | @Entity({ datastore })
15 | class MyEntity {}
16 |
17 | expect(() => {
18 | getToOneRelationshipMetadata(MyEntity, `fakeRelationship`);
19 | }).toThrow(RelationshipLookupError);
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/utils/relationships.ts:
--------------------------------------------------------------------------------
1 | import { EntityConstructor, PropertyKey, BaseEntity } from "../Entity/Entity";
2 | import {
3 | TOONE_RELATIONSHIP_KEY,
4 | TOMANY_RELATIONSHIP_KEY,
5 | ToOneRelationshipMetadata,
6 | ToManyRelationshipMetadata,
7 | RelationshipMetadata,
8 | } from "../Relationship/relationshipMetadata";
9 | import { RelationshipLookupError } from "./errors";
10 | import { getMetadatas, getMetadata, setMetadata } from "./metadata";
11 | import { getConstructor } from "./entities";
12 |
13 | export const getToOneRelationshipMetadatas = (
14 | constructor: EntityConstructor
15 | ): ToOneRelationshipMetadata[] =>
16 | getMetadatas(
17 | TOONE_RELATIONSHIP_KEY,
18 | constructor
19 | ) as ToOneRelationshipMetadata[];
20 |
21 | export const getToManyRelationshipMetadatas = (
22 | constructor: EntityConstructor
23 | ): ToManyRelationshipMetadata[] =>
24 | getMetadatas(
25 | TOMANY_RELATIONSHIP_KEY,
26 | constructor
27 | ) as ToManyRelationshipMetadata[];
28 |
29 | export const getToOneRelationshipMetadata = (
30 | constructor: EntityConstructor,
31 | property: PropertyKey
32 | ): ToOneRelationshipMetadata => {
33 | const toOneRelationshipMetadatas = getToOneRelationshipMetadatas(constructor);
34 | return getMetadata(toOneRelationshipMetadatas, property, () => {
35 | throw new RelationshipLookupError(
36 | constructor,
37 | property,
38 | `Could not find ToOne Relationship. Has it been defined yet?`
39 | );
40 | }) as ToOneRelationshipMetadata;
41 | };
42 |
43 | export const getToManyRelationshipMetadata = (
44 | constructor: EntityConstructor,
45 | property: PropertyKey
46 | ): ToManyRelationshipMetadata => {
47 | const toManyRelationshipMetadatas = getToManyRelationshipMetadatas(
48 | constructor
49 | );
50 | return getMetadata(toManyRelationshipMetadatas, property, () => {
51 | throw new RelationshipLookupError(
52 | constructor,
53 | property,
54 | `Could not find ToMany Relationship. Has it been defined yet?`
55 | );
56 | }) as ToManyRelationshipMetadata;
57 | };
58 |
59 | export const getRelationshipMetadata = (
60 | constructor: EntityConstructor,
61 | property: PropertyKey
62 | ): RelationshipMetadata => {
63 | const relationshipMetadatas = [
64 | ...getToOneRelationshipMetadatas(constructor),
65 | ...getToManyRelationshipMetadatas(constructor),
66 | ];
67 |
68 | return getMetadata(relationshipMetadatas, property, () => {
69 | throw new RelationshipLookupError(
70 | constructor,
71 | property,
72 | `Could not find Relationship. Has it been defined yet?`
73 | );
74 | }) as RelationshipMetadata;
75 | };
76 |
77 | export const setToOneRelationshipMetadata = (
78 | constructor: EntityConstructor,
79 | toOneRelationshipMetadata: ToOneRelationshipMetadata
80 | ): void =>
81 | setMetadata(TOONE_RELATIONSHIP_KEY, constructor, toOneRelationshipMetadata);
82 |
83 | export const setToManyRelationshipMetadata = (
84 | constructor: EntityConstructor,
85 | toManyRelationshipMetadata: ToManyRelationshipMetadata
86 | ): void =>
87 | setMetadata(TOMANY_RELATIONSHIP_KEY, constructor, toManyRelationshipMetadata);
88 |
89 | export async function* arrayToAsyncGenerator(array: T[]) {
90 | for (const item of array) {
91 | yield item;
92 | }
93 | }
94 |
95 | export async function* combineAsyncGenerators(
96 | a: AsyncGenerator,
97 | b: AsyncGenerator
98 | ): AsyncGenerator {
99 | for await (const item of a) {
100 | yield item;
101 | }
102 | for await (const item of b) {
103 | yield item;
104 | }
105 | }
106 |
107 | export const makeCachingGenerator = (
108 | instance: BaseEntity,
109 | toManyRelationshipMetadata: ToManyRelationshipMetadata
110 | ) => {
111 | const constructor = getConstructor(instance);
112 |
113 | return async function* cacheGenerator(instances: AsyncGenerator) {
114 | for await (const relationshipInstance of instances) {
115 | const cachedRelationshipInstances = [
116 | ...(toManyRelationshipMetadata.instances.get(instance) || []),
117 | relationshipInstance,
118 | ];
119 | toManyRelationshipMetadata.instances.set(
120 | instance,
121 | cachedRelationshipInstances
122 | );
123 | setToManyRelationshipMetadata(constructor, toManyRelationshipMetadata);
124 |
125 | yield relationshipInstance;
126 | }
127 | };
128 | };
129 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "ES2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
7 | // "lib": [], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | "declaration": true /* Generates corresponding '.d.ts' file. */,
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | "sourceMap": true /* Generates corresponding '.map' file. */,
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./dist/" /* Redirect output structure to the directory. */,
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true /* Enable all strict type-checking options. */,
27 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
28 | "strictNullChecks": true /* Enable strict null checks. */,
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
33 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [] /* List of folders to include type definitions from. */,
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
52 |
53 | "skipLibCheck": true,
54 | /* Source Map Options */
55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
59 |
60 | /* Experimental Options */
61 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
62 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
63 | },
64 | "exclude": [
65 | "node_modules/**",
66 | "dist",
67 | "src/**/*.test.ts",
68 | "src/**/*.testhelpers.ts"
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------