Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7029c10d9b | ||
|
b34b8765c1 | ||
|
8b62947466 | ||
|
3a684a177c | ||
|
def5b75101 | ||
|
aa38fae8cc | ||
|
41aa5d951a | ||
|
79cdca410e | ||
|
e799da6a48 | ||
|
bcefbb7425 | ||
|
e1d03577dd | ||
|
231895c7cd | ||
|
e282a887c3 | ||
|
14a46a71b3 | ||
|
ead279ca5d | ||
|
50261ac29e | ||
|
017f735613 | ||
|
8e619112cd | ||
|
d0942e59d9 | ||
|
3b6e00ec1d | ||
|
d92396967a | ||
|
ab5c1f70c4 | ||
|
20a969c829 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
*.db
|
17
.idea/dataSources.xml
Normal file
17
.idea/dataSources.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="sqlite.test" uuid="4938db2d-685c-43b9-a8f6-d56ef172daf6">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/src/tests/repo/sqlite.test.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
<libraries>
|
||||||
|
<library>
|
||||||
|
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.34.0/sqlite-jdbc-3.34.0.jar</url>
|
||||||
|
</library>
|
||||||
|
</libraries>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
.idea/runConfigurations.xml
Normal file
10
.idea/runConfigurations.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
72
HISTORY.md
72
HISTORY.md
@ -2,6 +2,78 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.5.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.4.0...v1.5.0) (2022-07-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* пример обновления версии ([b34b876](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/b34b8765c1415912712729dfead1d6c99a57327e))
|
||||||
|
|
||||||
|
## [1.4.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.3.3...v1.4.0) (2022-07-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* доработка ридми ([3a684a1](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/3a684a177c28b1682ad776bd62d84488fa0924b7))
|
||||||
|
* Принес класс хранилища SQLite и зависимости ([79cdca4](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/79cdca410e9c8316845c5373fe4c0449d2eb26c4))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Исправил ошибку метода Entity.isPersisted ([aa38fae](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/aa38fae8cc52b84837d91d7e244d7be1efb119da))
|
||||||
|
* Исправил ошибку установки внутреного айди для записи при создании сущности ([41aa5d9](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/41aa5d951ae1e5ddcca822621363c605c2fcf08b))
|
||||||
|
* обновил ts ([def5b75](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/def5b7510137f0da0a7432e5a62498c6a115c6a9))
|
||||||
|
* Перенес модули монги в optionalDependencies ([e799da6](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/e799da6a48ed975f9aa1847dbca49a14ea3e05bf))
|
||||||
|
|
||||||
|
### [1.3.3](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.3.2...v1.3.3) (2021-10-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Потерялась зависимость rfdc ([e1d0357](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/e1d03577dd973c6e651a8d175fbabcf5a8c1fd53))
|
||||||
|
|
||||||
|
### [1.3.2](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.3.1...v1.3.2) (2021-10-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* трансформер доступен для дочерних классов ([e282a88](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/e282a887c3e8c5fc6d159da9881f13adb2f527b4))
|
||||||
|
|
||||||
|
### [1.3.1](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.3.0...v1.3.1) (2021-10-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* обновил API типов ([ead279c](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/ead279ca5dbcc391aa30b224195295a4d1f81684))
|
||||||
|
|
||||||
|
## [1.3.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.2.0...v1.3.0) (2021-10-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* добавил управление трансформацией данных для репозитория ([017f735](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/017f7356134c306c9ba46b19385c3e96444c613b))
|
||||||
|
|
||||||
|
## [1.2.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.1.3...v1.2.0) (2021-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* добавил Entity Manager ([d0942e5](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/d0942e59d97d861aa3368ee96a3a15f0418263e4))
|
||||||
|
|
||||||
|
### [1.1.3](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.1.2...v1.1.3) (2021-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* путь к файлу типов ([d923969](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/d92396967a79b51fe63d6620eb8b2169260f4ec7))
|
||||||
|
|
||||||
|
### [1.1.2](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.1.1...v1.1.2) (2021-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* заменил все unknow на any ([20a969c](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/20a969c829d5d4d9ec5b4f001f6d13be3570a97d))
|
||||||
|
|
||||||
### [1.1.1](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.1.0...v1.1.1) (2021-09-29)
|
### [1.1.1](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.1.0...v1.1.1) (2021-09-29)
|
||||||
|
|
||||||
## [1.1.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.0.2...v1.1.0) (2021-09-29)
|
## [1.1.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.0.2...v1.1.0) (2021-09-29)
|
||||||
|
31
README.md
31
README.md
@ -4,3 +4,34 @@
|
|||||||
|
|
||||||
Позволяет задать модели для таблиц или коллекций и предоставляет менеджер сущностей для манипуляций с ними.
|
Позволяет задать модели для таблиц или коллекций и предоставляет менеджер сущностей для манипуляций с ними.
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Процедура обновления версии
|
||||||
|
|
||||||
|
В сообщении к комитам используем формат записи [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
|
||||||
|
Пример(!) процедуры фиксации изменений и обновлении версии:
|
||||||
|
```shell
|
||||||
|
npm run precommit
|
||||||
|
git add .
|
||||||
|
git commit -m'fix: исправил баг'
|
||||||
|
# что-то еще делаем
|
||||||
|
npm run precommit
|
||||||
|
git add .
|
||||||
|
git commit -m'feat: запилил новую фичу'
|
||||||
|
```
|
||||||
|
|
||||||
|
После того как все изменения зафиксированы, делаем:
|
||||||
|
```shell
|
||||||
|
npm run release
|
||||||
|
git push --follow-tags origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Примеры использования
|
||||||
|
|
||||||
|
TODO
|
@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
exports.default = {
|
exports.default = {
|
||||||
roots: ['<rootDir>/src/tests/'],
|
roots: ['<rootDir>/src/tests/'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
'^.+\\.(ts|tsx|js)$': 'ts-jest',
|
||||||
},
|
},
|
||||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
45
orm.d.ts
vendored
45
orm.d.ts
vendored
@ -1,10 +1,13 @@
|
|||||||
/** @public */
|
/** @public */
|
||||||
export declare abstract class Data {
|
export declare abstract class Data {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
/**
|
||||||
|
* @deprecated use fromObject()
|
||||||
|
*/
|
||||||
static assign(vo: Data, values: ValuesObject): Data;
|
static assign(vo: Data, values: ValuesObject): Data;
|
||||||
set(key: string, value: any): void;
|
set(key: string, value: any): void;
|
||||||
fromObject(object: ValuesObject): void;
|
fromObject(object: ValuesObject): void;
|
||||||
toObject(exclude?: string[]): Record<string, any>;
|
toObject(exclude?: string[]): ValuesObject;
|
||||||
abstract uniqKey(): string;
|
abstract uniqKey(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,6 +29,20 @@ export declare abstract class Entity<T extends Data> {
|
|||||||
/** @public */
|
/** @public */
|
||||||
export declare type EntityConstructor<T extends Data> = new (data?: T) => Entity<T>;
|
export declare type EntityConstructor<T extends Data> = new (data?: T) => Entity<T>;
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export declare class EntityManager {
|
||||||
|
private readonly _storage;
|
||||||
|
private _saveMap;
|
||||||
|
private _removeMap;
|
||||||
|
constructor(storage: Storage_2);
|
||||||
|
persist(entity: Entity<any>): this;
|
||||||
|
persistMany(entities: Entity<any>[]): this;
|
||||||
|
remove(entity: Entity<any>): this;
|
||||||
|
forget(): this;
|
||||||
|
flush(): Promise<this>;
|
||||||
|
refresh(entity: Entity<any>): Promise<Entity<any>>;
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export declare class ErrEntityHasNoUniqKeyValue extends Error {
|
export declare class ErrEntityHasNoUniqKeyValue extends Error {
|
||||||
}
|
}
|
||||||
@ -52,6 +69,7 @@ export declare abstract class Repo<T extends Entity<any>> {
|
|||||||
protected _entity: T;
|
protected _entity: T;
|
||||||
protected _limit: number;
|
protected _limit: number;
|
||||||
protected _offset: number;
|
protected _offset: number;
|
||||||
|
protected _transformer: (obj: any) => any;
|
||||||
/**
|
/**
|
||||||
* Возвращает объект соотв. сущности, например new App()
|
* Возвращает объект соотв. сущности, например new App()
|
||||||
*/
|
*/
|
||||||
@ -60,9 +78,8 @@ export declare abstract class Repo<T extends Entity<any>> {
|
|||||||
* Возвращает название коллекции/таблицы/... хранящей записи соотв. сущностей
|
* Возвращает название коллекции/таблицы/... хранящей записи соотв. сущностей
|
||||||
*/
|
*/
|
||||||
abstract Name(): string;
|
abstract Name(): string;
|
||||||
_transformer: (object: any) => any;
|
constructor(storage: Storage_2, transformer?: (obj: any) => any);
|
||||||
constructor(storage: Storage_2);
|
setTransformer(transformer?: (obj: any) => any): this;
|
||||||
resetTransformer(): void;
|
|
||||||
get storage(): Storage_2;
|
get storage(): Storage_2;
|
||||||
save(entity: T): Promise<this>;
|
save(entity: T): Promise<this>;
|
||||||
findById(id: string): Promise<T | null>;
|
findById(id: string): Promise<T | null>;
|
||||||
@ -70,10 +87,10 @@ export declare abstract class Repo<T extends Entity<any>> {
|
|||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_findByParams(parameters: Record<string, unknown>, limit?: number, order?: Record<string, unknown>): Promise<T[]>;
|
_findByParams(parameters: Record<string, any>, limit?: number, order?: Record<string, any>): Promise<T[]>;
|
||||||
findMany(parameters: Record<string, unknown>, order?: Record<string, unknown>): Promise<T[]>;
|
findMany(parameters: Record<string, any>, order?: Record<string, any>): Promise<T[]>;
|
||||||
shift(limit?: number, offset?: number): this;
|
shift(limit?: number, offset?: number): this;
|
||||||
count(query?: Record<string, unknown>): Promise<number>;
|
count(query?: Record<string, any>): Promise<number>;
|
||||||
remove(entity: T, silent?: boolean): Promise<this>;
|
remove(entity: T, silent?: boolean): Promise<this>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,17 +98,17 @@ export declare abstract class Repo<T extends Entity<any>> {
|
|||||||
declare abstract class Storage_2 {
|
declare abstract class Storage_2 {
|
||||||
protected _dsn: string;
|
protected _dsn: string;
|
||||||
constructor(dsn: string);
|
constructor(dsn: string);
|
||||||
abstract find(name: string, query: Record<string, unknown>): Promise<StorageCursor>;
|
abstract find(name: string, query: Record<string, any>): Promise<StorageCursor>;
|
||||||
abstract save(name: string, uniqKey: string, data: Record<string, unknown>): Promise<string>;
|
abstract save(name: string, uniqKey: string, data: Record<string, any>): Promise<string>;
|
||||||
abstract createSession(): StorageSession;
|
abstract createSession(): StorageSession;
|
||||||
abstract count(name: string, query?: Record<string, unknown>): Promise<number>;
|
abstract count(name: string, query?: Record<string, any>): Promise<number>;
|
||||||
abstract remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean>;
|
abstract remove(name: string, uniqKey: string, uniqVal: string | number): Promise<boolean>;
|
||||||
}
|
}
|
||||||
export { Storage_2 as Storage }
|
export { Storage_2 as Storage }
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export declare interface StorageCursor {
|
export declare interface StorageCursor {
|
||||||
limit(number_: number): StorageCursor;
|
limit(number: number): StorageCursor;
|
||||||
sort(parameters: Record<string, any>): StorageCursor;
|
sort(parameters: Record<string, any>): StorageCursor;
|
||||||
skip(offset: number): StorageCursor;
|
skip(offset: number): StorageCursor;
|
||||||
toArray(): Promise<any[]>;
|
toArray(): Promise<any[]>;
|
||||||
@ -99,8 +116,8 @@ export declare interface StorageCursor {
|
|||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export declare abstract class StorageSession {
|
export declare abstract class StorageSession {
|
||||||
abstract start(options?: Record<string, unknown>): Promise<void>;
|
abstract start(options?: Record<string, any>): Promise<void>;
|
||||||
abstract commit(fn: () => any, options?: Record<string, unknown>): Promise<void>;
|
abstract commit(fn: () => any, options?: Record<string, any>): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
1793
package-lock.json
generated
1793
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ivna-orm",
|
"name": "ivna-orm",
|
||||||
"version": "1.1.1",
|
"version": "1.5.0",
|
||||||
"description": "Mini ORM for convenience",
|
"description": "Mini ORM for convenience",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
@ -11,13 +11,12 @@
|
|||||||
"src/app/**/*",
|
"src/app/**/*",
|
||||||
"orm.d.ts"
|
"orm.d.ts"
|
||||||
],
|
],
|
||||||
"types": "dist/index.d.ts",
|
"types": "orm.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --no-warnings node_modules/.bin/jest --runInBand --forceExit",
|
"test": "node --no-warnings node_modules/.bin/jest --runInBand --forceExit",
|
||||||
"build": "rimraf dist && tsc -b -v",
|
"build": "rimraf dist && tsc -b -v",
|
||||||
"build:api": "npm run build && api-extractor run && rimraf 'dist/**/*.d.ts*'",
|
"build:api": "npm run build && api-extractor run && rimraf 'dist/**/*.d.ts*'",
|
||||||
"release": "standard-version -i HISTORY.md",
|
"release": "standard-version -i HISTORY.md"
|
||||||
"prepare": "npm test && npm run build:api"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -43,15 +42,16 @@
|
|||||||
"jest-teamcity": "^1.10.0",
|
"jest-teamcity": "^1.10.0",
|
||||||
"nanoid": "^3.1.28",
|
"nanoid": "^3.1.28",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"rfdc": "^1.3.0",
|
|
||||||
"standard-version": "^9.3.1",
|
"standard-version": "^9.3.1",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^27.0.5",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"typescript": "^4.4.3"
|
"typescript": "^4.5.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"optionalDependencies": {
|
||||||
"mongodb": "^4.1.2",
|
"mongodb": "^4.1.2",
|
||||||
"mongodb-client-encryption": "^1.2.7"
|
"mongodb-client-encryption": "^1.2.7",
|
||||||
|
"sqlite": "^4.0.23",
|
||||||
|
"sqlite3": "^5.0.2"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
@ -60,5 +60,8 @@
|
|||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 4
|
"tabWidth": 4
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"rfdc": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ export type ValuesObject = Record<string, any>
|
|||||||
export abstract class Data {
|
export abstract class Data {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use fromObject()
|
||||||
|
*/
|
||||||
static assign(vo: Data, values: ValuesObject): Data {
|
static assign(vo: Data, values: ValuesObject): Data {
|
||||||
for (const key of Object.getOwnPropertyNames(values)) {
|
for (const key of Object.getOwnPropertyNames(values)) {
|
||||||
vo[key] = values[key]
|
vo[key] = values[key]
|
||||||
@ -26,7 +29,7 @@ export abstract class Data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toObject(exclude: string[] = []): Record<string, any> {
|
toObject(exclude: string[] = []): ValuesObject {
|
||||||
const object = {} as ValuesObject
|
const object = {} as ValuesObject
|
||||||
for (const key of Object.getOwnPropertyNames(this)) {
|
for (const key of Object.getOwnPropertyNames(this)) {
|
||||||
if (key.startsWith('__')) {
|
if (key.startsWith('__')) {
|
||||||
|
@ -48,7 +48,7 @@ export abstract class Entity<T extends Data> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPersisted(): boolean {
|
isPersisted(): boolean {
|
||||||
return this.__id !== ''
|
return this.__id !== undefined && this.__id !== ''
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
|
64
src/app/entity/manager.ts
Normal file
64
src/app/entity/manager.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Entity } from '../entity'
|
||||||
|
import { Storage } from '../storage'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export class EntityManager {
|
||||||
|
private readonly _storage: Storage
|
||||||
|
private _saveMap: Map<string, Entity<any>>
|
||||||
|
private _removeMap: Map<string, Entity<any>>
|
||||||
|
|
||||||
|
constructor(storage: Storage) {
|
||||||
|
this._storage = storage
|
||||||
|
this._saveMap = new Map()
|
||||||
|
this._removeMap = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
persist(entity: Entity<any>): this {
|
||||||
|
const key = [entity.constructor.name, entity.getUniqKey()].join('&')
|
||||||
|
this._saveMap.set(key, entity)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
persistMany(entities: Entity<any>[]): this {
|
||||||
|
for (let entity of entities) {
|
||||||
|
this.persist(entity)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(entity: Entity<any>): this {
|
||||||
|
const key = [entity.constructor.name, entity.getUniqKey()].join('&')
|
||||||
|
this._removeMap.set(key, entity)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
forget(): this {
|
||||||
|
this._saveMap = new Map()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush() {
|
||||||
|
if (!this._saveMap.size && !this._removeMap.size) return this
|
||||||
|
|
||||||
|
const session = this._storage.createSession()
|
||||||
|
await session.start()
|
||||||
|
await session.commit(async () => {
|
||||||
|
for (let en of this._saveMap.values()) {
|
||||||
|
await en._getRepo(this._storage).save(en)
|
||||||
|
}
|
||||||
|
this._saveMap = new Map()
|
||||||
|
|
||||||
|
for (let en of this._removeMap.values()) {
|
||||||
|
await en._getRepo(this._storage).remove(en)
|
||||||
|
}
|
||||||
|
this._removeMap = new Map()
|
||||||
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(entity: Entity<any>) {
|
||||||
|
const repo = entity._getRepo(this._storage)
|
||||||
|
return repo.findById(entity.getUniqKey())
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,8 @@ export abstract class Repo<T extends Entity<any>> {
|
|||||||
protected _limit = 0
|
protected _limit = 0
|
||||||
protected _offset = 0
|
protected _offset = 0
|
||||||
|
|
||||||
|
protected _transformer: (obj: any) => any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает объект соотв. сущности, например new App()
|
* Возвращает объект соотв. сущности, например new App()
|
||||||
*/
|
*/
|
||||||
@ -25,15 +27,16 @@ export abstract class Repo<T extends Entity<any>> {
|
|||||||
*/
|
*/
|
||||||
abstract Name(): string
|
abstract Name(): string
|
||||||
|
|
||||||
_transformer: (object: any) => any
|
constructor(storage: Storage, transformer?: (obj: any) => any) {
|
||||||
|
|
||||||
constructor(storage: Storage) {
|
|
||||||
this._storage = storage
|
this._storage = storage
|
||||||
this._entity = this.Entity()
|
this._entity = this.Entity()
|
||||||
this.resetTransformer()
|
this.setTransformer(transformer)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetTransformer() {
|
setTransformer(transformer?: (obj: any) => any): this {
|
||||||
|
if (transformer) {
|
||||||
|
this._transformer = transformer
|
||||||
|
} else {
|
||||||
this._transformer = function (object: any): any {
|
this._transformer = function (object: any): any {
|
||||||
const entity = this.Entity()
|
const entity = this.Entity()
|
||||||
entity.data.fromObject(object)
|
entity.data.fromObject(object)
|
||||||
@ -41,6 +44,8 @@ export abstract class Repo<T extends Entity<any>> {
|
|||||||
return entity
|
return entity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
get storage(): Storage {
|
get storage(): Storage {
|
||||||
return this._storage
|
return this._storage
|
||||||
@ -74,9 +79,9 @@ export abstract class Repo<T extends Entity<any>> {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _findByParams(
|
async _findByParams(
|
||||||
parameters: Record<string, unknown>,
|
parameters: Record<string, any>,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
order?: Record<string, unknown>
|
order?: Record<string, any>
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const cursor = await this._storage.find(this.Name(), parameters)
|
const cursor = await this._storage.find(this.Name(), parameters)
|
||||||
if (limit && limit > 0) await cursor.limit(limit)
|
if (limit && limit > 0) await cursor.limit(limit)
|
||||||
@ -90,7 +95,7 @@ export abstract class Repo<T extends Entity<any>> {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(parameters: Record<string, unknown>, order?: Record<string, unknown>): Promise<T[]> {
|
async findMany(parameters: Record<string, any>, order?: Record<string, any>): Promise<T[]> {
|
||||||
const cursor = await this._storage.find(this.Name(), parameters)
|
const cursor = await this._storage.find(this.Name(), parameters)
|
||||||
if (this._offset > 0) await cursor.skip(this._offset)
|
if (this._offset > 0) await cursor.skip(this._offset)
|
||||||
if (this._limit > 0) await cursor.limit(this._limit)
|
if (this._limit > 0) await cursor.limit(this._limit)
|
||||||
@ -110,7 +115,7 @@ export abstract class Repo<T extends Entity<any>> {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(query?: Record<string, unknown>): Promise<number> {
|
async count(query?: Record<string, any>): Promise<number> {
|
||||||
return this._storage.count(this.Name(), query)
|
return this._storage.count(this.Name(), query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
35
src/app/repo/sqlite.ts
Normal file
35
src/app/repo/sqlite.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Entity } from '../entity'
|
||||||
|
import { ErrEntityNotFound, ErrFoundNotUniqEntity, Repo } from '../repo'
|
||||||
|
import { StorageSQLite } from '../storage/sqlite'
|
||||||
|
|
||||||
|
export abstract class RepoSQLite<T extends Entity<any>> extends Repo<T> {
|
||||||
|
abstract Create(): Promise<void>
|
||||||
|
|
||||||
|
async Drop(): Promise<void> {
|
||||||
|
const st = this._storage as StorageSQLite
|
||||||
|
await st?.db?.run(`DROP TABLE IF EXISTS ${this.Name()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async Recreate(): Promise<void> {
|
||||||
|
await this.Drop()
|
||||||
|
await this.Create()
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<T> {
|
||||||
|
const uniqKey = this.Entity().data.uniqKey()
|
||||||
|
const cond = `${uniqKey} = ?`
|
||||||
|
const cursor = await this._storage.find(this.Name(), { [cond]: [id] })
|
||||||
|
const entryList = await cursor.toArray()
|
||||||
|
if (entryList.length > 1) {
|
||||||
|
throw new ErrFoundNotUniqEntity(
|
||||||
|
`found few (${entryList.length}) entries in ${this.Name()} for id ${id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryList.length === 0) {
|
||||||
|
throw new ErrEntityNotFound(`not found entry in [${this.Name()}] for id [${id}]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._transformer(entryList[0])
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/** @public */
|
/** @public */
|
||||||
export interface StorageCursor {
|
export interface StorageCursor {
|
||||||
limit(number_: number): StorageCursor
|
limit(number: number): StorageCursor
|
||||||
sort(parameters: Record<string, any>): StorageCursor
|
sort(parameters: Record<string, any>): StorageCursor
|
||||||
skip(offset: number): StorageCursor
|
skip(offset: number): StorageCursor
|
||||||
toArray(): Promise<any[]>
|
toArray(): Promise<any[]>
|
||||||
@ -20,19 +20,19 @@ export abstract class Storage {
|
|||||||
this._dsn = dsn
|
this._dsn = dsn
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract find(name: string, query: Record<string, unknown>): Promise<StorageCursor>
|
abstract find(name: string, query: Record<string, any>): Promise<StorageCursor>
|
||||||
|
|
||||||
abstract save(name: string, uniqKey: string, data: Record<string, unknown>): Promise<string>
|
abstract save(name: string, uniqKey: string, data: Record<string, any>): Promise<string>
|
||||||
|
|
||||||
abstract createSession(): StorageSession
|
abstract createSession(): StorageSession
|
||||||
|
|
||||||
abstract count(name: string, query?: Record<string, unknown>): Promise<number>
|
abstract count(name: string, query?: Record<string, any>): Promise<number>
|
||||||
|
|
||||||
abstract remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean>
|
abstract remove(name: string, uniqKey: string, uniqVal: string | number): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export abstract class StorageSession {
|
export abstract class StorageSession {
|
||||||
abstract start(options?: Record<string, unknown>): Promise<void>
|
abstract start(options?: Record<string, any>): Promise<void>
|
||||||
abstract commit(fn: () => any, options?: Record<string, unknown>): Promise<void>
|
abstract commit(fn: () => any, options?: Record<string, any>): Promise<void>
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ClientSession, MongoClient, TransactionOptions } from 'mongodb'
|
import { ClientSession, MongoClient, TransactionOptions } from 'mongodb'
|
||||||
import { Storage, StorageCursor, StorageSession } from '../storage'
|
import { ErrStorage, Storage, StorageCursor, StorageSession } from '../storage'
|
||||||
import { ErrEntityHasNoUniqKeyValue } from '../entity'
|
import { ErrEntityHasNoUniqKeyValue } from '../entity'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
@ -21,23 +21,23 @@ export class MongoStorage extends Storage {
|
|||||||
await this._client.connect()
|
await this._client.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(collectionName: string, query: Record<string, unknown>): Promise<StorageCursor> {
|
async find(collectionName: string, query: Record<string, any>): Promise<StorageCursor> {
|
||||||
await this._connect()
|
await this._connect()
|
||||||
const coll = await this._client.db().collection(collectionName)
|
const coll = await this._client.db().collection(collectionName)
|
||||||
return coll.find(query)
|
return coll.find(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(collectionName: string, query: Record<string, unknown>): Promise<number> {
|
async count(collectionName: string, query: Record<string, any>): Promise<number> {
|
||||||
await this._connect()
|
await this._connect()
|
||||||
const coll = await this._client.db().collection(collectionName)
|
const coll = await this._client.db().collection(collectionName)
|
||||||
return coll.countDocuments(query)
|
return coll.countDocuments(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(collectionName: string, uniqKey: string, data: Record<string, unknown>): Promise<string> {
|
async save(collectionName: string, uniqKey: string, data: Record<string, any>): Promise<string> {
|
||||||
await this._connect()
|
await this._connect()
|
||||||
const id = data[uniqKey]
|
const id = data[uniqKey]
|
||||||
const coll = await this._client.db().collection(collectionName)
|
|
||||||
if (id !== null && id !== undefined) {
|
if (id !== null && id !== undefined) {
|
||||||
|
const coll = await this._client.db().collection(collectionName)
|
||||||
const filter = { [uniqKey]: id }
|
const filter = { [uniqKey]: id }
|
||||||
const result = await coll.findOneAndReplace(filter, data, { upsert: true })
|
const result = await coll.findOneAndReplace(filter, data, { upsert: true })
|
||||||
if (result.lastErrorObject) {
|
if (result.lastErrorObject) {
|
||||||
@ -48,7 +48,7 @@ export class MongoStorage extends Storage {
|
|||||||
return result.lastErrorObject.upserted
|
return result.lastErrorObject.upserted
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TypeError(`can not save data to ${collectionName} with result ${result}`)
|
throw new ErrStorage(`can not save data to ${collectionName} with result ${result}`)
|
||||||
} else {
|
} else {
|
||||||
// Нельзя сохранить сущность без значения уникального ключа
|
// Нельзя сохранить сущность без значения уникального ключа
|
||||||
// const result = await coll.insertOne(data)
|
// const result = await coll.insertOne(data)
|
||||||
@ -70,8 +70,8 @@ export class MongoStorage extends Storage {
|
|||||||
return this._client
|
return this._client
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
|
async remove(name: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
|
||||||
const coll = await this._client.db().collection(collectionName)
|
const coll = await this._client.db().collection(name)
|
||||||
const result = await coll.deleteOne({ [uniqKeyName]: uniqKey })
|
const result = await coll.deleteOne({ [uniqKeyName]: uniqKey })
|
||||||
return Promise.resolve(result.acknowledged)
|
return Promise.resolve(result.acknowledged)
|
||||||
}
|
}
|
||||||
|
207
src/app/storage/sqlite.ts
Normal file
207
src/app/storage/sqlite.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { Database, open } from 'sqlite'
|
||||||
|
import sqlite3 from 'sqlite3'
|
||||||
|
import { ErrStorage, Storage, StorageCursor, StorageSession } from '../storage'
|
||||||
|
import { ErrEntityHasNoUniqKeyValue } from '../entity'
|
||||||
|
|
||||||
|
export type SQLiteCriteria = Record<string, Array<string | number>>
|
||||||
|
|
||||||
|
function parseCriteria(criteria?: SQLiteCriteria): { where: string | null; params: Array<string | number> } {
|
||||||
|
if (criteria) {
|
||||||
|
const conditions = Object.entries(criteria)
|
||||||
|
const params = []
|
||||||
|
const where = []
|
||||||
|
if (conditions.length) {
|
||||||
|
for (const condition of conditions) {
|
||||||
|
where.push(condition[0].trim())
|
||||||
|
params.push(...condition[1])
|
||||||
|
}
|
||||||
|
return { where: where.join(' AND '), params }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { where: null, params: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteStr(val: string | number | unknown) {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
return `'${val.replace(/'/g, `"`)}'`
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformDate(val: Date | unknown) {
|
||||||
|
return val instanceof Date ? `'${val.getTime()}'` : val
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageSQLiteParams {
|
||||||
|
dsn: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StorageSQLite extends Storage {
|
||||||
|
private _db: Database | null = null
|
||||||
|
|
||||||
|
constructor(params: StorageSQLiteParams) {
|
||||||
|
super(params.dsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(): Promise<Database> {
|
||||||
|
try {
|
||||||
|
sqlite3.verbose()
|
||||||
|
this._db = await open({
|
||||||
|
filename: this._dsn,
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
})
|
||||||
|
return this._db
|
||||||
|
} catch (e) {
|
||||||
|
throw new ErrStorage(`error on connect to db [${this._dsn}]`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(name: string, criteria?: SQLiteCriteria): Promise<number> {
|
||||||
|
if (!this._db) await this.connect()
|
||||||
|
const query = [`SELECT COUNT(*) as cnt FROM ${name}`]
|
||||||
|
const { where, params } = parseCriteria(criteria)
|
||||||
|
if (where) {
|
||||||
|
query.push(`WHERE ${where}`)
|
||||||
|
}
|
||||||
|
const result = await this._db?.get(query.join(' '), params)
|
||||||
|
return parseInt(result.cnt, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(name: string, criteria: SQLiteCriteria): Promise<SQLiteCursor> {
|
||||||
|
if (!this._db) this._db = await this.connect()
|
||||||
|
return new SQLiteCursor(this._db, name, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вернет тру, даже если записи не было
|
||||||
|
* @param name
|
||||||
|
* @param uniqKey
|
||||||
|
* @param uniqValue
|
||||||
|
*/
|
||||||
|
async remove(name: string, uniqKey: string, uniqValue: string): Promise<boolean> {
|
||||||
|
if (!this._db) this._db = await this.connect()
|
||||||
|
const sql = `DELETE FROM ${name} WHERE ${uniqKey}=${quoteStr(uniqValue)}`
|
||||||
|
const result = await this._db.run(sql)
|
||||||
|
return !!(result && result.changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(name: string, uniqKey: string, data: Record<string, unknown>): Promise<string> {
|
||||||
|
if (!this._db) this._db = await this.connect()
|
||||||
|
const keys = []
|
||||||
|
const values = []
|
||||||
|
const assigns = []
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
if (v === undefined) continue
|
||||||
|
|
||||||
|
keys.push(k)
|
||||||
|
|
||||||
|
let vp
|
||||||
|
if (v === null) {
|
||||||
|
vp = 'NULL'
|
||||||
|
} else {
|
||||||
|
vp = quoteStr(v)
|
||||||
|
vp = transformDate(vp)
|
||||||
|
}
|
||||||
|
values.push(vp)
|
||||||
|
assigns.push(`${k}=${vp}`)
|
||||||
|
}
|
||||||
|
const id = data[uniqKey]
|
||||||
|
if (id !== null && id !== undefined) {
|
||||||
|
const sql = `INSERT INTO ${name}(${keys.join(', ')})
|
||||||
|
VALUES (${values.join(', ')})
|
||||||
|
ON CONFLICT(${uniqKey}) DO UPDATE SET ${assigns.join(', ')}
|
||||||
|
WHERE ${uniqKey} = ${quoteStr(id)}`
|
||||||
|
try {
|
||||||
|
const result = await this._db.run(sql)
|
||||||
|
return result.lastID + ''
|
||||||
|
} catch (e) {
|
||||||
|
throw new ErrStorage(`can not save data to ${name} by sql:\n"${sql}"\nwith error: ${e}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ErrEntityHasNoUniqKeyValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession(): StorageSession {
|
||||||
|
return new SQLiteSession(this._db)
|
||||||
|
}
|
||||||
|
|
||||||
|
get db(): Database | null {
|
||||||
|
return this._db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class SQLiteCursor implements StorageCursor {
|
||||||
|
private _limit = 0
|
||||||
|
private _offset = 0
|
||||||
|
private _order: Record<string, string> = {}
|
||||||
|
constructor(private _db: Database, private _tableName: string, private _criteria?: SQLiteCriteria) {}
|
||||||
|
|
||||||
|
limit(number: number): this {
|
||||||
|
this._limit = number
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(offset: number): this {
|
||||||
|
this._offset = offset
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(parameters: Record<string, string>): this {
|
||||||
|
this._order = parameters
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSQL(): { query: string[]; params: (string | number)[] } {
|
||||||
|
const query = [`SELECT ROWID as '_id', * FROM ${this._tableName}`]
|
||||||
|
const { where, params } = parseCriteria(this._criteria)
|
||||||
|
if (where) {
|
||||||
|
query.push(`WHERE ${where}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = Object.entries(this._order)
|
||||||
|
if (orders.length) {
|
||||||
|
const orderStr = orders.map(e => `${e[0]} ${e[1]}`).join(', ')
|
||||||
|
query.push(`ORDER BY ${orderStr}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._limit) query.push(`LIMIT ${this._limit}`)
|
||||||
|
if (this._offset) query.push(`OFFSET ${this._offset}`)
|
||||||
|
|
||||||
|
return { query, params }
|
||||||
|
}
|
||||||
|
|
||||||
|
async toArray(): Promise<Record<string, any>[]> {
|
||||||
|
const { query, params } = this._buildSQL()
|
||||||
|
return this._db.all(query.join(' '), params.length ? params : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class SQLiteSession extends StorageSession {
|
||||||
|
constructor(private _db: Database | null) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
await this._db?.run('BEGIN TRANSACTION')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit(fn: () => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fn()
|
||||||
|
} catch (e) {
|
||||||
|
await this._db?.run('ROLLBACK')
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._db?.run('COMMIT')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
@ -6,3 +6,4 @@ export { Data, ValuesObject } from './app/data'
|
|||||||
export { Entity, EntityConstructor, ErrEntityHasNoUniqKeyValue } from './app/entity'
|
export { Entity, EntityConstructor, ErrEntityHasNoUniqKeyValue } from './app/entity'
|
||||||
export { Repo, ErrFoundNotUniqEntity, ErrEntityNotFound } from './app/repo'
|
export { Repo, ErrFoundNotUniqEntity, ErrEntityNotFound } from './app/repo'
|
||||||
export { Storage, StorageCursor, ErrStorage, ErrNoSession, StorageSession } from './app/storage'
|
export { Storage, StorageCursor, ErrStorage, ErrNoSession, StorageSession } from './app/storage'
|
||||||
|
export { EntityManager } from './app/entity/manager'
|
||||||
|
@ -21,12 +21,12 @@ class Item extends Entity<ItemData> {
|
|||||||
return this._data.c
|
return this._data.c
|
||||||
}
|
}
|
||||||
|
|
||||||
_getRepo(storage: Storage): ItemRepository {
|
_getRepo(storage: Storage): ItemRepo {
|
||||||
return new ItemRepository(storage)
|
return new ItemRepo(storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ItemRepository extends Repo<Item> {
|
class ItemRepo extends Repo<Item> {
|
||||||
Name() {
|
Name() {
|
||||||
return 'items'
|
return 'items'
|
||||||
}
|
}
|
||||||
@ -36,10 +36,10 @@ class ItemRepository extends Repo<Item> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ItemRepository', () => {
|
describe('ItemRepo', () => {
|
||||||
describe('map storage save', () => {
|
describe('map storage save', () => {
|
||||||
const storage = new MapStorage('')
|
const storage = new MapStorage('')
|
||||||
const repo = new ItemRepository(storage)
|
const repo = new ItemRepo(storage)
|
||||||
|
|
||||||
it('should return entity', async () => {
|
it('should return entity', async () => {
|
||||||
const dt = Data.assign(new ItemData(), { a: 1, b: 2, c: { d: 3 } })
|
const dt = Data.assign(new ItemData(), { a: 1, b: 2, c: { d: 3 } })
|
42
src/tests/repo/helper.ts
Normal file
42
src/tests/repo/helper.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Data } from '../../app/data'
|
||||||
|
import { Entity } from '../../app/entity'
|
||||||
|
import { RepoSQLite } from '../../app/repo/sqlite'
|
||||||
|
import { StorageSQLite } from '../../app/storage/sqlite'
|
||||||
|
import { Storage } from '../../app/storage'
|
||||||
|
|
||||||
|
export class ItemData extends Data {
|
||||||
|
uniqKey(): string {
|
||||||
|
return 'uid'
|
||||||
|
}
|
||||||
|
|
||||||
|
uid: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
export class ItemEntity extends Entity<ItemData> {
|
||||||
|
_getRepo(storage: Storage): ItemsRepo {
|
||||||
|
return new ItemsRepo(storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getVO(): ItemData {
|
||||||
|
return new ItemData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ItemsRepo extends RepoSQLite<ItemEntity> {
|
||||||
|
Entity(): ItemEntity {
|
||||||
|
return new ItemEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
Name(): string {
|
||||||
|
return 'items'
|
||||||
|
}
|
||||||
|
|
||||||
|
async Create(): Promise<void> {
|
||||||
|
const st = this._storage as StorageSQLite
|
||||||
|
await st?.db?.run(`
|
||||||
|
create table if not exists ${this.Name()} (
|
||||||
|
uid varchar(255) not null unique,
|
||||||
|
title text null
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
33
src/tests/repo/sqlite.test.ts
Normal file
33
src/tests/repo/sqlite.test.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { StorageSQLite } from '../../app/storage/sqlite'
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import { ItemData, ItemEntity, ItemsRepo } from './helper'
|
||||||
|
|
||||||
|
describe('ItemsRepoSQLite', () => {
|
||||||
|
const dsn = __dirname + `/sqlite.test.db`
|
||||||
|
let storage: StorageSQLite
|
||||||
|
let repo: ItemsRepo
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
storage = new StorageSQLite({ dsn })
|
||||||
|
await storage.connect()
|
||||||
|
storage.db.on('trace', (data: string) => {
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
repo = new ItemsRepo(storage)
|
||||||
|
await repo.Recreate()
|
||||||
|
for (const uid of ['foo', 'bar', 'pew', 'baz', 'fox']) {
|
||||||
|
const d = new ItemData()
|
||||||
|
d.uid = uid
|
||||||
|
const e = new ItemEntity(d)
|
||||||
|
await repo.save(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#findById', () => {
|
||||||
|
it('should return persisted entity', async () => {
|
||||||
|
const e = await repo.findById('foo')
|
||||||
|
assert.isTrue(e.isPersisted())
|
||||||
|
assert.isDefined(e._id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -9,8 +9,8 @@ export class MapCursor implements StorageCursor {
|
|||||||
this._map = map
|
this._map = map
|
||||||
}
|
}
|
||||||
|
|
||||||
limit(number_: number): StorageCursor {
|
limit(number: number): StorageCursor {
|
||||||
this._limit = number_
|
this._limit = number
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export class MapStorage extends Storage {
|
|||||||
return Promise.resolve(0)
|
return Promise.resolve(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
|
async remove(name: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
|
||||||
return Promise.resolve(true)
|
return Promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
137
src/tests/storage/sqlite/sqlite.test.ts
Normal file
137
src/tests/storage/sqlite/sqlite.test.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
import { StorageSQLite } from '../../../app/storage/sqlite'
|
||||||
|
|
||||||
|
const abbrList = [
|
||||||
|
['bar', '{}', 2],
|
||||||
|
['pew', '{}', 2],
|
||||||
|
['baz', '{}', 2],
|
||||||
|
['xyz', '{}', 1],
|
||||||
|
['yes', '{}', 1],
|
||||||
|
['lol', JSON.stringify({ hello: 'world' }), 1],
|
||||||
|
['idk', JSON.stringify({ pew: 1 }), 1],
|
||||||
|
['gg', JSON.stringify({ pew: 100 }), 1],
|
||||||
|
]
|
||||||
|
const dsn = __dirname + `/sqlite.test.db`
|
||||||
|
|
||||||
|
describe('StorageSQLite', () => {
|
||||||
|
interface Item {
|
||||||
|
abbr: string
|
||||||
|
foo: string
|
||||||
|
n: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let storage: StorageSQLite
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
storage = new StorageSQLite({ dsn })
|
||||||
|
await storage.connect()
|
||||||
|
storage.db.on('trace', (data: string) => {
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
await storage.db.run('drop table if exists items')
|
||||||
|
// UNIQUE нужно, чтобы работал UPSERT в методе save()
|
||||||
|
await storage.db.run(
|
||||||
|
'create table items (abbr varchar(4) not null unique, foo text null, num integer not null default 0)'
|
||||||
|
)
|
||||||
|
const stmt = await storage.db.prepare('insert into "items" (abbr, foo, num) values (?, ?, ?)')
|
||||||
|
for (const values of abbrList) {
|
||||||
|
await stmt.run(values)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#find', () => {
|
||||||
|
it('should find all', async () => {
|
||||||
|
const cursor = await storage.find('items', {})
|
||||||
|
const items = await cursor.toArray()
|
||||||
|
assert.equal(items.length, abbrList.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find only one', async () => {
|
||||||
|
const cursor = await storage.find('items', { 'abbr = ?': ['bar'] })
|
||||||
|
const items = await cursor.toArray()
|
||||||
|
assert.equal(items.length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find 2 with offset', async () => {
|
||||||
|
const cursor = await storage.find('items', {})
|
||||||
|
const items = await cursor.limit(2).toArray()
|
||||||
|
assert.equal(items.length, 2)
|
||||||
|
const nextItems = await cursor.skip(2).toArray()
|
||||||
|
assert.equal(nextItems.length, 2)
|
||||||
|
assert.notDeepEqual(items[0], nextItems[0])
|
||||||
|
assert.notDeepEqual(items[1], nextItems[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find reversed order ', async () => {
|
||||||
|
const cursor = await storage.find('items', {})
|
||||||
|
const items = await cursor.sort({ abbr: 'DESC' }).toArray()
|
||||||
|
assert.equal(items.length, abbrList.length)
|
||||||
|
assert.equal(items[0].abbr, 'yes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find and get string from string field for json', async () => {
|
||||||
|
const cursor = await storage.find('items', { 'abbr = ?': ['gg'] })
|
||||||
|
const items = await cursor.toArray()
|
||||||
|
assert.lengthOf(items, 1)
|
||||||
|
|
||||||
|
const item = items[0] as Item
|
||||||
|
assert.equal(typeof item.foo, 'string')
|
||||||
|
assert.deepEqual(JSON.parse(item.foo), { pew: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find with criteria with many params 1', async () => {
|
||||||
|
const cursor = await storage.find('items', { 'num = ?': [1], 'foo != ?': ['{}'] })
|
||||||
|
const items = await cursor.toArray()
|
||||||
|
assert.lengthOf(items, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find with criteria with many params 2', async () => {
|
||||||
|
const cursor = await storage.find('items', { 'num = ?': [2], 'foo = ?': ['{}'] })
|
||||||
|
const items = await cursor.toArray()
|
||||||
|
assert.lengthOf(items, 3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#count', () => {
|
||||||
|
it('should return total count', async () => {
|
||||||
|
const count = await storage.count('items', {})
|
||||||
|
assert.equal(count, abbrList.length)
|
||||||
|
})
|
||||||
|
it('should return count with criteria', async () => {
|
||||||
|
const count1 = await storage.count('items', { "abbr LIKE '%'": [] })
|
||||||
|
assert.equal(count1, abbrList.length)
|
||||||
|
const count2 = await storage.count('items', { 'abbr = ? OR abbr = ?': ['bar', 'pew'] })
|
||||||
|
assert.equal(count2, 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#save', () => {
|
||||||
|
it('should save new item', async () => {
|
||||||
|
let lastId = await storage.save('items', 'abbr', { abbr: 'banzay' })
|
||||||
|
assert.equal(lastId, abbrList.length + 1 + '')
|
||||||
|
abbrList.push(['banzay', '{}'])
|
||||||
|
lastId = await storage.save('items', 'abbr', { abbr: 'babam' })
|
||||||
|
assert.equal(lastId, abbrList.length + 1 + '')
|
||||||
|
abbrList.push(['babam', '{}'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update item', async () => {
|
||||||
|
let lastId = await storage.save('items', 'abbr', { abbr: 'kish' })
|
||||||
|
assert.equal(lastId, abbrList.length + 1 + '')
|
||||||
|
abbrList.push(['kish', '{}'])
|
||||||
|
lastId = await storage.save('items', 'abbr', { abbr: 'kish' })
|
||||||
|
assert.equal(lastId, abbrList.length + '')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#remove', () => {
|
||||||
|
it('should remove item', async () => {
|
||||||
|
const lastId = await storage.save('items', 'abbr', { abbr: 'thx' })
|
||||||
|
assert.equal(lastId, abbrList.length + 1 + '')
|
||||||
|
const deleted = await storage.remove('items', 'abbr', 'thx')
|
||||||
|
assert.isTrue(deleted)
|
||||||
|
const count = await storage.count('items')
|
||||||
|
assert.equal(abbrList.length, count)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
46
src/tests/storage/sqlite/sqlsession.test.ts
Normal file
46
src/tests/storage/sqlite/sqlsession.test.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
import { StorageSQLite } from '../../../app/storage/sqlite'
|
||||||
|
|
||||||
|
const itemsData = ['zero', 'one', 'two', 'three', 'four', 'five']
|
||||||
|
const dsn = __dirname + '/sqlsession.test.db'
|
||||||
|
|
||||||
|
describe('SQLiteSession', () => {
|
||||||
|
let storage: StorageSQLite
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
storage = new StorageSQLite({ dsn })
|
||||||
|
await storage.connect()
|
||||||
|
storage.db.on('trace', (data: string) => {
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
await storage.db.run('DROP TABLE IF EXISTS items')
|
||||||
|
|
||||||
|
// UNIQUE нужно, чтобы работал UPSERT в методе save()
|
||||||
|
await storage.db.run(
|
||||||
|
'CREATE TABLE items (key varchar(8) NOT NULL UNIQUE, val integer NOT NULL DEFAULT 0)'
|
||||||
|
)
|
||||||
|
const stmt = await storage.db.prepare('INSERT INTO items (key, val) VALUES (?, ?)')
|
||||||
|
for (let k = 0; k < itemsData.length; k++) {
|
||||||
|
await stmt.run([k, itemsData[k]])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#commit', () => {
|
||||||
|
it('should commit save', async () => {
|
||||||
|
const session = storage.createSession()
|
||||||
|
await session.start()
|
||||||
|
await session.commit(async () => {
|
||||||
|
await storage.save('items', 'key', { key: 'ten', val: 10 })
|
||||||
|
await storage.save('items', 'key', { key: 'eleven', val: 11 })
|
||||||
|
await storage.remove('items', 'key', 'ten')
|
||||||
|
})
|
||||||
|
|
||||||
|
const cursor = await storage.find('items', {})
|
||||||
|
const items = await cursor.toArray()
|
||||||
|
assert.equal(itemsData.length + 1, items.length)
|
||||||
|
for (const item of items) {
|
||||||
|
if (item['key'] === 'ten') assert.fail('ten had to be removed in session')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user