Compare commits

..

No commits in common. "master" and "v1.0.1" have entirely different histories.

27 changed files with 186 additions and 3225 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules node_modules
dist dist
*.db

View File

@ -1,17 +0,0 @@
<?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>

View File

@ -1,10 +0,0 @@
<?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>

5
CHANGELOG.md Normal file
View File

@ -0,0 +1,5 @@
# Changelog
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.0.1 (2021-09-29)

View File

@ -1,91 +0,0 @@
# Changelog
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.0](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.0.2...v1.1.0) (2021-09-29)
### Features
* Добавил разметку для экстрактора апи и команду для сборника типов ([7f25b66](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/7f25b665f4f27665595065f006c41669b0f77dd3))
### [1.0.2](https://git.archive.systems/Dezzpil/ivanovna.orm/compare/v1.0.1...v1.0.2) (2021-09-29)
### Bug Fixes
* Добавил главный файл с экспортами, поправил пэкадж и названия файла хранилища монги ([f998ee6](https://git.archive.systems/Dezzpil/ivanovna.orm/commit/f998ee630bdb5277f06743b0fb332dab9c6ec638))

View File

@ -1,37 +0,0 @@
# Ivanovna ORM
Простая ОРМ для простейших задач.
Позволяет задать модели для таблиц или коллекций и предоставляет менеджер сущностей для манипуляций с ними.
## Тестирование
```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

View File

@ -1,44 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts",
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "",
"publicTrimmedFilePath": "<projectFolder>/orm.d.ts"
},
"tsdocMetadata": {
"enabled": false
},
"newlineKind": "lf",
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "error"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "error"
},
"ae-internal-missing-underscore": {
"logLevel": "none",
"addToApiReportFile": false
},
"ae-forgotten-export": {
"logLevel": "error",
"addToApiReportFile": false
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "none"
}
}
}
}

View File

@ -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|js)$': 'ts-jest', '^.+\\.(ts|tsx)$': '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'],

126
orm.d.ts vendored
View File

@ -1,126 +0,0 @@
/** @public */
export declare abstract class Data {
[key: string]: any;
/**
* @deprecated use fromObject()
*/
static assign(vo: Data, values: ValuesObject): Data;
set(key: string, value: any): void;
fromObject(object: ValuesObject): void;
toObject(exclude?: string[]): ValuesObject;
abstract uniqKey(): string;
}
/** @public */
export declare abstract class Entity<T extends Data> {
abstract _getRepo(storage: Storage_2): Repo<Entity<T>>;
abstract _getVO(): T;
protected _data: T;
private __id;
get data(): T;
get _id(): string;
set _id(value: string);
constructor(data?: T);
getUniqKey(): any;
isPersisted(): boolean;
toString(): any;
}
/** @public */
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 */
export declare class ErrEntityHasNoUniqKeyValue extends Error {
}
/** @public */
export declare class ErrEntityNotFound extends Error {
}
/** @public */
export declare class ErrFoundNotUniqEntity extends Error {
}
/** @public */
export declare class ErrNoSession extends ErrStorage {
}
/** @public */
export declare class ErrStorage extends Error {
}
/** @public */
export declare abstract class Repo<T extends Entity<any>> {
protected _storage: Storage_2;
protected _entity: T;
protected _limit: number;
protected _offset: number;
protected _transformer: (obj: any) => any;
/**
* Возвращает объект соотв. сущности, например new App()
*/
abstract Entity(): T;
/**
* Возвращает название коллекции/таблицы/... хранящей записи соотв. сущностей
*/
abstract Name(): string;
constructor(storage: Storage_2, transformer?: (obj: any) => any);
setTransformer(transformer?: (obj: any) => any): this;
get storage(): Storage_2;
save(entity: T): Promise<this>;
findById(id: string): Promise<T | null>;
/**
*
* @protected
*/
_findByParams(parameters: Record<string, any>, limit?: number, order?: Record<string, any>): Promise<T[]>;
findMany(parameters: Record<string, any>, order?: Record<string, any>): Promise<T[]>;
shift(limit?: number, offset?: number): this;
count(query?: Record<string, any>): Promise<number>;
remove(entity: T, silent?: boolean): Promise<this>;
}
/** @public */
declare abstract class Storage_2 {
protected _dsn: string;
constructor(dsn: string);
abstract find(name: string, query: Record<string, any>): Promise<StorageCursor>;
abstract save(name: string, uniqKey: string, data: Record<string, any>): Promise<string>;
abstract createSession(): StorageSession;
abstract count(name: string, query?: Record<string, any>): Promise<number>;
abstract remove(name: string, uniqKey: string, uniqVal: string | number): Promise<boolean>;
}
export { Storage_2 as Storage }
/** @public */
export declare interface StorageCursor {
limit(number: number): StorageCursor;
sort(parameters: Record<string, any>): StorageCursor;
skip(offset: number): StorageCursor;
toArray(): Promise<any[]>;
}
/** @public */
export declare abstract class StorageSession {
abstract start(options?: Record<string, any>): Promise<void>;
abstract commit(fn: () => any, options?: Record<string, any>): Promise<void>;
}
/** @public */
export declare type ValuesObject = Record<string, any>;
export { }

2371
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,15 @@
{ {
"name": "ivna-orm", "name": "ivna-orm",
"version": "1.5.0", "version": "1.0.1",
"description": "Mini ORM for convenience", "description": "Mini ORM for convenience",
"main": "dist/index.js", "main": "src/index.ts",
"files": [ "files": [
"dist/index.js", "dist/app/**/*"
"dist/index.d.ts",
"dist/app/**/*",
"src/index.ts",
"src/app/**/*",
"orm.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",
"prepare": "npm test && npm run build",
"build": "rimraf dist && tsc -b -v", "build": "rimraf dist && tsc -b -v",
"build:api": "npm run build && api-extractor run && rimraf 'dist/**/*.d.ts*'",
"release": "standard-version -i HISTORY.md" "release": "standard-version -i HISTORY.md"
}, },
"repository": { "repository": {
@ -31,7 +25,6 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@jest/test-sequencer": "^27.2.3", "@jest/test-sequencer": "^27.2.3",
"@microsoft/api-extractor": "^7.18.11",
"@types/chai": "^4.2.22", "@types/chai": "^4.2.22",
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/node": "^14.17.19", "@types/node": "^14.17.19",
@ -42,16 +35,15 @@
"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.5.5" "typescript": "^4.4.3"
}, },
"optionalDependencies": { "dependencies": {
"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,8 +52,5 @@
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"tabWidth": 4 "tabWidth": 4
},
"dependencies": {
"rfdc": "^1.3.0"
} }
} }

View File

@ -1,15 +1,10 @@
import rfdc from 'rfdc' import rfdc from 'rfdc'
/** @public */ type ValuesObject = Record<string, any>
export type ValuesObject = Record<string, any>
/** @public */
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]
@ -29,7 +24,7 @@ export abstract class Data {
} }
} }
toObject(exclude: string[] = []): ValuesObject { toObject(exclude: string[] = []): Record<string, any> {
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('__')) {

View File

@ -2,13 +2,10 @@ import { Data } from './data'
import { Repo } from './repo' import { Repo } from './repo'
import { Storage } from './storage' import { Storage } from './storage'
/** @public */
export type EntityConstructor<T extends Data> = new (data?: T) => Entity<T> export type EntityConstructor<T extends Data> = new (data?: T) => Entity<T>
/** @public */
export class ErrEntityHasNoUniqKeyValue extends Error {} export class ErrEntityHasNoUniqKeyValue extends Error {}
/** @public */
export abstract class Entity<T extends Data> { export abstract class Entity<T extends Data> {
abstract _getRepo(storage: Storage): Repo<Entity<T>> abstract _getRepo(storage: Storage): Repo<Entity<T>>
abstract _getVO(): T abstract _getVO(): T
@ -48,7 +45,7 @@ export abstract class Entity<T extends Data> {
} }
isPersisted(): boolean { isPersisted(): boolean {
return this.__id !== undefined && this.__id !== '' return this.__id !== ''
} }
toString() { toString() {

View File

@ -1,64 +0,0 @@
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())
}
}

View File

@ -1,13 +1,9 @@
import { Entity } from './entity' import { Entity } from './entity'
import { Storage } from './storage' import { Storage } from './storage'
/** @public */
export class ErrFoundNotUniqEntity extends Error {} export class ErrFoundNotUniqEntity extends Error {}
/** @public */
export class ErrEntityNotFound extends Error {} export class ErrEntityNotFound extends Error {}
/** @public */
export abstract class Repo<T extends Entity<any>> { export abstract class Repo<T extends Entity<any>> {
protected _storage: Storage protected _storage: Storage
protected _entity: T protected _entity: T
@ -15,8 +11,6 @@ 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()
*/ */
@ -27,24 +21,21 @@ export abstract class Repo<T extends Entity<any>> {
*/ */
abstract Name(): string abstract Name(): string
constructor(storage: Storage, transformer?: (obj: any) => any) { _transformer: (object: any) => any
constructor(storage: Storage) {
this._storage = storage this._storage = storage
this._entity = this.Entity() this._entity = this.Entity()
this.setTransformer(transformer) this.resetTransformer()
} }
setTransformer(transformer?: (obj: any) => any): this { resetTransformer() {
if (transformer) { this._transformer = function (object: any): any {
this._transformer = transformer const entity = this.Entity()
} else { entity.data.fromObject(object)
this._transformer = function (object: any): any { entity._id = object._id
const entity = this.Entity() return entity
entity.data.fromObject(object)
entity._id = object._id
return entity
}
} }
return this
} }
get storage(): Storage { get storage(): Storage {
@ -79,9 +70,9 @@ export abstract class Repo<T extends Entity<any>> {
* @protected * @protected
*/ */
async _findByParams( async _findByParams(
parameters: Record<string, any>, parameters: Record<string, unknown>,
limit?: number, limit?: number,
order?: Record<string, any> order?: Record<string, unknown>
): 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)
@ -95,7 +86,7 @@ export abstract class Repo<T extends Entity<any>> {
return list return list
} }
async findMany(parameters: Record<string, any>, order?: Record<string, any>): Promise<T[]> { async findMany(parameters: Record<string, unknown>, order?: Record<string, unknown>): 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)
@ -115,7 +106,7 @@ export abstract class Repo<T extends Entity<any>> {
return this return this
} }
async count(query?: Record<string, any>): Promise<number> { async count(query?: Record<string, unknown>): Promise<number> {
return this._storage.count(this.Name(), query) return this._storage.count(this.Name(), query)
} }

View File

@ -1,35 +0,0 @@
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])
}
}

View File

@ -1,18 +1,13 @@
/** @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[]>
} }
/** @public */
export class ErrStorage extends Error {} export class ErrStorage extends Error {}
/** @public */
export class ErrNoSession extends ErrStorage {} export class ErrNoSession extends ErrStorage {}
/** @public */
export abstract class Storage { export abstract class Storage {
protected _dsn: string protected _dsn: string
@ -20,19 +15,18 @@ export abstract class Storage {
this._dsn = dsn this._dsn = dsn
} }
abstract find(name: string, query: Record<string, any>): Promise<StorageCursor> abstract find(name: string, query: Record<string, unknown>): Promise<StorageCursor>
abstract save(name: string, uniqKey: string, data: Record<string, any>): Promise<string> abstract save(name: string, uniqKey: string, data: Record<string, unknown>): Promise<string>
abstract createSession(): StorageSession abstract createSession(): StorageSession
abstract count(name: string, query?: Record<string, any>): Promise<number> abstract count(name: string, query?: Record<string, unknown>): Promise<number>
abstract remove(name: string, uniqKey: string, uniqVal: string | number): Promise<boolean> abstract remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean>
} }
/** @public */
export abstract class StorageSession { export abstract class StorageSession {
abstract start(options?: Record<string, any>): Promise<void> abstract start(options?: Record<string, unknown>): Promise<void>
abstract commit(fn: () => any, options?: Record<string, any>): Promise<void> abstract commit(fn: () => any, options?: Record<string, unknown>): Promise<void>
} }

View File

@ -1,207 +0,0 @@
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
}
}

View File

@ -1,8 +1,7 @@
import { ClientSession, MongoClient, TransactionOptions } from 'mongodb' import { ClientSession, MongoClient, TransactionOptions } from 'mongodb'
import { ErrStorage, Storage, StorageCursor, StorageSession } from '../storage' import { Storage, StorageCursor, StorageSession } from '../storage'
import { ErrEntityHasNoUniqKeyValue } from '../entity' import { ErrEntityHasNoUniqKeyValue } from '../entity'
/** @public */
export class MongoStorage extends Storage { export class MongoStorage extends Storage {
private readonly _client: MongoClient private readonly _client: MongoClient
_session: ClientSession _session: ClientSession
@ -21,23 +20,23 @@ export class MongoStorage extends Storage {
await this._client.connect() await this._client.connect()
} }
async find(collectionName: string, query: Record<string, any>): Promise<StorageCursor> { async find(collectionName: string, query: Record<string, unknown>): 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, any>): Promise<number> { async count(collectionName: string, query: Record<string, unknown>): 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, any>): Promise<string> { async save(collectionName: string, uniqKey: string, data: Record<string, unknown>): 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 +47,7 @@ export class MongoStorage extends Storage {
return result.lastErrorObject.upserted return result.lastErrorObject.upserted
} }
throw new ErrStorage(`can not save data to ${collectionName} with result ${result}`) throw new TypeError(`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 +69,8 @@ export class MongoStorage extends Storage {
return this._client return this._client
} }
async remove(name: string, uniqKeyName: string, uniqKey: string): Promise<boolean> { async remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
const coll = await this._client.db().collection(name) const coll = await this._client.db().collection(collectionName)
const result = await coll.deleteOne({ [uniqKeyName]: uniqKey }) const result = await coll.deleteOne({ [uniqKeyName]: uniqKey })
return Promise.resolve(result.acknowledged) return Promise.resolve(result.acknowledged)
} }

