├── .eslintrc.js ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .npmrc ├── .prettierrc ├── README.md ├── cspell.json ├── docker-compose.yml ├── jest-e2e.json ├── libs ├── constants │ └── constant.ts ├── decorators │ ├── column.decorator.ts │ ├── entity-repository.decorator.ts │ ├── entity.decorator.ts │ ├── listeners │ │ └── before-save.decorator.ts │ └── scylla.decorator.ts ├── errors │ └── entity-not-found.error.ts ├── index.ts ├── interfaces │ ├── column-options.interface.ts │ ├── data.type.ts │ ├── entity-options.interface.ts │ ├── entity-subscriber.interface.ts │ ├── externals │ │ ├── scylla-client-options.interface.ts │ │ ├── scylla-connection.interface.ts │ │ └── scylla.interface.ts │ └── scylla-module-options.interface.ts ├── repositories │ ├── builder │ │ └── return-query.builder.ts │ ├── repository.factory.ts │ └── repository.ts ├── scylla-core.module.ts ├── scylla.constant.ts ├── scylla.module.ts ├── scylla.providers.ts └── utils │ ├── db.utils.ts │ ├── decorator.utils.ts │ ├── deep-merge.utils.ts │ ├── model.utils.ts │ ├── scylla.utils.ts │ └── transform-entity.utils.ts ├── ormconfig.json ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── app.module.ts ├── cats │ ├── cat.controller.ts │ ├── cat.module.ts │ ├── cat.repository.ts │ ├── cat.service.ts │ ├── dto │ │ └── create-cat.dto.ts │ ├── entities │ │ └── cat.entity.ts │ ├── index.ts │ └── pipes │ │ └── parse-uuid.pipe.ts ├── config.ts └── main.ts ├── tsconfig.json └── tsconfig.prod.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module' 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:prettier/recommended', 10 | 'plugin:@typescript-eslint/recommended' 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true 16 | }, 17 | overrides: [ 18 | { 19 | 'files': ['*.e2e-spec.ts', 'country.helper.ts', '*.spec.ts'], 20 | 'rules': { 21 | 'max-lines-per-function': 'off', 22 | 'max-lines': 'off', 23 | } 24 | } 25 | ], 26 | ignorePatterns: ['.eslintrc.js'], 27 | rules: { 28 | 'max-lines': ['error', { 29 | max: 400, 30 | skipComments: true 31 | }], 32 | 'max-lines-per-function': ['error', { 33 | max: 50, 34 | skipComments: true 35 | }], 36 | '@typescript-eslint/naming-convention': [ 37 | 'error', 38 | { 'selector': 'enumMember', 'format': ['UPPER_CASE'] }, 39 | { 40 | 'selector': [ 41 | 'objectLiteralProperty' 42 | ], 43 | 'format': ['camelCase', 'PascalCase', 'UPPER_CASE'] 44 | }, 45 | { 46 | 'selector': [ 47 | 'parameter', 48 | 'variable', 49 | 'function', 50 | 'classProperty', 51 | 'typeProperty', 52 | 'parameterProperty', 53 | 'classMethod', 54 | 'objectLiteralMethod', 55 | 'typeMethod' 56 | ], 57 | 'format': ['camelCase'] 58 | }, 59 | { 60 | 'selector': [ 61 | 'class', 62 | 'interface', 63 | 'enum' 64 | ], 65 | 'format': ['PascalCase'] 66 | }, 67 | { 68 | 'selector': [ 69 | 'variable' 70 | ], 71 | 'modifiers': ['exported'], 72 | 'format': ['PascalCase', 'camelCase', 'UPPER_CASE'] 73 | }, 74 | { 75 | 'selector': [ 76 | 'function' 77 | ], 78 | 'modifiers': ['exported'], 79 | 'format': ['PascalCase', 'camelCase'] 80 | } 81 | ], 82 | '@typescript-eslint/interface-name-prefix': 'off', 83 | '@typescript-eslint/no-empty-function': 'off', 84 | '@typescript-eslint/explicit-function-return-type': 'off', 85 | '@typescript-eslint/explicit-module-boundary-types': 'off', 86 | '@typescript-eslint/no-explicit-any': 'off', 87 | '@typescript-eslint/no-inferrable-types': 'off', 88 | '@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }], 89 | 'indent': 'off', 90 | 'prettier/prettier': [ 91 | 'error', 92 | { 93 | 'useTabs': false, 94 | 'tabWidth': 4, 95 | 'printWidth': 120, 96 | 'singleQuote': true, 97 | 'trailingComma': 'none' 98 | } 99 | ], 100 | 'prefer-const': 'off', 101 | '@typescript-eslint/ban-types': ['error', { 102 | types: { 103 | Object: false, 104 | Function: false 105 | } 106 | }] 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | push: 4 | branch: 5 | - master 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '16.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm install 16 | - run: npm run build 17 | - run: cd dist && npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | src/**/*.js 4 | src/**/*.js.map -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "libs/**/*.ts": ["cspell --no-must-find-files .", "eslint --fix --max-warnings 0"] 3 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | registry=https://registry.npmjs.org/ 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "proseWrap": "always", 5 | "tabWidth": 4, 6 | "useTabs": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "semi": true, 11 | "endOfLine": "auto" 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hodfords-solutions/nestjs-scylladb/c4cdaafcb7266ea4047a0b30503f7c39e7675942/README.md -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": ["nestjs", "scylladb", "Metadatas", "bytea", "timeuuid", "keyspace", "entitynotfound", "deepmerge", "Khang", 5 | "Gremin", "varint", "inet", "Geoshape", "udts", "udfs", "udas", "webnet", "tinyint", "TINYINT", "followee", "isnt", "hodfords", "snakecase"], 6 | "ignorePaths": ["node_modules", "test", "*.spec.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | scylla: 4 | image: scylladb/scylla:5.2.0 5 | restart: always 6 | ports: 7 | - 9043:9042 8 | container_name: test-scylla 9 | networks: 10 | webnet: 11 | volumes: 12 | data-volume: 13 | -------------------------------------------------------------------------------- /jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/constants/constant.ts: -------------------------------------------------------------------------------- 1 | export const ENTITY_METADATA = '__entity__'; 2 | export const COLUMN_METADATA = '__column__'; 3 | 4 | export const ENTITY_NAME_KEY = 'scylladb:entityName'; 5 | export const ATTRIBUTE_KEY = 'scylladb:attributes'; 6 | export const OPTIONS_KEY = 'scylladb:options'; 7 | 8 | export const BEFORE_SAVE = ENTITY_METADATA + 'before_save'; 9 | -------------------------------------------------------------------------------- /libs/decorators/column.decorator.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash'; 2 | import { ColumnOptions } from '../interfaces/column-options.interface'; 3 | import { addAttribute, addOptions, getOptions } from '../utils/decorator.utils'; 4 | import { BeforeSave } from './listeners/before-save.decorator'; 5 | import { timeuuid, uuid } from '../utils/db.utils'; 6 | 7 | export function Column(options: ColumnOptions): PropertyDecorator { 8 | let mappedOptions = cloneDeep(options); 9 | 10 | if (typeof options.default === 'object' && options.default?.$dbFunction) { 11 | mappedOptions.default = { [`$db_function`]: cloneDeep(options.default.$dbFunction) }; 12 | } 13 | 14 | return (target: object, propertyName: string) => { 15 | addAttribute(target, propertyName, mappedOptions); 16 | }; 17 | } 18 | 19 | export function GeneratedUUidColumn(type: 'uuid' | 'timeuuid' = 'uuid'): PropertyDecorator { 20 | return (target: object, propertyName: string) => { 21 | const fn: PropertyDescriptor = { 22 | value: (...args: any[]) => { 23 | const instance = args[0]; 24 | if (instance !== null && !instance[propertyName]) { 25 | instance[propertyName] = type === 'timeuuid' ? timeuuid() : uuid(); 26 | } 27 | } 28 | }; 29 | 30 | Column({ 31 | type, 32 | default: { $dbFunction: type === 'timeuuid' ? 'now()' : 'uuid()' } 33 | })(target, propertyName); 34 | BeforeSave()(target, propertyName, fn); 35 | }; 36 | } 37 | 38 | export function CreateDateColumn(): PropertyDecorator { 39 | return (target: object, propertyName: string) => { 40 | addOptions(target, { 41 | options: { timestamps: { createdAt: propertyName } } 42 | }); 43 | }; 44 | } 45 | 46 | export function UpdateDateColumn(): PropertyDecorator { 47 | return (target: object, propertyName: string) => { 48 | addOptions(target, { 49 | options: { timestamps: { updatedAt: propertyName } } 50 | }); 51 | }; 52 | } 53 | 54 | export function IndexColumn(): PropertyDecorator { 55 | return (target: object, propertyName: string) => { 56 | let { indexes } = getOptions(target); 57 | indexes = indexes || []; 58 | 59 | const isAdded = (indexes as string[]).some((value) => value === propertyName); 60 | 61 | if (isAdded) { 62 | return; 63 | } 64 | 65 | indexes.push(propertyName); 66 | addOptions(target, { indexes }); 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /libs/decorators/entity-repository.decorator.ts: -------------------------------------------------------------------------------- 1 | import { setEntity } from '../utils/decorator.utils'; 2 | 3 | export function EntityRepository(entity: Function): ClassDecorator { 4 | return (target: Function) => { 5 | setEntity(target, entity); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /libs/decorators/entity.decorator.ts: -------------------------------------------------------------------------------- 1 | import snakecaseKeys from 'snakecase-keys'; 2 | import { EntityOptions } from '../interfaces/entity-options.interface'; 3 | import { addOptions, setEntityName } from '../utils/decorator.utils'; 4 | 5 | export function Entity(nameOrOptions?: string | EntityOptions, maybeOptions?: EntityOptions): ClassDecorator { 6 | const options: any = (typeof nameOrOptions === 'object' ? (nameOrOptions as EntityOptions) : maybeOptions) || {}; 7 | const name = typeof nameOrOptions === 'string' ? nameOrOptions : options.tableName; 8 | 9 | return (target): void => { 10 | options.instanceMethods = target.prototype; 11 | options.classMethods = target; 12 | 13 | setEntityName(target.prototype, name); 14 | addOptions(target.prototype, snakecaseKeys(options, { deep: false })); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /libs/decorators/listeners/before-save.decorator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { BEFORE_SAVE } from '../../constants/constant'; 3 | import { addOptions, addHookFunction, getOptions } from '../../utils/decorator.utils'; 4 | 5 | export function BeforeSave(): MethodDecorator { 6 | return (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { 7 | const hookFuncLikeArray = Reflect.getMetadata(BEFORE_SAVE, target) || []; 8 | hookFuncLikeArray.push(descriptor.value); 9 | Reflect.defineMetadata(BEFORE_SAVE, hookFuncLikeArray, target); 10 | 11 | const { before_save } = getOptions(target); 12 | 13 | if (!before_save) { 14 | addOptions(target, { before_save: addHookFunction(target, BEFORE_SAVE) }); 15 | } 16 | return descriptor; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /libs/decorators/scylla.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { getModelToken, getRepositoryToken, getConnectionToken } from '../utils/scylla.utils'; 3 | import { Connection } from '../interfaces/externals/scylla-connection.interface'; 4 | import { ConnectionOptions } from '../interfaces/externals/scylla-client-options.interface'; 5 | 6 | export const InjectConnection: (connection?: Connection | ConnectionOptions | string) => ParameterDecorator = ( 7 | connection?: Connection | ConnectionOptions | string 8 | ) => Inject(getConnectionToken(connection)); 9 | 10 | export const InjectModel = (entity: Function) => Inject(getModelToken(entity)); 11 | 12 | export const InjectRepository = (entity: Function) => Inject(getRepositoryToken(entity)); 13 | -------------------------------------------------------------------------------- /libs/errors/entity-not-found.error.ts: -------------------------------------------------------------------------------- 1 | export class EntityNotFoundError extends Error { 2 | name = 'apollo.model.find.entitynotfound'; 3 | public readonly message: any; 4 | 5 | constructor(entityClass: Function | string, query: any) { 6 | super(); 7 | Object.setPrototypeOf(this, EntityNotFoundError.prototype); 8 | let targetName: string; 9 | if (typeof entityClass === 'function') { 10 | targetName = entityClass.name; 11 | } else { 12 | targetName = entityClass; 13 | } 14 | const queryString = this.stringifyQuery(query); 15 | this.message = `Could not find any entity of type "${targetName}" matching: ${queryString}`; 16 | } 17 | 18 | private stringifyQuery(query: any): string { 19 | try { 20 | return JSON.stringify(query, null, 4); 21 | } catch (e) {} 22 | return '' + query; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'cassandra-driver'; 2 | export * from './decorators/column.decorator'; 3 | export * from './decorators/entity-repository.decorator'; 4 | export * from './decorators/entity.decorator'; 5 | export * from './decorators/scylla.decorator'; 6 | export * from './errors/entity-not-found.error'; 7 | export * from './scylla-core.module'; 8 | export * from './scylla.constant'; 9 | export * from './scylla.module'; 10 | export * from './scylla.providers'; 11 | export * from './utils/db.utils'; 12 | export * from './utils/decorator.utils'; 13 | export * from './utils/deep-merge.utils'; 14 | export * from './utils/model.utils'; 15 | export * from './utils/scylla.utils'; 16 | export * from './utils/transform-entity.utils'; 17 | export * from './interfaces/scylla-module-options.interface'; 18 | export * from './interfaces/column-options.interface'; 19 | export * from './interfaces/data.type'; 20 | export * from './interfaces/entity-options.interface'; 21 | export * from './repositories/repository'; 22 | export * from './repositories/builder/return-query.builder'; 23 | -------------------------------------------------------------------------------- /libs/interfaces/column-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ColumnType, DataType } from './data.type'; 2 | 3 | export interface ColumnOptions { 4 | type?: ColumnType | DataType; 5 | 6 | typeDef?: string; 7 | 8 | default?: string | (() => any) | Function | { $dbFunction: string }; 9 | 10 | virtual?: { get?: any; set?: any }; 11 | 12 | rule?: ColumnRuleOptions | ((value: any) => boolean); 13 | 14 | static?: boolean; 15 | } 16 | 17 | export interface ColumnRuleOptions { 18 | validator?: (value: any) => boolean; 19 | 20 | validators?: any[]; 21 | 22 | message?: string | ((value: any) => string); 23 | 24 | ignoreDefault?: boolean; 25 | 26 | required?: boolean; 27 | } 28 | -------------------------------------------------------------------------------- /libs/interfaces/data.type.ts: -------------------------------------------------------------------------------- 1 | export type CassandraType = 2 | | 'int' 3 | | 'boolean' 4 | | 'text' 5 | | 'varchar' 6 | | 'uuid' 7 | | 'timeuuid' 8 | | 'timestamp' 9 | | 'date' 10 | | 'map' 11 | | 'set' 12 | | 'list' 13 | | 'double' 14 | | 'float' 15 | | 'decimal' 16 | | 'smallint' 17 | | 'bigint' 18 | | 'tinyint' 19 | | 'varint' 20 | | 'ascii' 21 | | 'counter' 22 | | 'inet' 23 | | 'time' 24 | | 'tuple' 25 | | 'frozen' 26 | | 'blob'; 27 | 28 | export type WithWidthColumnType = 'int' | 'smallint' | 'bigint' | 'tinyint' | 'varint'; 29 | 30 | export type ModelColumnType = 31 | | 'bigint' 32 | | 'blob' 33 | | 'counter' 34 | | 'date' 35 | | 'decimal' 36 | | 'inet' 37 | | 'time' 38 | | 'timeuuid' 39 | | 'tuple' 40 | | 'uuid' 41 | | 'varint'; 42 | 43 | export type ColumnType = CassandraType | WithWidthColumnType | ModelColumnType; 44 | 45 | export enum DataType { 46 | MAP = 'map', 47 | LIST = 'list', 48 | SET = 'set', 49 | FROZEN = 'frozen', 50 | NUMBER = 'int', 51 | TEXT = 'text', 52 | BOOLEAN = 'boolean', 53 | VARCHAR = 'varchar', 54 | UUID = 'uuid', 55 | TIMEUUID = 'timeuuid', 56 | TIMESTAMP = 'timestamp', 57 | DATE = 'date', 58 | DOUBLE = 'double', 59 | FLOAT = 'float', 60 | DECIMAL = 'decimal', 61 | SMALLINT = 'smallint', 62 | BIGINT = 'bigint', 63 | TINYINT = 'tinyint', 64 | VARINT = 'varint', 65 | COUNTER = 'counter', 66 | INET = 'inet', 67 | TIME = 'time', 68 | TUPLE = 'tuple', 69 | BLOB = 'blob' 70 | } 71 | -------------------------------------------------------------------------------- /libs/interfaces/entity-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindSubQueryStatic } from './externals/scylla.interface'; 2 | 3 | export interface EntityOptions { 4 | tableName?: string; 5 | 6 | key?: Array>; 7 | 8 | materializedViews?: { [index: string]: MaterializeViewStatic }; 9 | 10 | clusteringOrder?: { [index: string]: 'desc' | 'asc' }; 11 | 12 | options?: EntityExtraOptions; 13 | 14 | indexes?: Array | string[]; 15 | 16 | customIndexes?: Partial[]; 17 | 18 | methods?: { [index: string]: Function }; 19 | 20 | esIndexMapping?: { 21 | discover?: string; 22 | 23 | properties?: EsIndexPropertiesOptionsStatic; 24 | }; 25 | 26 | graphMapping?: Partial>; 27 | 28 | [index: string]: any; 29 | } 30 | 31 | export type ClusterOrder = { [P in keyof T]?: 'desc' | 'asc' }; 32 | 33 | export interface MaterializeViewStatic { 34 | select?: Array; 35 | 36 | key: Array>; 37 | 38 | clusteringOrder?: ClusterOrder; 39 | 40 | filter?: FilterOptions; 41 | } 42 | 43 | export interface EntityExtraOptions { 44 | timestamps?: { 45 | createdAt?: string; 46 | 47 | updatedAt?: string; 48 | }; 49 | 50 | versions?: { key: string }; 51 | } 52 | 53 | type FilterOptions = Partial<{ [P in keyof T]: FindSubQueryStatic }>; 54 | 55 | interface CustomIndexOptions { 56 | on: string; 57 | 58 | using: any; 59 | 60 | options: any; 61 | } 62 | 63 | type EsIndexPropertiesOptionsStatic = { 64 | [P in keyof T]?: { type?: string; index?: string }; 65 | }; 66 | 67 | interface GraphMappingOptionsStatic { 68 | relations: { 69 | follow?: 'MULTI' | 'SIMPLE' | 'MANY2ONE' | 'ONE2MANY' | 'ONE2ONE'; 70 | 71 | mother?: 'MULTI' | 'SIMPLE' | 'MANY2ONE' | 'ONE2MANY' | 'ONE2ONE'; 72 | }; 73 | 74 | properties: { 75 | [index: string]: { 76 | type?: JanusGraphDataTypes; 77 | 78 | cardinality?: 'SINGLE' | 'LIST' | 'SET'; 79 | }; 80 | }; 81 | 82 | indexes: { 83 | [index: string]: { 84 | type?: 'Composite' | 'Mixed' | 'VertexCentric'; 85 | 86 | keys?: Array; 87 | 88 | label?: 'follow'; 89 | 90 | direction?: 'BOTH' | 'IN' | 'OUT'; 91 | 92 | order?: 'incr' | 'decr'; 93 | 94 | unique?: boolean; 95 | }; 96 | }; 97 | } 98 | 99 | type JanusGraphDataTypes = 100 | | 'Integer' 101 | | 'String' 102 | | 'Character' 103 | | 'Boolean' 104 | | 'Byte' 105 | | 'Short' 106 | | 'Long' 107 | | 'Float' 108 | | 'Double' 109 | | 'Date' 110 | | 'Geoshape' 111 | | 'UUID'; 112 | -------------------------------------------------------------------------------- /libs/interfaces/entity-subscriber.interface.ts: -------------------------------------------------------------------------------- 1 | export interface EntitySubscriber { 2 | beforeSave?(instance: Entity, options: any): Promise | boolean | void; 3 | 4 | afterSave?(instance: Entity, options: any): Promise | boolean | void; 5 | 6 | beforeUpdate?( 7 | query: Partial, 8 | updateValues: Partial, 9 | options: any 10 | ): Promise | boolean | void; 11 | 12 | afterUpdate?( 13 | query: Partial, 14 | updateValues: Partial, 15 | options: any 16 | ): Promise | boolean | void; 17 | 18 | beforeDelete?(query: Partial, options: any): Promise | boolean | void; 19 | 20 | afterDelete?(query: Partial, options: any): Promise | boolean | void; 21 | } 22 | -------------------------------------------------------------------------------- /libs/interfaces/externals/scylla-client-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from 'cassandra-driver'; 2 | 3 | export type ConnectionOptions = { name?: string } & ClientOptionsStatic; 4 | 5 | export interface ClientOptionsStatic { 6 | clientOptions: ClientOptions & Partial & Partial; 7 | 8 | ormOptions: Partial; 9 | } 10 | 11 | export interface OrmOptionsStatic { 12 | defaultReplicationStrategy?: { 13 | class?: 'SimpleStrategy' | 'NetworkTopologyStrategy'; 14 | 15 | // eslint-disable-next-line @typescript-eslint/naming-convention 16 | replication_factor?: number; 17 | }; 18 | 19 | migration?: 'safe' | 'alter' | 'drop'; 20 | 21 | createKeyspace?: boolean; 22 | 23 | disableTTYConfirmation?: boolean; 24 | 25 | manageESIndex?: boolean; 26 | 27 | manageGraphs?: boolean; 28 | 29 | udts?: any; 30 | 31 | udfs?: any; 32 | 33 | udas?: any; 34 | } 35 | 36 | export interface ElasticSearchClientOptionsStatic { 37 | elasticsearch: { 38 | host?: string; 39 | 40 | apiVersion?: string; 41 | 42 | sniffOnStart?: boolean; 43 | }; 44 | } 45 | 46 | export interface GreminServerClientOptionsStatic { 47 | gremlin: { 48 | host?: string; 49 | 50 | port?: string | number; 51 | 52 | options?: { 53 | user: string; 54 | 55 | password: string; 56 | }; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /libs/interfaces/externals/scylla-connection.interface.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from './scylla-client-options.interface'; 2 | import { types } from 'cassandra-driver'; 3 | import { BaseModel } from './scylla.interface'; 4 | import Scylla from 'express-cassandra'; 5 | 6 | export interface Connection extends FunctionConstructor { 7 | uuid(): types.Uuid; 8 | uuidFromString(str: string): types.Uuid; 9 | uuidFromBuffer(buffer: Buffer): types.Uuid; 10 | 11 | timeuuid(): types.TimeUuid; 12 | timeuuidFromDate(date: Date): types.TimeUuid; 13 | timeuuidFromString(str: string): types.TimeUuid; 14 | timeuuidFromBuffer(buffer: Buffer): types.TimeUuid; 15 | maxTimeuuid(date: Date): types.TimeUuid; 16 | minTimeuuid(date: Date): types.TimeUuid; 17 | 18 | doBatchAsync(queries: string[]): Promise; 19 | 20 | loadSchema(schema: any, name?: string): BaseModel; 21 | 22 | instance: { [index: string]: BaseModel }; 23 | 24 | orm: any; 25 | 26 | closeAsync(): Promise; 27 | 28 | initAsync(): Promise; 29 | 30 | [index: string]: any; 31 | } 32 | 33 | export interface ScyllaStatic extends Object { 34 | new (options: Partial): Connection; 35 | 36 | createClient(options: ConnectionOptions): Connection; 37 | 38 | [index: string]: any; 39 | } 40 | 41 | export const Connection: ScyllaStatic = Scylla; 42 | -------------------------------------------------------------------------------- /libs/interfaces/externals/scylla.interface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | type Callback = (error: Error, value?: any) => void; 3 | 4 | export interface BaseModel { 5 | new (value?: Partial): BaseModelStatic & T; 6 | 7 | findOne(query: FindQuery, options: { return_query: true } & RawFindQueryOptionsStatic): string; 8 | 9 | findOne(query: FindQuery, callback: Callback): void; 10 | 11 | findOne(query: FindQuery, options: RawFindQueryOptionsStatic, callback: Callback): void; 12 | 13 | findOneAsync(query: FindQuery, options: RawFindQueryOptionsStatic & { raw: true }): Promise; 14 | 15 | findOneAsync(query: FindQuery, options?: RawFindQueryOptionsStatic): Promise>; 16 | 17 | find(query: FindQuery, options?: { return_query: true } & RawFindQueryOptionsStatic): string; 18 | 19 | find(query: FindQuery, callback: Callback): void; 20 | 21 | find(query: FindQuery, options: RawFindQueryOptionsStatic, callback: Callback): void; 22 | 23 | findAsync(query: FindQuery, options: RawFindQueryOptionsStatic & { raw: true }): Promise; 24 | 25 | findAsync(query: FindQuery, options?: RawFindQueryOptionsStatic): Promise[]>; 26 | 27 | update( 28 | query: FindQuery, 29 | updateValue: Partial, 30 | options: { return_query: true } & UpdateOptionsStatic 31 | ): string; 32 | 33 | update(query: FindQuery, updateValue: Partial, callback?: Callback): void; 34 | 35 | update(query: FindQuery, updateValue: Partial, options: UpdateOptionsStatic, callback?: Callback): void; 36 | 37 | updateAsync(query: FindQuery, updateValue: Partial, options?: UpdateOptionsStatic): Promise; 38 | 39 | delete(query: FindQuery, options: { return_query: true } & DeleteOptionsStatic): string; 40 | 41 | delete(query: FindQuery, callback?: Callback): void; 42 | 43 | delete(query: FindQuery, options?: DeleteOptionsStatic, callback?: Callback): void; 44 | 45 | deleteAsync(query: FindQuery, options?: DeleteOptionsStatic): Promise; 46 | 47 | truncateAsync(): Promise; 48 | 49 | stream( 50 | query: FindQuery, 51 | options: RawFindQueryOptionsStatic, 52 | reader: (reader) => void, 53 | done: (err: Error) => void 54 | ): void; 55 | 56 | eachRow( 57 | query: FindQuery, 58 | options: RawFindQueryOptionsStatic, 59 | onRow: (n, row) => void, 60 | done: (err: Error, result: any) => void 61 | ): void; 62 | 63 | execute_query(query: string, params: any[], callback?: Callback): void; 64 | 65 | execute_batch(queries: { query: string; params: any[] }[], callback?: (err: Error) => void): void; 66 | 67 | close(callback?: (err: Error) => void): void; 68 | 69 | syncDB(callback?: (err: Error, result: boolean) => void); 70 | 71 | get_keyspace_name(): string; 72 | 73 | get_table_name(): string; 74 | 75 | get_cql_client(): any; 76 | 77 | search(options: EsSearchOptionsStatic, callback?: Callback): void; 78 | 79 | get_es_client(): any; 80 | 81 | createVertex(entity: Partial, callback?: Callback): void; 82 | 83 | getVertex(id: any, callback?: Callback): void; 84 | 85 | updateVertex(id: any, updateEntity: Partial, callback?: Callback): void; 86 | 87 | deleteVertex(id: any, callback?: (err: Error) => void): void; 88 | 89 | createEdge(relation: string, followerVertexId: any, followeeVertexId: any, callback?: Callback): void; 90 | 91 | createEdge(relation: string, followerVertexId: any, followeeVertexId: any, model: any, callback?: Callback): void; 92 | 93 | getEdge(id: any, callback?: Callback): void; 94 | 95 | updateEdge(id: any, updateModel: any, callback?: Callback): void; 96 | 97 | deleteEdge(id: any, callback?: (err: Error) => void): void; 98 | 99 | graphQuery(query: string, entityQuery: Partial, callback?: Callback): void; 100 | 101 | get_gremlin_client(): GremlinClientStatic; 102 | 103 | [index: string]: any; 104 | } 105 | 106 | export interface BaseModelStatic { 107 | save(options: { return_query: boolean } & SaveOptionsStatic): string; 108 | 109 | save(callback?: Callback): void; 110 | 111 | save(options: SaveOptionsStatic, callback?: Callback): void; 112 | 113 | saveAsync(options?: SaveOptionsStatic): Promise; 114 | 115 | delete(options: { return_query: boolean } & DeleteOptionsStatic): string; 116 | 117 | delete(callback?: Callback): void; 118 | 119 | delete(options: DeleteOptionsStatic, callback?: Callback): void; 120 | 121 | deleteAsync(options?: DeleteOptionsStatic): Promise; 122 | 123 | isModify(key?: keyof T): boolean; 124 | 125 | toJSON(): T; 126 | 127 | [index: string]: any; 128 | } 129 | 130 | export interface RawFindQueryOptionsStatic { 131 | select?: Array; 132 | 133 | materialized_view?: string; 134 | 135 | allow_filtering?: boolean; 136 | 137 | distinct?: boolean; 138 | 139 | autoPage?: boolean; 140 | 141 | fetchSize?: number; 142 | 143 | pageState?: string; 144 | 145 | raw?: boolean; 146 | 147 | prepare?: boolean; 148 | 149 | [index: string]: any; 150 | } 151 | 152 | export interface FindQueryOptionsStatic { 153 | select?: Array; 154 | 155 | materializedView?: string; 156 | 157 | allowFiltering?: boolean; 158 | 159 | distinct?: boolean; 160 | 161 | autoPage?: boolean; 162 | 163 | fetchSize?: number; 164 | 165 | pageState?: string; 166 | 167 | raw?: boolean; 168 | 169 | prepare?: boolean; 170 | 171 | [index: string]: any; 172 | } 173 | 174 | export type FindQuery = { [P in keyof T]?: T[P] | FindSubQueryStatic } & FindQueryStatic; 175 | 176 | export interface FindQueryStatic { 177 | $orderby?: { 178 | $asc?: keyof T | Array; 179 | $desc?: keyof T | Array; 180 | }; 181 | 182 | $limit?: number; 183 | } 184 | 185 | export interface FindSubQueryStatic { 186 | $token?: any; 187 | 188 | $in?: string[]; 189 | 190 | $like?: string; 191 | 192 | $eq?: any; 193 | 194 | $ne?: any; 195 | 196 | $isnt?: any; 197 | 198 | $gt?: any; 199 | 200 | $lt?: any; 201 | 202 | $gte?: any; 203 | 204 | $lte?: any; 205 | 206 | $contains?: string; 207 | 208 | $contains_key?: string[]; 209 | 210 | $solr_query?: string; 211 | } 212 | 213 | export interface SaveOptionsStatic { 214 | ttl?: number; 215 | 216 | if_not_exist?: boolean; 217 | } 218 | 219 | export interface UpdateOptionsStatic { 220 | ttl?: number; 221 | 222 | if_exists?: boolean; 223 | 224 | conditions?: { [P in keyof T]?: T[P] }; 225 | } 226 | 227 | export interface DeleteOptionsStatic { 228 | if_exists?: boolean; 229 | } 230 | 231 | export interface EsSearchOptionsStatic { 232 | q?: string; 233 | 234 | from?: number; 235 | 236 | size?: number; 237 | 238 | sort?: string[]; 239 | 240 | body?: { 241 | query?: any; 242 | 243 | [index: string]: any; 244 | }; 245 | 246 | [index: string]: any; 247 | } 248 | 249 | interface GremlinClientStatic { 250 | execute(query: string, entityQuery: Partial, callback?: Callback): void; 251 | 252 | [index: string]: any; 253 | } 254 | -------------------------------------------------------------------------------- /libs/interfaces/scylla-module-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ModuleMetadata } from '@nestjs/common/interfaces'; 3 | import { ConnectionOptions } from './externals/scylla-client-options.interface'; 4 | 5 | export type ScyllaModuleOptions = { 6 | retryAttempts?: number; 7 | 8 | retryDelay?: number; 9 | 10 | keepConnectionAlive?: boolean; 11 | } & Partial; 12 | 13 | export interface ScyllaOptionsFactory { 14 | createScyllaOptions(): Promise | ScyllaModuleOptions; 15 | } 16 | 17 | export interface ScyllaModuleAsyncOptions extends Pick { 18 | name?: string; 19 | 20 | useExisting?: Type; 21 | 22 | useClass?: Type; 23 | 24 | useFactory?: (...args: any[]) => Promise | ScyllaModuleOptions; 25 | 26 | inject?: any[]; 27 | } 28 | -------------------------------------------------------------------------------- /libs/repositories/builder/return-query.builder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { 3 | BaseModel, 4 | SaveOptionsStatic, 5 | FindQuery, 6 | DeleteOptionsStatic, 7 | UpdateOptionsStatic 8 | } from '../../interfaces/externals/scylla.interface'; 9 | 10 | export class ReturnQueryBuilder { 11 | constructor(private readonly model: BaseModel) {} 12 | 13 | save(model: Partial, options: SaveOptionsStatic = {}): string { 14 | return new this.model(model).save({ ...options, return_query: true }); 15 | } 16 | 17 | update(query: FindQuery = {}, updateValue: Partial, options: UpdateOptionsStatic = {}): string { 18 | return this.model.update(query, updateValue, { 19 | ...options, 20 | return_query: true 21 | }); 22 | } 23 | 24 | delete(query: FindQuery = {}, options: DeleteOptionsStatic = {}): string { 25 | return this.model.delete(query, { ...options, return_query: true }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/repositories/repository.factory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { Repository } from './repository'; 3 | import { BaseModel } from '../interfaces/externals/scylla.interface'; 4 | import { ReturnQueryBuilder } from './builder/return-query.builder'; 5 | 6 | export class RepositoryFactory { 7 | static create(entity: Function, model: BaseModel, EntityRepository = Repository): Repository { 8 | const repository = new EntityRepository(); 9 | const returnQueryBuilder = new ReturnQueryBuilder(model); 10 | Object.assign(repository, { target: entity, model, returnQueryBuilder }); 11 | return repository; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libs/repositories/repository.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { types } from 'cassandra-driver'; 3 | import { Observable, Subject, defer, lastValueFrom } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import snakecaseKeys from 'snakecase-keys'; 6 | import { EntityNotFoundError } from '../errors/entity-not-found.error'; 7 | import { ConnectionOptions } from '../interfaces/externals/scylla-client-options.interface'; 8 | import { Connection } from '../interfaces/externals/scylla-connection.interface'; 9 | import { 10 | BaseModel, 11 | DeleteOptionsStatic, 12 | FindQuery, 13 | FindQueryOptionsStatic, 14 | RawFindQueryOptionsStatic, 15 | SaveOptionsStatic, 16 | UpdateOptionsStatic 17 | } from '../interfaces/externals/scylla.interface'; 18 | import { uuid } from '../utils/db.utils'; 19 | import { getEntity } from '../utils/decorator.utils'; 20 | import { getSchema } from '../utils/model.utils'; 21 | import { transformEntity } from '../utils/transform-entity.utils'; 22 | import { ReturnQueryBuilder } from './builder/return-query.builder'; 23 | import { RepositoryFactory } from './repository.factory'; 24 | import { snakeCase } from 'lodash'; 25 | 26 | const defaultOptions = { 27 | findOptions: { raw: true }, 28 | updateOptions: snakecaseKeys({ ifExists: true }), 29 | deleteOptions: snakecaseKeys({ ifExists: true }) 30 | }; 31 | 32 | const convertedFindQueryOptions = ['materializedView', 'allowFiltering']; 33 | 34 | export class Repository { 35 | readonly model: BaseModel; 36 | 37 | readonly target: Type; 38 | 39 | readonly returnQueryBuilder: ReturnQueryBuilder; 40 | 41 | constructor() {} 42 | 43 | static make(options: Partial): Repository { 44 | const entity = getEntity(this); 45 | const connection = new Connection(options); 46 | const model = connection.loadSchema(entity.name, getSchema(entity)); 47 | 48 | return RepositoryFactory.create(entity, model, this); 49 | } 50 | 51 | private mapFindQueryOptions(options?: FindQueryOptionsStatic): RawFindQueryOptionsStatic { 52 | return Object.entries(options).reduce( 53 | (option, [key, value]) => ({ 54 | ...option, 55 | [`${convertedFindQueryOptions.includes(key) ? snakeCase(key) : key}`]: value 56 | }), 57 | {} 58 | ); 59 | } 60 | 61 | create(entity?: Partial): Entity; 62 | 63 | create(entityLikeArray: Partial[]): Entity[]; 64 | 65 | create(entityLike?: any): Entity | Entity[] { 66 | return transformEntity(this.target, entityLike); 67 | } 68 | 69 | findOne(query: FindQuery, options?: FindQueryOptionsStatic): Promise; 70 | 71 | findOne( 72 | query: FindQuery, 73 | options: FindQueryOptionsStatic = { allowFiltering: true } 74 | ): Promise { 75 | if (query[`id`] && typeof query[`id`] === 'string') { 76 | query[`id`] = uuid(query[`id`]); 77 | } 78 | 79 | return lastValueFrom( 80 | defer(() => 81 | this.model.findOneAsync(query, { 82 | ...this.mapFindQueryOptions(options), 83 | ...defaultOptions.findOptions 84 | }) 85 | ).pipe(map((x) => x && transformEntity(this.target, x))) 86 | ); 87 | } 88 | 89 | findOneOrFail(query: FindQuery, options?: FindQueryOptionsStatic): Promise; 90 | 91 | findOneOrFail(query: FindQuery, maybeOptions: FindQueryOptionsStatic = {}): Promise { 92 | return this.findOne(query, maybeOptions).then((entity) => { 93 | if (entity === undefined) { 94 | throw new EntityNotFoundError(this.target, query); 95 | } 96 | return entity; 97 | }); 98 | } 99 | 100 | find(query: FindQuery, options?: FindQueryOptionsStatic): Promise; 101 | 102 | find( 103 | query: FindQuery, 104 | options: FindQueryOptionsStatic = { allowFiltering: true } 105 | ): Promise { 106 | if (query[`id`] && typeof query[`id`] === 'string') { 107 | query[`id`] = uuid(query[`id`]); 108 | } 109 | 110 | return lastValueFrom( 111 | defer(() => 112 | this.model.findAsync(query, { 113 | ...this.mapFindQueryOptions(options), 114 | ...defaultOptions.findOptions 115 | }) 116 | ).pipe(map((x) => transformEntity(this.target, x))) 117 | ); 118 | } 119 | 120 | findAndCount( 121 | query: FindQuery, 122 | options: FindQueryOptionsStatic = { allowFiltering: true } 123 | ): Promise<[Entity[], number]> { 124 | if (query[`id`] && typeof query[`id`] === 'string') { 125 | query[`id`] = uuid(query[`id`]); 126 | } 127 | 128 | return lastValueFrom( 129 | defer(() => 130 | this.model.findAsync(query, { 131 | ...this.mapFindQueryOptions(options), 132 | ...defaultOptions.findOptions 133 | }) 134 | ).pipe( 135 | map((x) => transformEntity(this.target, x)), 136 | map((entities) => [entities, entities.length] as [Entity[], number]) 137 | ) 138 | ); 139 | } 140 | 141 | save(entity: Partial, options?: SaveOptionsStatic): Promise; 142 | 143 | save(entities: Partial[], options?: SaveOptionsStatic): Promise; 144 | 145 | save( 146 | entityLike: Partial | Partial[], 147 | options: SaveOptionsStatic = {} 148 | ): Promise | Promise { 149 | const saveFunc = async (entity) => { 150 | const model = new this.model(entity); 151 | await model.saveAsync(options); 152 | return transformEntity(this.target, model.toJSON()); 153 | }; 154 | const saveMultipleFunc = (arrayLike: Entity[]) => Promise.all(arrayLike.map((x) => saveFunc(x))); 155 | 156 | return Array.isArray(entityLike) 157 | ? lastValueFrom(defer(() => saveMultipleFunc(entityLike as any))) 158 | : lastValueFrom(defer(() => saveFunc(entityLike as any))); 159 | } 160 | 161 | update(query: FindQuery, updateValue: Partial, options?: UpdateOptionsStatic): Promise; 162 | 163 | update( 164 | query: FindQuery, 165 | updateValue: Partial, 166 | options: UpdateOptionsStatic = {} 167 | ): Promise { 168 | if (query[`id`] && typeof query[`id`] === 'string') { 169 | query[`id`] = uuid(query[`id`]); 170 | } 171 | 172 | return lastValueFrom( 173 | defer(() => 174 | this.model.updateAsync(query, updateValue, { 175 | ...defaultOptions.updateOptions, 176 | ...options 177 | }) 178 | ) 179 | ); 180 | } 181 | 182 | remove(entity: Entity, options?: DeleteOptionsStatic): Promise; 183 | 184 | remove(entity: Entity[], options?: DeleteOptionsStatic): Promise; 185 | 186 | remove(entityOrEntities: Entity | Entity[], options: DeleteOptionsStatic = {}): Promise { 187 | const removeFunc = (entity) => 188 | new this.model(entity).deleteAsync({ 189 | ...defaultOptions.deleteOptions, 190 | ...options 191 | }); 192 | const promiseArray = 193 | entityOrEntities instanceof Array 194 | ? entityOrEntities.map((x) => removeFunc(x)) 195 | : [removeFunc(entityOrEntities)]; 196 | 197 | return lastValueFrom(defer(() => Promise.all(promiseArray)).pipe(map(() => entityOrEntities))); 198 | } 199 | 200 | delete(query: FindQuery, options?: DeleteOptionsStatic): Promise; 201 | 202 | delete(query: FindQuery = {}, options = {}) { 203 | if (query[`id`] && typeof query[`id`] === 'string') { 204 | query[`id`] = uuid(query[`id`]); 205 | } 206 | 207 | return lastValueFrom( 208 | defer(() => 209 | this.model.deleteAsync(query, { 210 | ...defaultOptions.deleteOptions, 211 | ...options 212 | }) 213 | ) 214 | ); 215 | } 216 | 217 | truncate(): Promise { 218 | return lastValueFrom(defer(() => this.model.truncateAsync())); 219 | } 220 | 221 | stream( 222 | query: FindQuery, 223 | options: FindQueryOptionsStatic = { allowFiltering: true } 224 | ): Promise { 225 | const reader$ = new Subject(); 226 | 227 | const onRead = (reader): void => { 228 | while (true) { 229 | const row = reader.readRow(); 230 | if (row === null) { 231 | break; 232 | } 233 | reader$.next(transformEntity(this.target, row)); 234 | } 235 | }; 236 | 237 | const onDone = (error): void => { 238 | if (error) { 239 | reader$.error(error); 240 | } 241 | reader$.complete(); 242 | return; 243 | }; 244 | 245 | if (query[`id`] && typeof query[`id`] === 'string') { 246 | query[`id`] = uuid(query[`id`]); 247 | } 248 | 249 | this.model.stream( 250 | query, 251 | { ...this.mapFindQueryOptions(options), ...defaultOptions.findOptions }, 252 | onRead, 253 | onDone 254 | ); 255 | 256 | return lastValueFrom(reader$.asObservable()); 257 | } 258 | 259 | eachRow( 260 | query: FindQuery, 261 | options: FindQueryOptionsStatic = { allowFiltering: true } 262 | ): EachRowArgument { 263 | const reader$ = new Subject(); 264 | const done$ = new Subject(); 265 | const getReader = () => reader$.asObservable(); 266 | const getDone = () => done$.asObservable(); 267 | 268 | const onRow = (n, row): void => reader$.next(transformEntity(this.target, row)); 269 | const onDone = (err: Error, result: any): void => { 270 | if (err) { 271 | reader$.error(err); 272 | done$.error(err); 273 | } else { 274 | done$.next(result); 275 | } 276 | reader$.complete(); 277 | done$.complete(); 278 | }; 279 | 280 | if (query[`id`] && typeof query[`id`] === 'string') { 281 | query[`id`] = uuid(query[`id`]); 282 | } 283 | 284 | this.model.eachRow( 285 | query, 286 | { ...this.mapFindQueryOptions(options), ...defaultOptions.findOptions }, 287 | onRow, 288 | onDone 289 | ); 290 | 291 | return { getReader, getDone }; 292 | } 293 | 294 | get getModelRef(): BaseModel { 295 | return this.model; 296 | } 297 | 298 | getReturnQueryBuilder(): ReturnQueryBuilder { 299 | return this.returnQueryBuilder; 300 | } 301 | 302 | doBatch(queries): Promise { 303 | return this.model.execute_batchAsync(queries); 304 | } 305 | } 306 | 307 | export interface EachRowArgument { 308 | getReader(): Observable; 309 | 310 | getDone(): Observable; 311 | } 312 | -------------------------------------------------------------------------------- /libs/scylla-core.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Inject, Logger, Module, OnModuleDestroy, Provider } from '@nestjs/common'; 2 | import { ModuleRef } from '@nestjs/core'; 3 | import { defer, lastValueFrom } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { ConnectionOptions } from './interfaces/externals/scylla-client-options.interface'; 6 | import { Connection } from './interfaces/externals/scylla-connection.interface'; 7 | import { 8 | ScyllaModuleAsyncOptions, 9 | ScyllaModuleOptions, 10 | ScyllaOptionsFactory 11 | } from './interfaces/scylla-module-options.interface'; 12 | import { SCYLLA_DB_MODULE_ID, SCYLLA_DB_MODULE_OPTIONS } from './scylla.constant'; 13 | import { generateString, getConnectionToken, handleRetry } from './utils/scylla.utils'; 14 | 15 | @Global() 16 | @Module({}) 17 | export class ScyllaCoreModule implements OnModuleDestroy { 18 | constructor( 19 | @Inject(SCYLLA_DB_MODULE_OPTIONS) 20 | private readonly options: ScyllaModuleOptions, 21 | private readonly moduleRef: ModuleRef 22 | ) {} 23 | 24 | static forRoot(options: ScyllaModuleOptions = {}): DynamicModule { 25 | const expressModuleOptions = { 26 | provide: SCYLLA_DB_MODULE_OPTIONS, 27 | useValue: options 28 | }; 29 | const connectionProvider = { 30 | provide: getConnectionToken(options as ConnectionOptions), 31 | useFactory: async () => await this.createConnectionFactory(options) 32 | }; 33 | 34 | return { 35 | module: ScyllaCoreModule, 36 | providers: [expressModuleOptions, connectionProvider], 37 | exports: [connectionProvider] 38 | }; 39 | } 40 | 41 | static forRootAsync(options: ScyllaModuleAsyncOptions): DynamicModule { 42 | const connectionProvider = { 43 | provide: getConnectionToken(options as ConnectionOptions), 44 | useFactory: async (ormOptions: ScyllaModuleOptions) => { 45 | if (options.name) { 46 | return await this.createConnectionFactory({ 47 | ...ormOptions, 48 | name: options.name 49 | }); 50 | } 51 | return await this.createConnectionFactory(ormOptions); 52 | }, 53 | inject: [SCYLLA_DB_MODULE_OPTIONS] 54 | }; 55 | 56 | const asyncProviders = this.createAsyncProviders(options); 57 | return { 58 | module: ScyllaCoreModule, 59 | imports: options.imports, 60 | providers: [ 61 | ...asyncProviders, 62 | connectionProvider, 63 | { 64 | provide: SCYLLA_DB_MODULE_ID, 65 | useValue: generateString() 66 | } 67 | ], 68 | exports: [connectionProvider] 69 | }; 70 | } 71 | 72 | async onModuleDestroy() { 73 | if (this.options.keepConnectionAlive) { 74 | return; 75 | } 76 | Logger.log('Closing connection', 'ScyllaModule'); 77 | const connection = this.moduleRef.get(getConnectionToken(this.options as ConnectionOptions) as any); 78 | connection && (await connection.closeAsync()); 79 | } 80 | 81 | private static createAsyncProviders(options: ScyllaModuleAsyncOptions): Provider[] { 82 | if (options.useExisting || options.useFactory) { 83 | return [this.createAsyncOptionsProvider(options)]; 84 | } 85 | return [ 86 | this.createAsyncOptionsProvider(options), 87 | { 88 | provide: options.useClass, 89 | useClass: options.useClass 90 | } 91 | ]; 92 | } 93 | 94 | private static createAsyncOptionsProvider(options: ScyllaModuleAsyncOptions): Provider { 95 | if (options.useFactory) { 96 | return { 97 | provide: SCYLLA_DB_MODULE_OPTIONS, 98 | useFactory: options.useFactory, 99 | inject: options.inject || [] 100 | }; 101 | } 102 | return { 103 | provide: SCYLLA_DB_MODULE_OPTIONS, 104 | useFactory: async (optionsFactory: ScyllaOptionsFactory) => await optionsFactory.createScyllaOptions(), 105 | inject: [options.useClass || options.useExisting] 106 | }; 107 | } 108 | 109 | private static async createConnectionFactory(options: ScyllaModuleOptions): Promise { 110 | const { retryAttempts, retryDelay, ...scyllaOptions } = options; 111 | const connection = new Connection(scyllaOptions); 112 | 113 | return await lastValueFrom( 114 | defer(() => connection.initAsync()).pipe( 115 | handleRetry(retryAttempts, retryDelay), 116 | map(() => connection) 117 | ) 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /libs/scylla.constant.ts: -------------------------------------------------------------------------------- 1 | export const SCYLLA_DB_MODULE_OPTIONS = 'ScyllaModuleOptions'; 2 | export const SCYLLA_DB_MODULE_ID = 'ScyllaModuleId'; 3 | -------------------------------------------------------------------------------- /libs/scylla.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { ScyllaCoreModule } from './scylla-core.module'; 3 | import { createScyllaProviders } from './scylla.providers'; 4 | import { ScyllaModuleAsyncOptions, ScyllaModuleOptions } from './interfaces/scylla-module-options.interface'; 5 | import { Connection } from './interfaces/externals/scylla-connection.interface'; 6 | import { ConnectionOptions } from './interfaces/externals/scylla-client-options.interface'; 7 | 8 | @Module({}) 9 | export class ScyllaModule { 10 | static forRoot(options: ScyllaModuleOptions): DynamicModule { 11 | return { 12 | module: ScyllaModule, 13 | imports: [ScyllaCoreModule.forRoot(options)] 14 | }; 15 | } 16 | 17 | static forFeature( 18 | entities: Function[] = [], 19 | connection: Connection | ConnectionOptions | string = 'default' 20 | ): DynamicModule { 21 | const providers = createScyllaProviders(entities, connection); 22 | return { 23 | module: ScyllaModule, 24 | providers, 25 | exports: providers 26 | }; 27 | } 28 | 29 | static forRootAsync(options: ScyllaModuleAsyncOptions): DynamicModule { 30 | return { 31 | module: ScyllaModule, 32 | imports: [ScyllaCoreModule.forRootAsync(options)] 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libs/scylla.providers.ts: -------------------------------------------------------------------------------- 1 | import { getModelToken, getConnectionToken, getRepositoryToken } from './utils/scylla.utils'; 2 | import { defer, lastValueFrom } from 'rxjs'; 3 | import { getEntity } from './utils/decorator.utils'; 4 | import { Provider } from '@nestjs/common'; 5 | import { RepositoryFactory } from './repositories/repository.factory'; 6 | import { Repository } from './repositories/repository'; 7 | import { loadModel } from './utils/model.utils'; 8 | import { Connection } from './interfaces/externals/scylla-connection.interface'; 9 | import { ConnectionOptions } from './interfaces/externals/scylla-client-options.interface'; 10 | 11 | export function createScyllaProviders(entities?: Function[], connection?: Connection | ConnectionOptions | string) { 12 | const providerModel = (entity) => ({ 13 | provide: getModelToken(entity), 14 | useFactory: async (connectionLike: Connection) => { 15 | return await lastValueFrom(defer(() => loadModel(connectionLike, entity))); 16 | }, 17 | inject: [getConnectionToken(connection)] 18 | }); 19 | 20 | const provideRepository = (entity) => ({ 21 | provide: getRepositoryToken(entity), 22 | useFactory: async (model) => RepositoryFactory.create(entity, model), 23 | inject: [getModelToken(entity)] 24 | }); 25 | 26 | const provideCustomRepository = (entityRepository) => { 27 | const entity = getEntity(entityRepository); 28 | 29 | return { 30 | provide: getRepositoryToken(entityRepository), 31 | useFactory: async (model) => RepositoryFactory.create(entity, model, entityRepository), 32 | inject: [getModelToken(entity)] 33 | }; 34 | }; 35 | 36 | const providers: Provider[] = []; 37 | (entities || []).forEach((entity) => { 38 | if (entity.prototype instanceof Repository) { 39 | return providers.push(provideCustomRepository(entity)); 40 | } 41 | return providers.push(providerModel(entity), provideRepository(entity)); 42 | }); 43 | 44 | return [...providers]; 45 | } 46 | -------------------------------------------------------------------------------- /libs/utils/db.utils.ts: -------------------------------------------------------------------------------- 1 | import { types } from 'cassandra-driver'; 2 | 3 | export const isUuid = (id: any): boolean => id && id instanceof types.Uuid; 4 | 5 | export const uuid = (id?: any): types.Uuid => { 6 | if (!id) { 7 | return types.Uuid.random(); 8 | } 9 | if (typeof id === 'string') { 10 | return types.Uuid.fromString(id); 11 | } 12 | return id; 13 | }; 14 | 15 | export const isTimeUuid = (id: any): boolean => id && id instanceof types.TimeUuid; 16 | 17 | export const timeuuid = (idOrDate?: string | Date): types.TimeUuid => { 18 | if (!idOrDate) { 19 | return new types.TimeUuid(); 20 | } 21 | if (typeof idOrDate === 'string') { 22 | return types.TimeUuid.fromString(idOrDate); 23 | } 24 | if (idOrDate instanceof Date) { 25 | return types.TimeUuid.fromDate(idOrDate); 26 | } 27 | return idOrDate; 28 | }; 29 | -------------------------------------------------------------------------------- /libs/utils/decorator.utils.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ENTITY_NAME_KEY, ATTRIBUTE_KEY, OPTIONS_KEY, ENTITY_METADATA } from '../constants/constant'; 3 | import { mergeDeep } from './deep-merge.utils'; 4 | 5 | export function setEntity(target: any, entity: Function): void { 6 | Reflect.defineMetadata(ENTITY_METADATA, entity, target); 7 | } 8 | 9 | export function getEntity(target: any): Function { 10 | return Reflect.getMetadata(ENTITY_METADATA, target); 11 | } 12 | 13 | export function setEntityName(target: any, modelName: string): void { 14 | Reflect.defineMetadata(ENTITY_NAME_KEY, modelName, target); 15 | } 16 | 17 | export function getEntityName(target: any): string { 18 | return Reflect.getMetadata(ENTITY_NAME_KEY, target); 19 | } 20 | 21 | export function getAttributes(target: any): any | undefined { 22 | const attributes = Reflect.getMetadata(ATTRIBUTE_KEY, target); 23 | 24 | if (attributes) { 25 | return Object.keys(attributes).reduce((copy, key) => { 26 | copy[key] = { ...attributes[key] }; 27 | return copy; 28 | }, {}); 29 | } 30 | } 31 | 32 | export function setAttributes(target: any, attributes: any) { 33 | Reflect.defineMetadata(ATTRIBUTE_KEY, { ...attributes }, target); 34 | } 35 | 36 | export function addAttribute(target: any, name: string, options: any): void { 37 | const attributes = getAttributes(target) || {}; 38 | attributes[name] = { ...options }; 39 | setAttributes(target, attributes); 40 | } 41 | 42 | export function addAttributeOptions(target: any, propertyName: string, options: any): void { 43 | const attributes = getAttributes(target); 44 | attributes[propertyName] = mergeDeep(attributes[propertyName], options); 45 | setAttributes(target, attributes); 46 | } 47 | 48 | export function getOptions(target: any): any | undefined { 49 | const options = Reflect.getMetadata(OPTIONS_KEY, target); 50 | return { ...options } || {}; 51 | } 52 | 53 | export function setOptions(target: any, options: any): void { 54 | Reflect.defineMetadata(OPTIONS_KEY, { ...options }, target); 55 | } 56 | 57 | export function addOptions(target: any, options: any): void { 58 | const mOptions = getOptions(target) || {}; 59 | setOptions(target, mergeDeep(mOptions, options)); 60 | } 61 | 62 | export const addHookFunction = (target: object, metadataKey: string) => { 63 | const funcLikeArray: any[] = Reflect.getMetadata(metadataKey, target) || []; 64 | return (...args: any[]) => funcLikeArray.map((funcLike) => funcLike(...args)); 65 | }; 66 | -------------------------------------------------------------------------------- /libs/utils/deep-merge.utils.ts: -------------------------------------------------------------------------------- 1 | import merge from 'deepmerge'; 2 | 3 | export function mergeDeep(target, sources): object { 4 | return merge(target, sources); 5 | } 6 | -------------------------------------------------------------------------------- /libs/utils/model.utils.ts: -------------------------------------------------------------------------------- 1 | import { getAttributes, getOptions } from './decorator.utils'; 2 | import { Logger } from '@nestjs/common'; 3 | 4 | export function loadModel(connection: any, entity: any): Promise { 5 | const schema = getSchema(entity); 6 | const modelName = entity.name || entity.table_name; 7 | const model = connection.loadSchema(modelName, schema); 8 | 9 | return new Promise((resolve) => { 10 | model.syncDB((err) => { 11 | if (err) { 12 | Logger.error(err.message, err.stack, 'ScyllaModule'); 13 | return resolve(model); 14 | } 15 | return resolve(model); 16 | }); 17 | }); 18 | } 19 | 20 | export function getSchema(entity: Function) { 21 | const attributes = getAttributes(entity.prototype); 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | const { instanceMethods, classMethods, ...options } = getOptions(entity.prototype); 24 | const model = { ...options }; 25 | model.fields = { ...attributes }; 26 | return model; 27 | } 28 | -------------------------------------------------------------------------------- /libs/utils/scylla.utils.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { delay, retryWhen, scan } from 'rxjs/operators'; 3 | import { Logger, Type } from '@nestjs/common'; 4 | import { Repository } from '../repositories/repository'; 5 | import { ConnectionOptions } from '../interfaces/externals/scylla-client-options.interface'; 6 | import { Connection } from '../interfaces/externals/scylla-connection.interface'; 7 | 8 | export function handleRetry( 9 | retryAttempts: number = 6, 10 | retryDelay: number = 3000 11 | ): (source: Observable) => Observable { 12 | return (source: Observable) => 13 | source.pipe( 14 | retryWhen((e) => 15 | e.pipe( 16 | scan((errorCount: number, error: Error) => { 17 | Logger.error( 18 | `Unable to connect to the database. Retrying (${errorCount + 1})...`, 19 | error.stack, 20 | 'ScyllaModule' 21 | ); 22 | if (errorCount + 1 >= retryAttempts) { 23 | throw error; 24 | } 25 | return errorCount + 1; 26 | }, 0), 27 | delay(retryDelay) 28 | ) 29 | ) 30 | ); 31 | } 32 | 33 | /** 34 | * This function returns a Connection injection token for given Connection, ConnectionOptions or connection name. 35 | * @param {(Connection | ConnectionOptions | string)} [connection='default'] This optional parameter is either 36 | * a Connection or a ConnectionOptions or a string. 37 | * @returns {(string | Function | Type)} The Connection injection token. 38 | */ 39 | export function getConnectionToken( 40 | connection: Connection | ConnectionOptions | string = 'default' 41 | ): string | Function | Type { 42 | return 'default' === connection 43 | ? Connection 44 | : 'string' === typeof connection 45 | ? `${connection}Connection` 46 | : 'default' === connection.name || !connection.name 47 | ? Connection 48 | : `${connection.name}Connection`; 49 | } 50 | 51 | /** 52 | * This function returns a Cassandra model token for given entity. 53 | * @param {Function} entity This parameter is an Entity class. 54 | * @returns {string} The Cassandra model injection token. 55 | */ 56 | export function getModelToken(entity: Function): string { 57 | return `${entity.name}Model`; 58 | } 59 | 60 | /** 61 | * This function returns a Repository injection token for given entity. 62 | * @param {Function} entity This options is either an Entity class or Repository. 63 | * @returns {string} The Repository injection token. 64 | */ 65 | export function getRepositoryToken(entity: Function): string { 66 | if (entity.prototype instanceof Repository) { 67 | return entity.name; 68 | } 69 | return `${entity.name}Repository`; 70 | } 71 | 72 | export function getConnectionName(options: ConnectionOptions) { 73 | return options && options.name ? options.name : 'default'; 74 | } 75 | 76 | export const generateString = () => 77 | // tslint:disable-next-line:no-bitwise 78 | [...Array(10)].map((i) => ((Math.random() * 36) | 0).toString(36)).join; 79 | -------------------------------------------------------------------------------- /libs/utils/transform-entity.utils.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | 3 | export function transformEntity(target: Type, entityLike: any[]): T[]; 4 | export function transformEntity(target: Type, entityLike: any): T; 5 | 6 | export function transformEntity(target: Type, entityLike: any): T | T[] { 7 | if (!target || !(target && typeof target === 'function') || !entityLike) { 8 | return entityLike; 9 | } 10 | if (entityLike instanceof Array) { 11 | return entityLike.map((entity) => Object.assign(new target(), entity)); 12 | } 13 | return Object.assign(new target(), entityLike); 14 | } 15 | -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "default", 3 | "type": "postgres", 4 | "host": "localhost", 5 | "port": 9432, 6 | "username": "test", 7 | "password": "test", 8 | "database": "test", 9 | "synchronize": false, 10 | "logging": false, 11 | "keepConnectionAlive": true, 12 | "migrationsRun": true, 13 | "migrationsDir": "src/migration", 14 | "entities": [ 15 | "src/entity/*.js" 16 | ], 17 | "subscribers": [ 18 | "src/subscriber/*.ts" 19 | ], 20 | "migrations": [ 21 | "src/migration/*.ts" 22 | ], 23 | "cli": { 24 | "entitiesDir": "src/entity", 25 | "migrationsDir": "src/migration", 26 | "subscribersDir": "src/subscriber" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hodfords/nestjs-scylladb", 3 | "version": "10.0.6", 4 | "description": "NestJS ScyllaDB", 5 | "license": "MIT", 6 | "readmeFilename": "README.md", 7 | "author": { 8 | "name": "Khang Tran Thanh", 9 | "email": "khang.tran@hodfords.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hodfords-solutions/nestjs-scylladb" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/hodford/nestjs-scylladb/issues" 17 | }, 18 | "tags": [ 19 | "orm", 20 | "typescript", 21 | "typescript-orm", 22 | "nestjs-scylladb", 23 | "nestjs-scylladb-sample", 24 | "nestjs-scylladb-example" 25 | ], 26 | "devDependencies": { 27 | "@nestjs/common": "10.2.10", 28 | "@nestjs/core": "10.2.10", 29 | "@nestjs/platform-express": "^10.3.0", 30 | "@types/jest": "29.2.5", 31 | "@types/node": "20.10.5", 32 | "@typescript-eslint/eslint-plugin": "6.16.0", 33 | "@typescript-eslint/parser": "6.16.0", 34 | "cspell": "8.2.3", 35 | "eslint": "8.56.0", 36 | "eslint-config-prettier": "9.1.0", 37 | "eslint-plugin-prettier": "5.1.2", 38 | "husky": "8.0.3", 39 | "is-ci": "3.0.1", 40 | "jest": "29.7.0", 41 | "lint-staged": "15.2.0", 42 | "lodash": "^4.17.21", 43 | "prettier": "3.1.1", 44 | "reflect-metadata": "0.1.13", 45 | "rxjs": "^7.1.0", 46 | "ts-jest": "29.1.1", 47 | "ts-node": "10.9.2", 48 | "tsconfig-paths": "^3.5.0", 49 | "typescript": "5.3.3" 50 | }, 51 | "dependencies": { 52 | "@types/cassandra-driver": "3.6.0", 53 | "deepmerge": "^4.3.1", 54 | "express-cassandra": "2.3.2", 55 | "snakecase-keys": "^6.0.0" 56 | }, 57 | "scripts": { 58 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 59 | "build": "tsc --project tsconfig.prod.json && cp package.json dist/package.json", 60 | "deploy": "npm run build && npm publish dist", 61 | "format": "prettier --write \"**/*.ts\"", 62 | "check": "prettier --check \"**/*.ts\"", 63 | "test": "jest --passWithNoTests --testTimeout=450000 ", 64 | "cspell": "cspell --no-must-find-files libs/**/*.{ts,js}", 65 | "prepare": "is-ci || husky install", 66 | "lint": "eslint \"libs/**/*.ts\" --fix --max-warnings 0" 67 | }, 68 | "jest": { 69 | "moduleFileExtensions": [ 70 | "js", 71 | "json", 72 | "ts" 73 | ], 74 | "rootDir": ".", 75 | "testRegex": ".*\\.spec\\.ts$", 76 | "transform": { 77 | "^.+\\.(t|j)s$": "ts-jest" 78 | }, 79 | "collectCoverageFrom": [ 80 | "**/*.(t|j)s" 81 | ], 82 | "coverageDirectory": "../coverage", 83 | "testEnvironment": "node" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "prConcurrentLimit": 5, 5 | "assignees": ["hodfords_dung_senior_dev"], 6 | "labels": ["renovate"] 7 | } 8 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScyllaModule } from 'libs'; 3 | import { CatModule } from './cats'; 4 | import { scyllaOptions } from './config'; 5 | 6 | @Module({ 7 | imports: [ScyllaModule.forRoot(scyllaOptions), CatModule] 8 | }) 9 | export class AppModule {} 10 | -------------------------------------------------------------------------------- /src/cats/cat.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, Delete, Patch, Logger } from '@nestjs/common'; 2 | import { CatsService } from './cat.service'; 3 | import { CreateCatDto } from './dto/create-cat.dto'; 4 | import { ParseUuidPipe } from './pipes/parse-uuid.pipe'; 5 | import { CatRepository } from './cat.repository'; 6 | import { InjectRepository } from 'libs'; 7 | import { isUuid, timeuuid } from 'libs/utils/db.utils'; 8 | 9 | @Controller('cats') 10 | export class CatsController { 11 | constructor( 12 | private readonly catsService: CatsService, 13 | @InjectRepository(CatRepository) 14 | private readonly catRepository: CatRepository 15 | ) {} 16 | 17 | @Post() 18 | async create(@Body() createCatDto: CreateCatDto | CreateCatDto[]) { 19 | if (Array.isArray(createCatDto)) { 20 | return this.catRepository.saveMultiple(createCatDto); 21 | } 22 | 23 | const cat = await this.catsService.create(createCatDto); 24 | 25 | return cat; 26 | } 27 | 28 | @Get() 29 | async findAll() { 30 | return this.catsService.findAll(); 31 | } 32 | 33 | @Get(':id') 34 | findOne(@Param('id', new ParseUuidPipe()) id) { 35 | return this.catsService.findById(id); 36 | } 37 | 38 | @Patch(':id') 39 | update(@Param('id', new ParseUuidPipe()) id, @Body() updateBody) { 40 | Logger.log(`Is uuid: ${isUuid(id)}`, CatsController.name); 41 | 42 | if (typeof updateBody.time_id === 'string') { 43 | updateBody.time_id = timeuuid(updateBody.time_id); 44 | } 45 | 46 | const { ...restBody } = updateBody; 47 | 48 | return this.catRepository.update({ id }, restBody); 49 | } 50 | 51 | @Post('batch') 52 | async doBatch() { 53 | await this.catsService.batch(); 54 | return; 55 | } 56 | 57 | @Delete() 58 | delete() { 59 | return this.catRepository.truncate(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/cats/cat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScyllaModule } from 'libs'; 3 | import { CatsController } from './cat.controller'; 4 | import { CatRepository } from './cat.repository'; 5 | import { CatsService } from './cat.service'; 6 | import { CatEntity } from './entities/cat.entity'; 7 | 8 | @Module({ 9 | imports: [ScyllaModule.forFeature([CatEntity, CatRepository])], 10 | controllers: [CatsController], 11 | providers: [CatsService] 12 | }) 13 | export class CatModule {} 14 | -------------------------------------------------------------------------------- /src/cats/cat.repository.ts: -------------------------------------------------------------------------------- 1 | import { CatEntity } from './entities/cat.entity'; 2 | import { from } from 'rxjs'; 3 | import { mergeMap, toArray } from 'rxjs/operators'; 4 | import { Repository } from 'libs/repositories/repository'; 5 | import { EntityRepository } from 'libs'; 6 | 7 | @EntityRepository(CatEntity) 8 | export class CatRepository extends Repository { 9 | saveMultiple(cats: any[]) { 10 | return from(cats).pipe( 11 | mergeMap((cat) => this.save(cat)), 12 | toArray() 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cats/cat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { InjectRepository } from 'libs'; 3 | import { CatRepository } from './cat.repository'; 4 | import { CreateCatDto } from './dto/create-cat.dto'; 5 | import { CatEntity } from './entities/cat.entity'; 6 | 7 | @Injectable() 8 | export class CatsService { 9 | private readonly logger = new Logger(CatsService.name); 10 | 11 | constructor( 12 | @InjectRepository(CatRepository) 13 | private readonly catRepository: CatRepository 14 | ) {} 15 | 16 | async create(createCatDto: CreateCatDto): Promise { 17 | return this.catRepository.save(createCatDto); 18 | } 19 | 20 | findAll() { 21 | return this.catRepository.findAndCount({}); 22 | } 23 | 24 | findById(id): Promise { 25 | return this.catRepository.findOne({ id }); 26 | } 27 | 28 | async batch() { 29 | const queries = []; 30 | const catBuilder = this.catRepository.getReturnQueryBuilder(); 31 | 32 | queries.push(catBuilder.save({ name: 'batch cat' })); 33 | 34 | await this.catRepository.doBatch(queries); 35 | 36 | this.logger.log(`Running batch`, CatsService.name); 37 | this.logger.log(`Batch queries ${JSON.stringify(queries)}`, CatsService.name); 38 | 39 | return; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/cats/dto/create-cat.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCatDto { 2 | readonly name: string; 3 | readonly age: number; 4 | readonly breed: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/cats/entities/cat.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, GeneratedUUidColumn, UpdateDateColumn } from 'libs'; 2 | 3 | @Entity({ 4 | tableName: 'cat', 5 | key: ['id'] 6 | }) 7 | export class CatEntity { 8 | @GeneratedUUidColumn() 9 | id: string; 10 | 11 | @CreateDateColumn() 12 | createdAt: number; 13 | 14 | @UpdateDateColumn() 15 | updatedAt: number; 16 | 17 | @Column({ 18 | type: 'varchar' 19 | }) 20 | name: string; 21 | 22 | @Column({ 23 | type: 'int' 24 | }) 25 | age: number; 26 | 27 | @Column({ 28 | type: 'varchar' 29 | }) 30 | breed: string; 31 | } 32 | -------------------------------------------------------------------------------- /src/cats/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cat.module'; 2 | -------------------------------------------------------------------------------- /src/cats/pipes/parse-uuid.pipe.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; 2 | import { isUuid, types, uuid } from 'libs'; 3 | 4 | @Injectable() 5 | export class ParseUuidPipe implements PipeTransform { 6 | transform(value: any): types.Uuid { 7 | if (isUuid(value)) { 8 | return value; 9 | } 10 | 11 | if (!(typeof value === 'string')) { 12 | return value; 13 | } 14 | 15 | try { 16 | value = uuid(value); 17 | } catch (error) { 18 | throw new BadRequestException(`${error.message}`); 19 | } 20 | return value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ScyllaModuleOptions } from 'libs'; 2 | 3 | export const scyllaOptions: ScyllaModuleOptions = { 4 | clientOptions: { 5 | contactPoints: ['localhost'], 6 | keyspace: 'test', 7 | protocolOptions: { 8 | port: 9043 9 | }, 10 | queryOptions: { 11 | consistency: 1 12 | } 13 | // authProvider: new auth.PlainTextAuthProvider('scylla', 'scylla') 14 | }, 15 | ormOptions: { 16 | createKeyspace: true, 17 | defaultReplicationStrategy: { 18 | class: 'SimpleStrategy', 19 | // eslint-disable-next-line @typescript-eslint/naming-convention 20 | replication_factor: 1 21 | }, 22 | migration: 'safe' 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule, { bodyParser: true }); 6 | await app.listen(3000); 7 | } 8 | bootstrap() 9 | .then(() => console.log('Init app success')) 10 | .catch(console.error); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "outDir": "dist", 15 | "incremental": true 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "outDir": "dist", 14 | "baseUrl": "./", 15 | "incremental": true 16 | }, 17 | "include": [ 18 | "libs", 19 | ], 20 | "exclude": [ 21 | "tests", 22 | "src", 23 | "dist", 24 | "node_modules" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------