View File

@ -1,9 +0,0 @@
import { EntityConstructor, ErrEntityHasNoUniqKeyValue } from './app/entity'
import { ErrEntityNotFound, ErrFoundNotUniqEntity } from './app/repo'
import { ErrNoSession, ErrStorage, StorageCursor, StorageSession } from './app/storage'
export { Data, ValuesObject } from './app/data'
export { Entity, EntityConstructor, ErrEntityHasNoUniqKeyValue } from './app/entity'
export { Repo, ErrFoundNotUniqEntity, ErrEntityNotFound } from './app/repo'
export { Storage, StorageCursor, ErrStorage, ErrNoSession, StorageSession } from './app/storage'
export { EntityManager } from './app/entity/manager'

View File

@ -1,42 +0,0 @@
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
)
`)
}
}

View File

@ -1,33 +0,0 @@
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)
})
})
})

View File

@ -21,12 +21,12 @@ class Item extends Entity<ItemData> {
return this._data.c return this._data.c
} }
_getRepo(storage: Storage): ItemRepo { _getRepo(storage: Storage): ItemRepository {
return new ItemRepo(storage) return new ItemRepository(storage)
} }
} }
class ItemRepo extends Repo<Item> { class ItemRepository extends Repo<Item> {
Name() { Name() {
return 'items' return 'items'
} }
@ -36,10 +36,10 @@ class ItemRepo extends Repo<Item> {
} }
} }
describe('ItemRepo', () => { describe('ItemRepository', () => {
describe('map storage save', () => { describe('map storage save', () => {
const storage = new MapStorage('') const storage = new MapStorage('')
const repo = new ItemRepo(storage) const repo = new ItemRepository(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 } })

View File

@ -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(name: string, uniqKeyName: string, uniqKey: string): Promise<boolean> { async remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
return Promise.resolve(true) return Promise.resolve(true)
} }

View File

@ -1,137 +0,0 @@
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)
})
})
})

View File

@ -1,46 +0,0 @@
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')
}
})
})
})

View File

@ -4,9 +4,10 @@
"target": "ES2020", "target": "ES2020",
"sourceMap": false, "sourceMap": false,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": false,
"allowJs": false, "allowJs": false,
"checkJs": false, "checkJs": false,
"removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,