├── .nvmrc ├── index.ts ├── lib ├── index.ts ├── public_api.ts ├── table-storage │ ├── azure-table.constant.ts │ ├── index.ts │ ├── azure-table.mapper.spec.ts │ ├── azure-table.interface.ts │ ├── azure-table.providers.ts │ ├── azure-table.service.ts │ ├── azure-table.mapper.ts │ ├── azure-table.decorators.spec.ts │ ├── azure-table.module.ts │ ├── azure-table.decorators.ts │ └── azure-table.repository.ts └── cosmos-db │ ├── index.ts │ ├── cosmos-db.constants.ts │ ├── cosmos-db.module.ts │ ├── cosmos-db.decorators.spec.ts │ ├── cosmos-db.interface.ts │ ├── cosmos-db.providers.ts │ ├── cosmos-db.utils.spec.ts │ ├── cosmos-db.utils.ts │ ├── cosmos-db-core.module.ts │ └── cosmos-db.decorators.ts ├── sample ├── table-storage │ ├── .env.sample │ ├── .prettierrc │ ├── src │ │ ├── event │ │ │ ├── event.dto.ts │ │ │ ├── event.entity.ts │ │ │ ├── event.module.ts │ │ │ ├── event.service.ts │ │ │ └── event.controller.ts │ │ ├── app.module.ts │ │ └── main.ts │ ├── tsconfig.build.json │ ├── nest-cli.json │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── package.json │ └── README.md ├── cosmos-db │ ├── .prettierrc │ ├── tsconfig.build.json │ ├── .env.sample │ ├── src │ │ ├── event │ │ │ ├── event.dto.ts │ │ │ ├── event.entity.ts │ │ │ ├── event.module.ts │ │ │ ├── event.controller.ts │ │ │ └── event.service.ts │ │ ├── app.module.ts │ │ └── main.ts │ ├── nest-cli.json │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── package.json │ └── README.md └── index.js ├── tests └── table-storage │ ├── services │ ├── entities │ │ ├── index.ts │ │ └── event.entity.ts │ ├── index.ts │ ├── event.service.ts │ └── azurite-table.ts │ ├── modules │ ├── table-storage-options.module.ts │ ├── azurite.module.ts │ ├── table-storage-async-options.module.ts │ ├── table-storage-async-options-class.module.ts │ └── table-storage-async-options-existing.module.ts │ └── e2e │ ├── table-storage-options-module.spec.ts │ ├── table-storage-async-options-existing-module.spec.ts │ ├── table-storage-async-options-module.spec.ts │ ├── table-storage-async-options-class-module.spec.ts │ ├── azurite-integration.spec.ts │ ├── azurite-integration-async.spec.ts │ ├── azurite-integration-class.spec.ts │ └── azurite-integration-existing.spec.ts ├── .prettierrc ├── .npmignore ├── jest.config.js ├── renovate.json ├── .gitignore ├── env.sample ├── tsconfig.json ├── .commitlintrc.json ├── LICENSE ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── eslint.config.mjs ├── .circleci └── config.yml ├── package.json ├── CONTRINBUTING.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.13 2 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public_api'; 2 | -------------------------------------------------------------------------------- /sample/table-storage/.env.sample: -------------------------------------------------------------------------------- 1 | AZURE_STORAGE_CONNECTION_STRING= 2 | -------------------------------------------------------------------------------- /tests/table-storage/services/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event.entity'; 2 | -------------------------------------------------------------------------------- /lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './table-storage'; 2 | export * from './cosmos-db'; 3 | -------------------------------------------------------------------------------- /sample/cosmos-db/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /sample/table-storage/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /sample/table-storage/src/event/event.dto.ts: -------------------------------------------------------------------------------- 1 | export class EventDTO { 2 | name: string; 3 | type: string; 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "arrowParens": "avoid", 5 | "printWidth": 120 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | index.ts 4 | package-lock.json 5 | tsconfig.json 6 | .prettierrc 7 | .DS_Store 8 | 9 | # config 10 | .env -------------------------------------------------------------------------------- /tests/table-storage/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities'; 2 | 3 | export * from './azurite-table'; 4 | export * from './event.service'; 5 | -------------------------------------------------------------------------------- /sample/cosmos-db/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /sample/table-storage/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /sample/cosmos-db/.env.sample: -------------------------------------------------------------------------------- 1 | AZURE_COSMOS_DB_NAME="XXX" 2 | AZURE_COSMOS_DB_ENDPOINT="https://YYY.documents.azure.com:443" 3 | AZURE_COSMOS_DB_KEY="ZZZ" 4 | -------------------------------------------------------------------------------- /tests/table-storage/services/entities/event.entity.ts: -------------------------------------------------------------------------------- 1 | export class Event { 2 | name: string; 3 | partitionKey: string; 4 | rowKey: string; 5 | } 6 | -------------------------------------------------------------------------------- /sample/table-storage/src/event/event.entity.ts: -------------------------------------------------------------------------------- 1 | export class Event { 2 | partitionKey: string; 3 | rowKey: string; 4 | name: string; 5 | type: string; 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/sample/', '/dist/'], 5 | }; 6 | -------------------------------------------------------------------------------- /sample/cosmos-db/src/event/event.dto.ts: -------------------------------------------------------------------------------- 1 | export class EventDTO { 2 | id?: string; 3 | name: string; 4 | type: { 5 | label: string; 6 | } 7 | createdAt: Date; 8 | } 9 | -------------------------------------------------------------------------------- /sample/cosmos-db/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventModule } from './event/event.module'; 3 | 4 | @Module({ 5 | imports: [EventModule], 6 | }) 7 | export class AppModule {} 8 | -------------------------------------------------------------------------------- /sample/table-storage/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventModule } from './event/event.module'; 3 | 4 | @Module({ 5 | imports: [EventModule], 6 | }) 7 | export class AppModule {} 8 | -------------------------------------------------------------------------------- /sample/cosmos-db/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sample/table-storage/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "semanticCommits": true, 6 | "packageRules": [{ 7 | "depTypeList": ["devDependencies"], 8 | "automerge": true 9 | }], 10 | "rangeStrategy": "replace" 11 | } 12 | -------------------------------------------------------------------------------- /sample/cosmos-db/test/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 | -------------------------------------------------------------------------------- /sample/table-storage/test/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | .DS_Store 12 | 13 | # tests 14 | /test 15 | /coverage 16 | /.nyc_output 17 | 18 | # source 19 | dist 20 | 21 | # config 22 | .env -------------------------------------------------------------------------------- /lib/table-storage/azure-table.constant.ts: -------------------------------------------------------------------------------- 1 | export const AZURE_TABLE_STORAGE_MODULE_OPTIONS = 'AZURE_TABLE_STORAGE_MODULE_OPTIONS'; 2 | export const AZURE_TABLE_STORAGE_FEATURE_OPTIONS = 'AZURE_TABLE_STORAGE_FEATURE_OPTIONS'; 3 | 4 | export const AZURE_TABLE_STORAGE_NAME = 'AZURE_TABLE_STORAGE_NAME'; 5 | -------------------------------------------------------------------------------- /lib/cosmos-db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cosmos-db.constants'; 2 | export * from './cosmos-db.decorators'; 3 | export * from './cosmos-db.interface'; 4 | export * from './cosmos-db.module'; 5 | export * from './cosmos-db.providers'; 6 | export { getConnectionToken, getModelToken } from './cosmos-db.utils'; 7 | -------------------------------------------------------------------------------- /lib/table-storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './azure-table.constant'; 2 | export * from './azure-table.service'; 3 | export * from './azure-table.module'; 4 | export * from './azure-table.repository'; 5 | export * from './azure-table.interface'; 6 | export * from './azure-table.decorators'; 7 | export * from './azure-table.providers'; 8 | -------------------------------------------------------------------------------- /sample/table-storage/src/main.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') require('dotenv').config(); 2 | 3 | import { NestFactory } from '@nestjs/core'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | } 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.constants.ts: -------------------------------------------------------------------------------- 1 | export const COSMOS_DB_MODULE_OPTIONS = 'COSMOS_DB_MODULE_OPTIONS'; 2 | 3 | /** @deprecated the `COSMOS_DB_CONNECTION_NAME` token is not used anymore and will be removed in the next version */ 4 | export const COSMOS_DB_CONNECTION_NAME = 'COSMOS_DB_CONNECTION_NAME'; 5 | export const DEFAULT_DB_CONNECTION = 'COSMOS_DB_DEFAULT_CONNECTION'; 6 | -------------------------------------------------------------------------------- /sample/cosmos-db/src/main.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config(); 3 | } 4 | 5 | import { NestFactory } from '@nestjs/core'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule, { 10 | logger: ['verbose'], 11 | }); 12 | await app.listen(3000); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | ### For Table Storage 2 | 3 | # See: http://bit.ly/azure-storage-sas-key 4 | AZURE_STORAGE_ACCESS_KEY= 5 | # See: http://bit.ly/azure-storage-account 6 | AZURE_STORAGE_ACCOUNT= 7 | # See: http://bit.ly/azure-storage-cs 8 | AZURE_STORAGE_CONNECTION_STRING= 9 | 10 | ### For Cosmos DB 11 | 12 | # See: https://bit.ly/azure-cosmosdb-access 13 | AZURE_COSMOS_DB_NAME= 14 | AZURE_COSMOS_DB_ENDPOINT= 15 | AZURE_COSMOS_DB_KEY= 16 | -------------------------------------------------------------------------------- /tests/table-storage/modules/table-storage-options.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AzureTableStorageModule } from '../../../lib'; 3 | 4 | @Module({ 5 | imports: [ 6 | AzureTableStorageModule.forRoot({ 7 | accountName: 'account-name', 8 | sasKey: 'sas-key', 9 | connectionString: 'connection-string', 10 | allowInsecureConnection: false, 11 | }), 12 | ], 13 | }) 14 | export class TableStorageModule {} 15 | -------------------------------------------------------------------------------- /tests/table-storage/modules/azurite.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AzureTableStorageModule } from '../../../lib'; 3 | import { EventService } from '../services'; 4 | 5 | @Module({ 6 | imports: [ 7 | AzureTableStorageModule.forFeature(Event, { 8 | table: 'events', 9 | createTableIfNotExists: true, 10 | }), 11 | ], 12 | providers: [EventService], 13 | exports: [EventService], 14 | }) 15 | export class AzuriteModule {} 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "skipLibCheck": true 14 | }, 15 | "include": ["lib/**/*"], 16 | "exclude": ["node_modules", "tests", "samples"] 17 | } -------------------------------------------------------------------------------- /sample/cosmos-db/src/event/event.entity.ts: -------------------------------------------------------------------------------- 1 | import { CosmosDateTime, CosmosPartitionKey } from '@nestjs/azure-database'; 2 | import { PartitionKeyDefinitionVersion, PartitionKeyKind } from '@azure/cosmos'; 3 | 4 | @CosmosPartitionKey({ 5 | paths: ['/name', '/type/label'], 6 | version: PartitionKeyDefinitionVersion.V2, 7 | kind: PartitionKeyKind.MultiHash 8 | }) 9 | export class Event { 10 | id?: string; 11 | name: string; 12 | type: { 13 | label: string; 14 | } 15 | @CosmosDateTime() createdAt: Date; 16 | } 17 | -------------------------------------------------------------------------------- /sample/table-storage/src/event/event.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventController } from './event.controller'; 3 | import { AzureTableStorageModule } from '@nestjs/azure-database'; 4 | import { EventService } from './event.service'; 5 | 6 | @Module({ 7 | imports: [ 8 | AzureTableStorageModule.forFeature(Event, { 9 | createTableIfNotExists: true, 10 | }), 11 | ], 12 | providers: [EventService], 13 | controllers: [EventController], 14 | }) 15 | export class EventModule {} 16 | -------------------------------------------------------------------------------- /tests/table-storage/modules/table-storage-async-options.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AzureTableStorageModule } from '../../../lib'; 3 | 4 | @Module({ 5 | imports: [ 6 | AzureTableStorageModule.forRootAsync({ 7 | useFactory: async () => ({ 8 | accountName: 'account-name', 9 | sasKey: 'sas-key', 10 | connectionString: 'connection-string', 11 | allowInsecureConnection: true, 12 | }), 13 | }), 14 | ], 15 | }) 16 | export class TableStorageAsyncModule {} 17 | -------------------------------------------------------------------------------- /sample/table-storage/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /sample/cosmos-db/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /sample/cosmos-db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/table-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config(); 3 | } 4 | 5 | (async () => { 6 | const cosmos = require('@azure/cosmos'); 7 | const endpoint = process.env.AZURE_COSMOS_DB_ENDPOINT; 8 | const key = process.env.AZURE_COSMOS_DB_KEY; 9 | const dbDefName = process.env.AZURE_COSMOS_DB_NAME; 10 | const client = new cosmos.CosmosClient({ endpoint, key }); 11 | 12 | const dbResponse = await client.databases.createIfNotExists({ 13 | id: dbDefName, 14 | }); 15 | 16 | const { resource } = await client.database('ToDoList').container('events').item('1689756044201', 'type').read(); 17 | console.log({ resource }); 18 | })(); 19 | -------------------------------------------------------------------------------- /tests/table-storage/modules/table-storage-async-options-class.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AzureTableStorageModule, AzureTableStorageOptionsFactory } from '../../../lib'; 3 | 4 | class ConfigService implements AzureTableStorageOptionsFactory { 5 | createAzureTableStorageOptions() { 6 | return { 7 | accountName: 'account-name', 8 | sasKey: 'sas-key', 9 | connectionString: 'connection-string', 10 | allowInsecureConnection: true, 11 | }; 12 | } 13 | } 14 | 15 | @Module({ 16 | imports: [ 17 | AzureTableStorageModule.forRootAsync({ 18 | useClass: ConfigService, 19 | }), 20 | ], 21 | }) 22 | export class TableStorageAsyncClassModule {} 23 | -------------------------------------------------------------------------------- /sample/cosmos-db/src/event/event.module.ts: -------------------------------------------------------------------------------- 1 | import { AzureCosmosDbModule } from '@nestjs/azure-database'; 2 | import { Module } from '@nestjs/common'; 3 | import { Event } from './event.entity'; 4 | import { EventService } from './event.service'; 5 | import { EventController } from './event.controller'; 6 | 7 | @Module({ 8 | imports: [ 9 | AzureCosmosDbModule.forRoot({ 10 | dbName: process.env.AZURE_COSMOS_DB_NAME, 11 | endpoint: process.env.AZURE_COSMOS_DB_ENDPOINT, 12 | key: process.env.AZURE_COSMOS_DB_KEY, 13 | retryAttempts: 1, 14 | }), 15 | AzureCosmosDbModule.forFeature([{ dto: Event }]), 16 | ], 17 | providers: [EventService], 18 | controllers: [EventController], 19 | }) 20 | export class EventModule {} 21 | -------------------------------------------------------------------------------- /sample/cosmos-db/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /sample/table-storage/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /sample/cosmos-db/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /sample/table-storage/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tests/table-storage/modules/table-storage-async-options-existing.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AzureTableStorageModule, AzureTableStorageOptionsFactory } from '../../../lib'; 3 | 4 | class ConfigService implements AzureTableStorageOptionsFactory { 5 | createAzureTableStorageOptions() { 6 | return { 7 | accountName: 'account-name', 8 | sasKey: 'sas-key', 9 | connectionString: 'connection-string', 10 | }; 11 | } 12 | } 13 | 14 | @Module({ 15 | providers: [ConfigService], 16 | exports: [ConfigService], 17 | }) 18 | class ConfigModule {} 19 | 20 | @Module({ 21 | imports: [ 22 | AzureTableStorageModule.forRootAsync({ 23 | imports: [ConfigModule], 24 | useExisting: ConfigService, 25 | }), 26 | ], 27 | }) 28 | export class TableStorageAsyncExistingModule {} 29 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/table-storage-options-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AZURE_TABLE_STORAGE_MODULE_OPTIONS } from '../../../lib'; 3 | import { TableStorageModule } from '../modules/table-storage-options.module'; 4 | 5 | describe('Table Storage (sync class)', () => { 6 | let moduleRef: TestingModule; 7 | 8 | const originalEnv = process.env; 9 | afterEach(() => { 10 | process.env = originalEnv; 11 | }); 12 | 13 | beforeEach(async () => { 14 | process.env.AZURE_STORAGE_CONNECTION_STRING = 'abc'; 15 | moduleRef = await Test.createTestingModule({ 16 | imports: [TableStorageModule], 17 | }).compile(); 18 | }); 19 | 20 | it('should return options provide by useFactory', () => { 21 | const options = moduleRef.get(AZURE_TABLE_STORAGE_MODULE_OPTIONS); 22 | 23 | expect(options).toEqual({ 24 | accountName: 'account-name', 25 | allowInsecureConnection: false, 26 | sasKey: 'sas-key', 27 | connectionString: 'connection-string', 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/table-storage-async-options-existing-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AZURE_TABLE_STORAGE_MODULE_OPTIONS } from '../../../lib'; 3 | import { TableStorageAsyncExistingModule } from '../modules/table-storage-async-options-existing.module'; 4 | 5 | describe('Table Storage (async existing class)', () => { 6 | let moduleRef: TestingModule; 7 | 8 | const originalEnv = process.env; 9 | afterEach(() => { 10 | process.env = originalEnv; 11 | }); 12 | 13 | beforeEach(async () => { 14 | process.env.AZURE_STORAGE_CONNECTION_STRING = 'abc'; 15 | moduleRef = await Test.createTestingModule({ 16 | imports: [TableStorageAsyncExistingModule], 17 | }).compile(); 18 | }); 19 | 20 | it('should return options provide by useFactory', () => { 21 | const options = moduleRef.get(AZURE_TABLE_STORAGE_MODULE_OPTIONS); 22 | 23 | expect(options).toEqual({ 24 | accountName: 'account-name', 25 | sasKey: 'sas-key', 26 | connectionString: 'connection-string', 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/table-storage-async-options-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AZURE_TABLE_STORAGE_MODULE_OPTIONS } from '../../../lib'; 3 | import { TableStorageAsyncModule } from '../modules/table-storage-async-options.module'; 4 | 5 | describe('Table Storage (async class)', () => { 6 | let moduleRef: TestingModule; 7 | 8 | const originalEnv = process.env; 9 | afterEach(() => { 10 | process.env = originalEnv; 11 | }); 12 | 13 | beforeEach(async () => { 14 | process.env.AZURE_STORAGE_CONNECTION_STRING = 'abc'; 15 | moduleRef = await Test.createTestingModule({ 16 | imports: [TableStorageAsyncModule], 17 | }).compile(); 18 | }); 19 | 20 | it('should return options provide by useFactory', () => { 21 | const options = moduleRef.get(AZURE_TABLE_STORAGE_MODULE_OPTIONS); 22 | 23 | expect(options).toEqual({ 24 | accountName: 'account-name', 25 | allowInsecureConnection: true, 26 | sasKey: 'sas-key', 27 | connectionString: 'connection-string', 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/table-storage/services/event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository, Repository } from '../../../lib'; 3 | import { Event } from './entities'; 4 | 5 | @Injectable() 6 | export class EventService { 7 | constructor( 8 | @InjectRepository(Event) 9 | private readonly eventRepository: Repository, 10 | ) {} 11 | 12 | async find(partitionKey: string, rowKey: string): Promise { 13 | return await this.eventRepository.find(partitionKey, rowKey); 14 | } 15 | 16 | async findAll(): Promise { 17 | return await this.eventRepository.findAll(); 18 | } 19 | 20 | async create(event: Event): Promise { 21 | return await this.eventRepository.create(event); 22 | } 23 | 24 | async update(partitionKey: string, rowKey: string, event: Event): Promise { 25 | return await this.eventRepository.update(partitionKey, rowKey, event); 26 | } 27 | 28 | async delete(partitionKey: string, rowKey: string): Promise { 29 | await this.eventRepository.delete(partitionKey, rowKey); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/table-storage-async-options-class-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AZURE_TABLE_STORAGE_MODULE_OPTIONS } from '../../../lib'; 3 | import { TableStorageAsyncClassModule } from '../modules/table-storage-async-options-class.module'; 4 | 5 | describe('Table Storage (async class)', () => { 6 | let moduleRef: TestingModule; 7 | 8 | const originalEnv = process.env; 9 | afterEach(() => { 10 | process.env = originalEnv; 11 | }); 12 | 13 | beforeEach(async () => { 14 | process.env.AZURE_STORAGE_CONNECTION_STRING = 'abc'; 15 | moduleRef = await Test.createTestingModule({ 16 | imports: [TableStorageAsyncClassModule], 17 | }).compile(); 18 | }); 19 | 20 | it('should return options provide by class', () => { 21 | const options = moduleRef.get(AZURE_TABLE_STORAGE_MODULE_OPTIONS); 22 | 23 | expect(options).toEqual({ 24 | accountName: 'account-name', 25 | allowInsecureConnection: true, 26 | sasKey: 'sas-key', 27 | connectionString: 'connection-string', 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /sample/table-storage/src/event/event.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectRepository, Repository } from '@nestjs/azure-database'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { Event } from './event.entity'; 4 | 5 | @Injectable() 6 | export class EventService { 7 | constructor( 8 | @InjectRepository(Event) 9 | private readonly eventRepository: Repository, 10 | ) {} 11 | 12 | async find(partitionKey: string, rowKey: string): Promise { 13 | return await this.eventRepository.find(partitionKey, rowKey); 14 | } 15 | 16 | async findAll(): Promise { 17 | return await this.eventRepository.findAll(); 18 | } 19 | 20 | async create(event: Event): Promise { 21 | return await this.eventRepository.create(event); 22 | } 23 | 24 | async update( 25 | partitionKey: string, 26 | rowKey: string, 27 | event: Event, 28 | ): Promise { 29 | return await this.eventRepository.update(partitionKey, rowKey, event); 30 | } 31 | 32 | async delete(partitionKey: string, rowKey: string): Promise { 33 | await this.eventRepository.delete(partitionKey, rowKey); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { AzureCosmosDbCoreModule } from './cosmos-db-core.module'; 3 | import { AzureCosmosDbModuleAsyncOptions, AzureCosmosDbOptions } from './cosmos-db.interface'; 4 | import { createAzureCosmosDbProviders } from './cosmos-db.providers'; 5 | 6 | @Module({}) 7 | export class AzureCosmosDbModule { 8 | static forRoot(options: AzureCosmosDbOptions): DynamicModule { 9 | return { 10 | module: AzureCosmosDbModule, 11 | imports: [AzureCosmosDbCoreModule.forRoot(options)], 12 | }; 13 | } 14 | 15 | static forRootAsync(options: AzureCosmosDbModuleAsyncOptions): DynamicModule { 16 | return { 17 | module: AzureCosmosDbModule, 18 | imports: [AzureCosmosDbCoreModule.forRootAsync(options)], 19 | }; 20 | } 21 | 22 | static forFeature(models: { dto: any; collection?: string }[] = [], connectionName?: string): DynamicModule { 23 | const providers = createAzureCosmosDbProviders(connectionName, models); 24 | return { 25 | module: AzureCosmosDbModule, 26 | providers, 27 | exports: providers, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { AZURE_TABLE_ENTITY } from './azure-table.decorators'; 2 | import { AzureEntityMapper, PartitionRowKeyValues } from './azure-table.mapper'; 3 | 4 | class MockEntity { 5 | foo: string; 6 | } 7 | 8 | describe('AzureEntityMapper', () => { 9 | describe('createEntity()', () => { 10 | it('should always return new entity instance', () => { 11 | const entityDescriptorMock: PartitionRowKeyValues & any = { 12 | RowKey: { _: '', $: '' }, 13 | PartitionKey: { _: '', $: '' }, 14 | foo: { _: '', $: '' }, 15 | }; 16 | 17 | Reflect.defineMetadata(AZURE_TABLE_ENTITY, entityDescriptorMock, MockEntity); 18 | Reflect.defineMetadata(AZURE_TABLE_ENTITY, entityDescriptorMock, MockEntity); 19 | 20 | const test = new MockEntity(); 21 | const test2 = new MockEntity(); 22 | 23 | test.foo = 'foo'; 24 | 25 | const entity = AzureEntityMapper.createEntity(test); 26 | const entity2 = AzureEntityMapper.createEntity(test2); 27 | 28 | expect(entity).not.toBe(entity2); 29 | expect(entity.foo).not.toEqual(entity2.foo); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata, Type } from '@nestjs/common'; 2 | import { AzureTableStorageRepository } from './azure-table.repository'; 3 | import { TableServiceClientOptions } from '@azure/data-tables'; 4 | 5 | export interface AzureTableStorageOptions extends TableServiceClientOptions { 6 | accountName?: string; 7 | sasKey?: string; 8 | connectionString?: string; 9 | } 10 | 11 | export interface AzureTableStorageOptionsFactory { 12 | createAzureTableStorageOptions(): Promise | AzureTableStorageOptions; 13 | } 14 | 15 | export interface AzureTableStorageModuleAsyncOptions extends Pick { 16 | useExisting?: Type; 17 | useClass?: Type; 18 | useFactory?: (...args: any[]) => Promise | AzureTableStorageOptions; 19 | inject?: any[]; 20 | } 21 | 22 | export interface AzureTableStorageFeatureOptions { 23 | table?: string; 24 | createTableIfNotExists?: boolean; 25 | } 26 | 27 | export type ValueType = ((e: any) => string) | string; 28 | 29 | export type Repository = AzureTableStorageRepository; 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | 13 | - [ ] Bugfix 14 | - [ ] Feature 15 | - [ ] Code style update (formatting, local variables) 16 | - [ ] Refactoring (no functional changes, no api changes) 17 | - [ ] Build related changes 18 | - [ ] CI related changes 19 | - [ ] Other... Please describe: 20 | 21 | ## What is the current behavior? 22 | 23 | 24 | Issue Number: N/A 25 | 26 | 27 | ## What is the new behavior? 28 | 29 | 30 | ## Does this PR introduce a breaking change? 31 | - [ ] Yes 32 | - [ ] No 33 | 34 | 35 | 36 | 37 | ## Other information 38 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | import { AZURE_TABLE_STORAGE_FEATURE_OPTIONS, AZURE_TABLE_STORAGE_NAME } from './azure-table.constant'; 3 | import { AzureTableStorageFeatureOptions } from './azure-table.interface'; 4 | import { AzureTableStorageRepository } from './azure-table.repository'; 5 | import { AzureTableStorageService } from './azure-table.service'; 6 | 7 | type EntityFn = /* Function */ { 8 | name: string; 9 | }; 10 | 11 | export function createRepositoryProviders(entity: EntityFn): Provider[] { 12 | return [getRepositoryProvider(entity)]; 13 | } 14 | 15 | export function getRepositoryProvider(entity: EntityFn): Provider { 16 | const provide = getRepositoryToken(entity); 17 | const o = { 18 | provide, 19 | useFactory: (service: AzureTableStorageService, tableName: string, options: AzureTableStorageFeatureOptions) => { 20 | return new AzureTableStorageRepository(service, tableName, options); 21 | }, 22 | inject: [AzureTableStorageService, AZURE_TABLE_STORAGE_NAME, AZURE_TABLE_STORAGE_FEATURE_OPTIONS], 23 | }; 24 | return o; 25 | } 26 | 27 | export function getRepositoryToken(entity: EntityFn) { 28 | return `${entity.name}AzureTableStorageRepository`; 29 | } 30 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['tests/**'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | ecmaVersion: 5, 21 | sourceType: 'module', 22 | parserOptions: { 23 | projectService: true, 24 | tsconfigRootDir: import.meta.dirname, 25 | }, 26 | }, 27 | }, 28 | { 29 | rules: { 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-unsafe-assignment': 'off', 32 | '@typescript-eslint/no-unsafe-call': 'off', 33 | '@typescript-eslint/no-unsafe-member-access': 'off', 34 | '@typescript-eslint/no-unsafe-function-type': 'off', 35 | '@typescript-eslint/no-unsafe-argument': 'off', 36 | '@typescript-eslint/no-unsafe-return': 'off', 37 | 'no-prototype-builtins': 'off', 38 | '@typescript-eslint/no-unused-vars': 'warn', 39 | '@typescript-eslint/no-redundant-type-constituents': 'warn', 40 | }, 41 | }, 42 | ); -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.decorators.spec.ts: -------------------------------------------------------------------------------- 1 | import { AZURE_COSMOS_DB_ENTITY, CosmosPartitionKey } from './cosmos-db.decorators'; 2 | import { PartitionKeyDefinitionVersion, PartitionKeyKind } from '@azure/cosmos'; 3 | 4 | describe('Azure CosmosDB Decorators', () => { 5 | beforeEach(() => { 6 | // tslint:disable-next-line: no-empty 7 | function MockEntity() {} 8 | }); 9 | 10 | describe('@CosmosPartitionKey()', () => { 11 | it('should add a PartitionKey ', () => { 12 | @CosmosPartitionKey('value') 13 | class MockClass {} 14 | 15 | const metadata = Reflect.getMetadata(AZURE_COSMOS_DB_ENTITY, MockClass); 16 | expect(metadata).toStrictEqual({ 17 | PartitionKey: 'value', 18 | }); 19 | }); 20 | 21 | it('should add a Hierarchical PartitionKey ', () => { 22 | @CosmosPartitionKey({ 23 | paths: ['/name', '/address/zip'], 24 | version: PartitionKeyDefinitionVersion.V2, 25 | kind: PartitionKeyKind.MultiHash, 26 | }) 27 | class MockClass {} 28 | 29 | const metadata = Reflect.getMetadata(AZURE_COSMOS_DB_ENTITY, MockClass); 30 | expect(metadata).toStrictEqual({ 31 | PartitionKey: { 32 | paths: ['/name', '/address/zip'], 33 | version: PartitionKeyDefinitionVersion.V2, 34 | kind: PartitionKeyKind.MultiHash, 35 | }, 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: npm install --ignore-scripts 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: npm run build 15 | 16 | jobs: 17 | build: 18 | working_directory: ~/nest-azure-database 19 | docker: 20 | - image: cimg/node:21.1 21 | steps: 22 | - checkout 23 | - restore_cache: 24 | key: dependency-cache-{{ checksum "package.json" }} 25 | - run: 26 | name: Install dependencies 27 | command: npm install --ignore-scripts 28 | - save_cache: 29 | key: dependency-cache-{{ checksum "package.json" }} 30 | paths: 31 | - ./node_modules 32 | - run: 33 | name: Build 34 | command: npm run build 35 | 36 | integration_tests: 37 | working_directory: ~/nest-azure-database 38 | docker: 39 | - image: cimg/node:21.1 40 | steps: 41 | - checkout 42 | - *restore-cache 43 | - *install-deps 44 | - run: 45 | name: Integration tests 46 | command: npm test 47 | 48 | workflows: 49 | version: 2 50 | build-and-test: 51 | jobs: 52 | - build 53 | - integration_tests: 54 | requires: 55 | - build 56 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.service.ts: -------------------------------------------------------------------------------- 1 | import { TableClient, TableServiceClient } from '@azure/data-tables'; 2 | import { Inject, Injectable } from '@nestjs/common'; 3 | import { AZURE_TABLE_STORAGE_MODULE_OPTIONS, AZURE_TABLE_STORAGE_NAME } from './azure-table.constant'; 4 | import { AzureTableStorageOptions } from './azure-table.interface'; 5 | 6 | @Injectable() 7 | export class AzureTableStorageService { 8 | constructor( 9 | @Inject(AZURE_TABLE_STORAGE_NAME) private readonly tableName: string, 10 | @Inject(AZURE_TABLE_STORAGE_MODULE_OPTIONS) private readonly options?: AzureTableStorageOptions, 11 | ) {} 12 | 13 | private tableServiceClient: TableServiceClient; 14 | private tableClient: TableClient; 15 | 16 | get tableServiceClientInstance() { 17 | if (this.tableServiceClient) { 18 | return this.tableServiceClient; 19 | } 20 | this.tableServiceClient = TableServiceClient.fromConnectionString( 21 | this.options.connectionString || process.env.AZURE_STORAGE_CONNECTION_STRING, 22 | this.options, 23 | ); 24 | return this.tableServiceClient; 25 | } 26 | 27 | get tableClientInstance() { 28 | if (this.tableClient) { 29 | return this.tableClient; 30 | } 31 | this.tableClient = TableClient.fromConnectionString( 32 | this.options.connectionString || process.env.AZURE_STORAGE_CONNECTION_STRING, 33 | this.tableName, 34 | this.options, 35 | ); 36 | return this.tableClient; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/cosmos-db/src/event/event.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | NotFoundException, 7 | Param, 8 | Post, 9 | Put, 10 | } from '@nestjs/common'; 11 | import { EventDTO } from './event.dto'; 12 | import { EventService } from './event.service'; 13 | 14 | const SPLIT_SEP = /[.,|-]+/; // Choose your own separator for the hierarchical partition key 15 | 16 | @Controller('event') 17 | export class EventController { 18 | constructor(private readonly events: EventService) {} 19 | 20 | @Get() 21 | async getEvents() { 22 | return await this.events.getEvents(); 23 | } 24 | 25 | @Get(':type/:id') 26 | async getEvent(@Param('id') id: string, @Param('type') type: string) { 27 | const event = await this.events.getEvent(id, type.split(SPLIT_SEP)); 28 | if (event === undefined) { 29 | throw new NotFoundException('Event not found'); 30 | } 31 | return event; 32 | } 33 | 34 | @Post() 35 | async createEvent(@Body() eventDto: EventDTO) { 36 | return await this.events.create(eventDto); 37 | } 38 | 39 | @Delete(':type/:id') 40 | async deleteEvent(@Param('id') id: string, @Param('type') type: string) { 41 | return await this.events.deleteEvent(id, type.split(SPLIT_SEP)); 42 | } 43 | 44 | @Put(':type/:id') 45 | async updateEvent( 46 | @Param('id') id: string, 47 | @Param('type') type: string, 48 | @Body() eventDto: EventDTO, 49 | ) { 50 | return await this.events.updateEvent(id, type.split(SPLIT_SEP), eventDto); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## I'm submitting a... 8 | 11 |

12 | [ ] Regression 
13 | [ ] Bug report
14 | [ ] Feature request
15 | [ ] Documentation issue or request
16 | [ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.
17 | 
18 | 19 | ## Current behavior 20 | 21 | 22 | 23 | ## Expected behavior 24 | 25 | 26 | 27 | ## Minimal reproduction of the problem with instructions 28 | 29 | 30 | ## What is the motivation / use case for changing the behavior? 31 | 32 | 33 | 34 | ## Environment 35 | 36 |

37 | Nest version: X.Y.Z
38 | 
39 |  
40 | For Tooling issues:
41 | - Node version: XX  
42 | - Platform:  
43 | 
44 | Others:
45 | 
46 | 
-------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.interface.ts: -------------------------------------------------------------------------------- 1 | import { CosmosClientOptions } from '@azure/cosmos'; 2 | import { Type } from '@nestjs/common'; 3 | import { ModuleMetadata } from '@nestjs/common/interfaces'; 4 | 5 | export interface AzureCosmosDbOptions extends CosmosClientOptions { 6 | dbName: string; 7 | retryAttempts?: number; 8 | retryDelay?: number; 9 | connectionName?: string; 10 | } 11 | 12 | export interface AzureCosmosDbOptionsFactory { 13 | createAzureCosmosDbOptions(): Promise | AzureCosmosDbOptions; 14 | } 15 | 16 | export interface AzureCosmosDbModuleAsyncOptions extends Pick { 17 | connectionName?: string; 18 | useExisting?: Type; 19 | useClass?: Type; 20 | useFactory?: (...args: any[]) => Promise | AzureCosmosDbOptions; 21 | inject?: any[]; 22 | } 23 | 24 | type GeoJsonTypes = 'Point' | 'Polygon' | 'LineStrings'; 25 | 26 | export type Position = number[]; // [number, number] | [number, number, number]; Longitude, Latitude 27 | 28 | interface GeoJsonObject { 29 | type: GeoJsonTypes; 30 | } 31 | 32 | export class Point implements GeoJsonObject { 33 | type: 'Point' = 'Point' as const; 34 | coordinates: Position; 35 | } 36 | 37 | export class LineString implements GeoJsonObject { 38 | type: 'LineStrings' = 'LineStrings' as const; 39 | coordinates: Position[]; 40 | } 41 | 42 | export class Polygon implements GeoJsonObject { 43 | type: 'Polygon' = 'Polygon' as const; 44 | coordinates: Position[][]; 45 | } 46 | -------------------------------------------------------------------------------- /sample/table-storage/src/event/event.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | } from '@nestjs/common'; 10 | import { EventDTO } from './event.dto'; 11 | import { Event } from './event.entity'; 12 | import { EventService } from './event.service'; 13 | 14 | @Controller('event') 15 | export class EventController { 16 | constructor(private readonly events: EventService) {} 17 | 18 | @Get(':partitionKey/:rowKey') 19 | async getEvent( 20 | @Param('partitionKey') partitionKey: string, 21 | @Param('rowKey') rowKey: string, 22 | ) { 23 | return await this.events.find(partitionKey, rowKey); 24 | } 25 | 26 | @Get() 27 | async getEvents() { 28 | return await this.events.findAll(); 29 | } 30 | 31 | @Post() 32 | async createEvent(@Body() eventDto: EventDTO) { 33 | const event = new Event(); 34 | Object.assign(event, eventDto); 35 | 36 | return await this.events.create(event); 37 | } 38 | 39 | @Put(':partitionKey/:rowKey') 40 | async updateEvent( 41 | @Param('partitionKey') partitionKey: string, 42 | @Param('rowKey') rowKey: string, 43 | @Body() eventDto: EventDTO, 44 | ) { 45 | const event = new Event(); 46 | Object.assign(event, eventDto); 47 | 48 | return await this.events.update(partitionKey, rowKey, event); 49 | } 50 | 51 | @Delete(':partitionKey/:rowKey') 52 | async deleteDelete( 53 | @Param('partitionKey') partitionKey: string, 54 | @Param('rowKey') rowKey: string, 55 | ) { 56 | return await this.events.delete(partitionKey, rowKey); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.mapper.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import { AZURE_TABLE_ENTITY } from './azure-table.decorators'; 3 | 4 | export interface PartitionRowKeyValues { 5 | partitionKey: string; 6 | rowKey: string; 7 | } 8 | 9 | export class AzureEntityMapper { 10 | static serializeAll(entriesDescriptor: any[]): E[] { 11 | return entriesDescriptor.map(entry => { 12 | return AzureEntityMapper.serialize(entry); 13 | }); 14 | } 15 | static serialize(entryDescriptor: any) { 16 | const result = {} as E; 17 | 18 | for (const key in entryDescriptor) { 19 | if (key !== 'odata.metadata') { 20 | result[key] = entryDescriptor[key]; 21 | } 22 | } 23 | 24 | return result; 25 | } 26 | static createEntity(partialDto: Partial) { 27 | // Note: make sure we are getting the metadata from the DTO constructor 28 | // See: src/table-storage/azure-table.repository.ts 29 | const entityDescriptor = Reflect.getMetadata(AZURE_TABLE_ENTITY, partialDto.constructor) as PartitionRowKeyValues; 30 | 31 | if (typeof entityDescriptor === 'undefined') { 32 | throw new Error( 33 | `Could not extract metadata from ${partialDto.constructor.name}. Make sure you are passing a valid Entity`, 34 | ); 35 | } 36 | 37 | const entity: PartitionRowKeyValues = { 38 | ...entityDescriptor, 39 | }; 40 | 41 | for (const key in partialDto) { 42 | if (entityDescriptor[key]) { 43 | entity[key] = partialDto[key]; 44 | } 45 | } 46 | 47 | if (!entity.rowKey) { 48 | entity.rowKey = randomUUID() as string; 49 | } 50 | 51 | return entity as D & PartitionRowKeyValues; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample/table-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "table-storage", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^11.0.0", 24 | "@nestjs/core": "^11.0.0", 25 | "@nestjs/platform-express": "^11.0.0", 26 | "dotenv": "^16.3.1", 27 | "reflect-metadata": "^0.1.13", 28 | "rxjs": "^7.8.1" 29 | }, 30 | "devDependencies": { 31 | "@nestjs/cli": "^11.0.0", 32 | "@nestjs/schematics": "^11.0.0", 33 | "@nestjs/testing": "^11.0.0", 34 | "@types/express": "^5.0.0", 35 | "@types/jest": "^29.5.2", 36 | "@types/node": "^24.0.0", 37 | "@types/supertest": "^6.0.0", 38 | "@typescript-eslint/eslint-plugin": "^8.0.0", 39 | "@typescript-eslint/parser": "^8.0.0", 40 | "eslint": "^8.42.0", 41 | "eslint-config-prettier": "^10.0.0", 42 | "eslint-plugin-prettier": "^5.0.0", 43 | "jest": "^29.5.0", 44 | "prettier": "^3.0.0", 45 | "source-map-support": "^0.5.21", 46 | "supertest": "^7.0.0", 47 | "ts-jest": "^29.1.0", 48 | "ts-loader": "^9.4.3", 49 | "ts-node": "^10.9.1", 50 | "tsconfig-paths": "^4.2.0", 51 | "typescript": "^5.1.3" 52 | }, 53 | "jest": { 54 | "moduleFileExtensions": [ 55 | "js", 56 | "json", 57 | "ts" 58 | ], 59 | "rootDir": "src", 60 | "testRegex": ".*\\.spec\\.ts$", 61 | "transform": { 62 | "^.+\\.(t|j)s$": "ts-jest" 63 | }, 64 | "collectCoverageFrom": [ 65 | "**/*.(t|j)s" 66 | ], 67 | "coverageDirectory": "../coverage", 68 | "testEnvironment": "node" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sample/cosmos-db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmos-db", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/azure-database": "file:../../", 24 | "@nestjs/common": "^11.0.0", 25 | "@nestjs/core": "^11.0.0", 26 | "@nestjs/platform-express": "^11.0.0", 27 | "dotenv": "^16.3.1", 28 | "reflect-metadata": "^0.1.13", 29 | "rxjs": "^7.8.1" 30 | }, 31 | "devDependencies": { 32 | "@nestjs/cli": "^11.0.0", 33 | "@nestjs/schematics": "^11.0.0", 34 | "@nestjs/testing": "^11.0.0", 35 | "@types/express": "^5.0.0", 36 | "@types/jest": "^29.5.2", 37 | "@types/node": "^24.0.0", 38 | "@types/supertest": "^6.0.0", 39 | "@typescript-eslint/eslint-plugin": "^8.0.0", 40 | "@typescript-eslint/parser": "^8.0.0", 41 | "eslint": "^8.42.0", 42 | "eslint-config-prettier": "^10.0.0", 43 | "eslint-plugin-prettier": "^5.0.0", 44 | "jest": "^29.5.0", 45 | "prettier": "^3.0.0", 46 | "source-map-support": "^0.5.21", 47 | "supertest": "^7.0.0", 48 | "ts-jest": "^29.1.0", 49 | "ts-loader": "^9.4.3", 50 | "ts-node": "^10.9.1", 51 | "tsconfig-paths": "^4.2.0", 52 | "typescript": "^5.1.3" 53 | }, 54 | "jest": { 55 | "moduleFileExtensions": [ 56 | "js", 57 | "json", 58 | "ts" 59 | ], 60 | "rootDir": "src", 61 | "testRegex": ".*\\.spec\\.ts$", 62 | "transform": { 63 | "^.+\\.(t|j)s$": "ts-jest" 64 | }, 65 | "collectCoverageFrom": [ 66 | "**/*.(t|j)s" 67 | ], 68 | "coverageDirectory": "../coverage", 69 | "testEnvironment": "node" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/azurite-integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AzureTableStorageModule } from '../../../lib'; 3 | import { AzuriteModule } from '../modules/azurite.module'; 4 | import { AzuriteTable, Event, EventService } from '../services'; 5 | 6 | /** 7 | * Integration test to integrate the library to Azurite emulator 8 | * https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite 9 | */ 10 | describe('azurite-integration', () => { 11 | let moduleRef: TestingModule; 12 | let server: AzuriteTable; 13 | let service: EventService; 14 | 15 | beforeAll(async () => { 16 | server = new AzuriteTable(10101); 17 | await server.start(); 18 | 19 | const connectionString = server.getConnectionString(); 20 | 21 | moduleRef = await Test.createTestingModule({ 22 | imports: [ 23 | AzureTableStorageModule.forRoot({ 24 | connectionString, 25 | allowInsecureConnection: true, 26 | }), 27 | AzuriteModule, 28 | ], 29 | }).compile(); 30 | 31 | service = moduleRef.get(EventService); 32 | }); 33 | 34 | afterAll(async () => { 35 | await moduleRef.close(); 36 | server.stop(); 37 | }); 38 | 39 | it('should be defined', () => { 40 | expect(moduleRef).toBeDefined(); 41 | expect(server).toBeDefined(); 42 | expect(service).toBeDefined(); 43 | }); 44 | 45 | describe('azurite', () => { 46 | const partitionKey = 'partition-1'; 47 | const rowKey = '1'; 48 | 49 | it('should write data', async () => { 50 | const event: Event = { 51 | name: 'Event name', 52 | partitionKey, 53 | rowKey, 54 | }; 55 | 56 | const result = await service.create(event); 57 | expect(result).toBeDefined(); 58 | }); 59 | 60 | it('should read data', async () => { 61 | const result = await service.find(partitionKey, rowKey); 62 | expect(result).toBeDefined(); 63 | expect(result).toHaveProperty('name', 'Event name'); 64 | }); 65 | 66 | it('should read all data', async () => { 67 | const result = await service.findAll(); 68 | expect(result).toBeDefined(); 69 | expect(result).toMatchObject([{ name: 'Event name' }]); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/azurite-integration-async.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AzureTableStorageModule } from '../../../lib'; 3 | import { AzuriteModule } from '../modules/azurite.module'; 4 | import { AzuriteTable, Event, EventService } from '../services'; 5 | 6 | /** 7 | * Integration test to integrate the @nestjs/azure-database library to the Azurite emulator 8 | * https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite 9 | */ 10 | describe('azurite-integration-async', () => { 11 | let moduleRef: TestingModule; 12 | let server: AzuriteTable; 13 | let service: EventService; 14 | 15 | beforeAll(async () => { 16 | server = new AzuriteTable(10102); 17 | await server.start(); 18 | 19 | const connectionString = server.getConnectionString(); 20 | 21 | moduleRef = await Test.createTestingModule({ 22 | imports: [ 23 | AzureTableStorageModule.forRootAsync({ 24 | useFactory: async () => ({ 25 | connectionString, 26 | allowInsecureConnection: true, 27 | }), 28 | }), 29 | AzuriteModule, 30 | ], 31 | }).compile(); 32 | 33 | service = moduleRef.get(EventService); 34 | }); 35 | 36 | afterAll(async () => { 37 | await moduleRef.close(); 38 | server.stop(); 39 | }); 40 | 41 | it('should be defined', () => { 42 | expect(moduleRef).toBeDefined(); 43 | expect(server).toBeDefined(); 44 | expect(service).toBeDefined(); 45 | }); 46 | 47 | describe('azurite', () => { 48 | const partitionKey = 'partition-1'; 49 | const rowKey = '1'; 50 | 51 | it('should write data', async () => { 52 | const event: Event = { 53 | name: 'Event name', 54 | partitionKey, 55 | rowKey, 56 | }; 57 | 58 | const result = await service.create(event); 59 | expect(result).toBeDefined(); 60 | }); 61 | 62 | it('should read data', async () => { 63 | const result = await service.find(partitionKey, rowKey); 64 | expect(result).toBeDefined(); 65 | expect(result).toHaveProperty('name', 'Event name'); 66 | }); 67 | 68 | it('should read all data', async () => { 69 | const result = await service.findAll(); 70 | expect(result).toBeDefined(); 71 | expect(result).toMatchObject([{ name: 'Event name' }]); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /sample/cosmos-db/src/event/event.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectModel } from '@nestjs/azure-database'; 2 | import type { Container } from '@azure/cosmos'; 3 | import { Injectable, UnprocessableEntityException } from '@nestjs/common'; 4 | import { EventDTO } from './event.dto'; 5 | import { Event } from './event.entity'; 6 | 7 | @Injectable() 8 | export class EventService { 9 | constructor( 10 | @InjectModel(Event) 11 | private readonly eventContainer: Container, 12 | ) {} 13 | 14 | async create(eventDto: EventDTO): Promise { 15 | if (!eventDto.id) { 16 | eventDto.id = Date.now().toString(); 17 | } 18 | const { resource } = await this.eventContainer.items.create( 19 | eventDto, 20 | ); 21 | return resource; 22 | } 23 | 24 | async getEvents(): Promise { 25 | try { 26 | const querySpec = { 27 | query: 'SELECT * FROM events', 28 | }; 29 | const { resources } = await this.eventContainer.items 30 | .query(querySpec) 31 | .fetchAll(); 32 | return resources; 33 | } catch (error) { 34 | throw new UnprocessableEntityException(error); 35 | } 36 | } 37 | 38 | async getEvent(id: string, type: string | string[]): Promise { 39 | try { 40 | const { resource } = await this.eventContainer 41 | .item(id, type) 42 | .read(); 43 | 44 | return resource; 45 | } catch (error) { 46 | throw new UnprocessableEntityException(error); 47 | } 48 | } 49 | 50 | async updateEvent( 51 | id: string, 52 | type: string | string[], 53 | eventData: EventDTO, 54 | ): Promise { 55 | try { 56 | let { resource: item } = await this.eventContainer 57 | .item(id, type) 58 | .read(); 59 | 60 | item = { 61 | ...item, 62 | ...eventData, 63 | }; 64 | 65 | const { resource: replaced } = await this.eventContainer 66 | .item(id, type) 67 | .replace(item); 68 | 69 | return replaced; 70 | } catch (error) { 71 | throw new UnprocessableEntityException(error); 72 | } 73 | } 74 | 75 | async deleteEvent(id: string, type: string | string[]): Promise { 76 | try { 77 | const { resource: deleted } = await this.eventContainer 78 | .item(id, type) 79 | .delete(); 80 | 81 | return deleted; 82 | } catch (error) { 83 | throw new UnprocessableEntityException(error); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.providers.ts: -------------------------------------------------------------------------------- 1 | import { ContainerDefinition, Database, DataType, IndexKind } from '@azure/cosmos'; 2 | import { AZURE_COSMOS_DB_ENTITY } from './cosmos-db.decorators'; 3 | import { getConnectionToken, getModelToken, pluralize } from './cosmos-db.utils'; 4 | 5 | export interface PartitionKeyValues { 6 | PartitionKey: string; 7 | } 8 | 9 | export function createAzureCosmosDbProviders( 10 | connectionName?: string, 11 | models: { dto: any; collection?: string }[] = [], 12 | ) { 13 | const providers = (models || []).map(model => ({ 14 | provide: getModelToken(model.dto.name), 15 | useFactory: async (database: Database) => { 16 | const entityDescriptor = Reflect.getMetadata(AZURE_COSMOS_DB_ENTITY, model.dto) as PartitionKeyValues; 17 | const partitionKey = entityDescriptor ? entityDescriptor.PartitionKey : null; 18 | const containerName = model.collection ?? pluralize(model.dto.name); 19 | const containerOptions: ContainerDefinition = { 20 | id: containerName, 21 | uniqueKeyPolicy: { 22 | uniqueKeys: [], 23 | }, 24 | }; 25 | 26 | // If the container has a DateTime field we add a Range Index 27 | // ref: https://docs.microsoft.com/en-us/azure/cosmos-db/working-with-dates#indexing-datetimes-for-range-queries 28 | if (Object.values(entityDescriptor).indexOf('DateTime') > -1) { 29 | containerOptions.indexingPolicy = { 30 | includedPaths: [ 31 | { 32 | path: `/*`, 33 | indexes: [ 34 | { 35 | kind: IndexKind.Range, 36 | dataType: DataType.String, 37 | precision: -1, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }; 43 | } 44 | 45 | for (const key in entityDescriptor) { 46 | if (entityDescriptor.hasOwnProperty(key)) { 47 | const element = entityDescriptor[key]; 48 | if (element === 'UniqueKey') { 49 | containerOptions.uniqueKeyPolicy.uniqueKeys.push({ paths: [`/${key}`] }); 50 | } 51 | } 52 | } 53 | 54 | if (partitionKey != null) { 55 | containerOptions.partitionKey = { 56 | paths: [`/${partitionKey}`], 57 | }; 58 | } 59 | const coResponse = await database.containers.createIfNotExists(containerOptions); 60 | 61 | return coResponse.container; 62 | }, 63 | inject: [getConnectionToken(connectionName)], 64 | })); 65 | return providers; 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nestjs/azure-database", 3 | "version": "4.0.0", 4 | "description": "The Azure Table Storage module for Nest framework (node.js)", 5 | "main": "dist/index.js", 6 | "author": { 7 | "name": "Wassim Chegham", 8 | "email": "github@wassim.dev", 9 | "url": "https://twitter.com/manekinekko" 10 | }, 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/nestjs/azure-database.git" 15 | }, 16 | "scripts": { 17 | "test": "jest --passWithNoTests --runInBand", 18 | "precommit": "lint-staged", 19 | "prettier": "prettier \"{lib,tests}/**/*.ts\" --write && git status", 20 | "build": "rimraf dist && npm run build:lib && npm test", 21 | "format": "prettier --write \"{lib,tests}/**/*.ts\"", 22 | "lint": "eslint \"{lib,tests}/**/*.ts\" --fix", 23 | "build:lib": "tsc -p tsconfig.json", 24 | "prepare": "npm run build", 25 | "prepublish:npm": "npm run build", 26 | "publish:npm": "npm publish --access public", 27 | "prepublish:next": "npm run build", 28 | "publish:next": "npm publish --access public --tag next" 29 | }, 30 | "peerDependencies": { 31 | "@nestjs/common": "^10.0.0 || ^11.0.0", 32 | "@nestjs/core": "^10.0.0 || ^11.0.0" 33 | }, 34 | "dependencies": { 35 | "@azure/cosmos": "^4.0.0", 36 | "@azure/data-tables": "^13.2.2", 37 | "@nestjs/common": "^11.0.0", 38 | "@nestjs/core": "^11.0.0" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "20.2.0", 42 | "@commitlint/config-angular": "20.2.0", 43 | "@eslint/eslintrc": "3.3.3", 44 | "@eslint/js": "9.39.2", 45 | "@nestjs/testing": "11.1.9", 46 | "@types/jest": "29.5.14", 47 | "@types/node": "24.9.1", 48 | "azurite": "3.35.0", 49 | "dotenv": "16.6.1", 50 | "eslint": "9.39.2", 51 | "eslint-config-prettier": "10.1.8", 52 | "eslint-plugin-prettier": "5.5.4", 53 | "globals": "16.5.0", 54 | "husky": "9.1.7", 55 | "jest": "29.7.0", 56 | "lint-staged": "16.2.7", 57 | "prettier": "3.7.4", 58 | "reflect-metadata": "0.1.14", 59 | "rimraf": "6.1.2", 60 | "rxjs": "7.8.2", 61 | "supertest": "7.1.4", 62 | "ts-jest": "29.4.6", 63 | "typescript": "5.9.3", 64 | "typescript-eslint": "8.50.0" 65 | }, 66 | "lint-staged": { 67 | "*.ts": [ 68 | "prettier --write" 69 | ] 70 | }, 71 | "husky": { 72 | "hooks": { 73 | "pre-commit": "lint-staged", 74 | "commit-msg": "commitlint -c .commitlintrc.json -E HUSKY_GIT_PARAMS" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { getuserAgentSuffix } from './cosmos-db.utils'; 2 | import { readFile } from 'fs/promises'; 3 | import { join } from 'path'; 4 | 5 | // Mock fs/promises 6 | jest.mock('fs/promises'); 7 | const mockReadFile = readFile as jest.MockedFunction; 8 | 9 | describe('cosmos-db.utils', () => { 10 | describe('getuserAgentSuffix', () => { 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('should return user agent with package info when package.json exists', async () => { 16 | const mockPackageJson = { 17 | name: '@nestjs/azure-database', 18 | version: '4.0.0', 19 | }; 20 | 21 | mockReadFile.mockResolvedValue(JSON.stringify(mockPackageJson)); 22 | 23 | const result = await getuserAgentSuffix(); 24 | 25 | expect(mockReadFile).toHaveBeenCalledWith(join(__dirname, '..', '..', 'package.json'), 'utf8'); 26 | expect(result).toBe( 27 | `node.js/${process.version} (${process.platform}; ${process.arch}) ${mockPackageJson.name}/${mockPackageJson.version}`, 28 | ); 29 | }); 30 | 31 | it('should return fallback user agent when package.json cannot be read', async () => { 32 | mockReadFile.mockRejectedValue(new Error('ENOENT: no such file or directory')); 33 | 34 | const result = await getuserAgentSuffix(); 35 | 36 | expect(mockReadFile).toHaveBeenCalledWith(join(__dirname, '..', '..', 'package.json'), 'utf8'); 37 | expect(result).toBe(`node.js/${process.version} (${process.platform}; ${process.arch}) @nestjs/azure-database/0.0.0`); 38 | }); 39 | 40 | it('should return fallback user agent when package.json is invalid JSON', async () => { 41 | mockReadFile.mockResolvedValue('invalid json content'); 42 | 43 | const result = await getuserAgentSuffix(); 44 | 45 | expect(mockReadFile).toHaveBeenCalledWith(join(__dirname, '..', '..', 'package.json'), 'utf8'); 46 | expect(result).toBe(`node.js/${process.version} (${process.platform}; ${process.arch}) @nestjs/azure-database/0.0.0`); 47 | }); 48 | 49 | it('should return fallback user agent when package.json has missing properties', async () => { 50 | const mockPackageJson = { 51 | name: '@nestjs/azure-database', 52 | // version is missing 53 | }; 54 | 55 | mockReadFile.mockResolvedValue(JSON.stringify(mockPackageJson)); 56 | 57 | const result = await getuserAgentSuffix(); 58 | 59 | expect(result).toBe(`node.js/${process.version} (${process.platform}; ${process.arch}) @nestjs/azure-database/0.0.0`); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/azurite-integration-class.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AzureTableStorageModule, AzureTableStorageOptionsFactory } from '../../../lib'; 3 | import { AzuriteModule } from '../modules/azurite.module'; 4 | import { AzuriteTable, Event, EventService } from '../services'; 5 | 6 | let connectionString: string; 7 | 8 | class ConfigService implements AzureTableStorageOptionsFactory { 9 | createAzureTableStorageOptions() { 10 | return { 11 | accountName: 'account-name', 12 | sasKey: 'sas-key', 13 | connectionString, 14 | allowInsecureConnection: true, 15 | }; 16 | } 17 | } 18 | 19 | /** 20 | * Integration test to integrate the @nestjs/azure-database library to the Azurite emulator 21 | * https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite 22 | */ 23 | describe('azurite-integration-class', () => { 24 | let moduleRef: TestingModule; 25 | let server: AzuriteTable; 26 | let service: EventService; 27 | 28 | beforeAll(async () => { 29 | server = new AzuriteTable(10103); 30 | await server.start(); 31 | 32 | connectionString = server.getConnectionString(); 33 | 34 | moduleRef = await Test.createTestingModule({ 35 | imports: [ 36 | AzureTableStorageModule.forRootAsync({ 37 | useClass: ConfigService, 38 | }), 39 | AzuriteModule, 40 | ], 41 | }).compile(); 42 | 43 | service = moduleRef.get(EventService); 44 | }); 45 | 46 | afterAll(async () => { 47 | await moduleRef.close(); 48 | server.stop(); 49 | }); 50 | 51 | it('should be defined', () => { 52 | expect(moduleRef).toBeDefined(); 53 | expect(server).toBeDefined(); 54 | expect(service).toBeDefined(); 55 | }); 56 | 57 | describe('azurite', () => { 58 | const partitionKey = 'partition-1'; 59 | const rowKey = '1'; 60 | 61 | it('should write data', async () => { 62 | const event: Event = { 63 | name: 'Event name', 64 | partitionKey, 65 | rowKey, 66 | }; 67 | 68 | const result = await service.create(event); 69 | expect(result).toBeDefined(); 70 | }); 71 | 72 | it('should read data', async () => { 73 | const result = await service.find(partitionKey, rowKey); 74 | expect(result).toBeDefined(); 75 | expect(result).toHaveProperty('name', 'Event name'); 76 | }); 77 | 78 | it('should read all data', async () => { 79 | const result = await service.findAll(); 80 | expect(result).toBeDefined(); 81 | expect(result).toMatchObject([{ name: 'Event name' }]); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/table-storage/e2e/azurite-integration-existing.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AzureTableStorageModule, AzureTableStorageOptionsFactory } from '../../../lib'; 3 | import { AzuriteModule } from '../modules/azurite.module'; 4 | import { AzuriteTable, Event, EventService } from '../services'; 5 | import { Module } from '@nestjs/common'; 6 | 7 | let connectionString: string; 8 | 9 | class ConfigService implements AzureTableStorageOptionsFactory { 10 | createAzureTableStorageOptions() { 11 | return { 12 | accountName: 'account-name', 13 | sasKey: 'sas-key', 14 | connectionString, 15 | allowInsecureConnection: true, 16 | }; 17 | } 18 | } 19 | 20 | @Module({ 21 | providers: [ConfigService], 22 | exports: [ConfigService], 23 | }) 24 | class ConfigModule {} 25 | 26 | /** 27 | * Integration test to integrate the @nestjs/azure-database library to the Azurite emulator 28 | * https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite 29 | */ 30 | describe('azurite-integration-existing', () => { 31 | let moduleRef: TestingModule; 32 | let server: AzuriteTable; 33 | let service: EventService; 34 | 35 | beforeAll(async () => { 36 | server = new AzuriteTable(10104); 37 | await server.start(); 38 | 39 | connectionString = server.getConnectionString(); 40 | 41 | moduleRef = await Test.createTestingModule({ 42 | imports: [ 43 | AzureTableStorageModule.forRootAsync({ 44 | imports: [ConfigModule], 45 | useExisting: ConfigService, 46 | }), 47 | AzuriteModule, 48 | ], 49 | }).compile(); 50 | 51 | service = moduleRef.get(EventService); 52 | }); 53 | 54 | afterAll(async () => { 55 | await moduleRef.close(); 56 | server.stop(); 57 | }); 58 | 59 | it('should be defined', () => { 60 | expect(moduleRef).toBeDefined(); 61 | expect(server).toBeDefined(); 62 | expect(service).toBeDefined(); 63 | }); 64 | 65 | describe('azurite', () => { 66 | const partitionKey = 'partition-1'; 67 | const rowKey = '1'; 68 | 69 | it('should write data', async () => { 70 | const event: Event = { 71 | name: 'Event name', 72 | partitionKey, 73 | rowKey, 74 | }; 75 | 76 | const result = await service.create(event); 77 | expect(result).toBeDefined(); 78 | }); 79 | 80 | it('should read data', async () => { 81 | const result = await service.find(partitionKey, rowKey); 82 | expect(result).toBeDefined(); 83 | expect(result).toHaveProperty('name', 'Event name'); 84 | }); 85 | 86 | it('should read all data', async () => { 87 | const result = await service.findAll(); 88 | expect(result).toBeDefined(); 89 | expect(result).toMatchObject([{ name: 'Event name' }]); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/table-storage/services/azurite-table.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; 2 | import { Logger } from '@nestjs/common'; 3 | 4 | export class AzuriteTable { 5 | private readonly logger = new Logger(this.constructor.name); 6 | 7 | private server: ChildProcessWithoutNullStreams; 8 | 9 | private started = false; 10 | private tableHost: string; 11 | private connectionString: string; 12 | 13 | constructor(private readonly port: number = 10101) {} 14 | 15 | public getConnectionString() { 16 | return this.connectionString; 17 | } 18 | 19 | public isStarted() { 20 | return this.started; 21 | } 22 | 23 | public async start() { 24 | this.server = spawn('azurite-table', ['--inMemoryPersistence', '--tablePort', this.port.toString()], { 25 | // Suppress DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. 26 | env: { ...process.env, NODE_NO_WARNINGS: '1' }, 27 | }); 28 | 29 | this.server.on('error', this.logger.error.bind(this.logger)); 30 | this.server.on('warning', this.logger.warn.bind(this.logger)); 31 | this.server.on('message', this.logger.log.bind(this.logger)); 32 | 33 | await new Promise((resolve, reject) => { 34 | this.server.stdout.on('data', (data: Buffer) => { 35 | if (data.toString().includes(`Azurite Table service successfully started on `)) { 36 | this.started = true; 37 | const matches = data.toString().match(/Azurite Table service successfully started on (?[\d.:]+)/); 38 | if (matches && matches.groups) { 39 | this.tableHost = matches.groups['host']; 40 | resolve(); 41 | } else { 42 | reject('Host cannot be determined'); 43 | } 44 | } 45 | }); 46 | 47 | this.server.stderr.on('data', (data: Buffer) => { 48 | this.logger.error(data.toString()); 49 | if (!this.started) reject(data.toString()); 50 | }); 51 | }); 52 | 53 | this.logger.log('Server listening'); 54 | 55 | /** 56 | * https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite 57 | * The emulator supports a single fixed account and a well-known authentication key for Shared Key authentication. 58 | * This account and key are the only Shared Key credentials permitted for use with the emulator. 59 | * 60 | * They are: 61 | * Account name: devstoreaccount1 62 | * Account key: Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== 63 | */ 64 | this.connectionString = `DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://${this.tableHost}/devstoreaccount1;`; 65 | process.env.AZURE_STORAGE_CONNECTION_STRING = this.connectionString; 66 | } 67 | 68 | public stop() { 69 | // Unsubscribe from listeners to avoid open handles 70 | this.server.off('error', this.logger.error.bind(this.logger)); 71 | this.server.off('warning', this.logger.warn.bind(this.logger)); 72 | this.server.off('message', this.logger.log.bind(this.logger)); 73 | this.server.stdout.removeAllListeners(); 74 | this.server.stderr.removeAllListeners(); 75 | 76 | // Attempt to gracefully terminate the server 77 | this.server.kill(); 78 | 79 | this.logger.log('Azurite server terminated:', this.server.killed); 80 | 81 | this.started = false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /sample/cosmos-db/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.decorators.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AZURE_TABLE_ENTITY, 3 | EntityBinary, 4 | EntityBoolean, 5 | EntityDateTime, 6 | EntityInt32, 7 | EntityInt64, 8 | EntityPartitionKey, 9 | EntityRowKey, 10 | EntityString, 11 | InjectRepository, 12 | } from './azure-table.decorators'; 13 | 14 | type DecoratorFn = () => (target: object, propertyKey?: string) => void; 15 | 16 | describe('Azure Table Storage Decorators', () => { 17 | beforeEach(() => { 18 | // tslint:disable-next-line: no-empty 19 | function MockEntity() {} 20 | }); 21 | 22 | describe('@InjectRepository()', () => { 23 | // tslint:disable-next-line: no-empty 24 | function MockEntity() {} 25 | const value = InjectRepository(MockEntity); 26 | 27 | it('should be of type function', () => { 28 | expect(typeof value).toBe('function'); 29 | }); 30 | 31 | it('should throw when invoked with null target', () => { 32 | expect(() => { 33 | value(null, null); 34 | }).toThrow(); 35 | }); 36 | it('should not throw when invoked', () => { 37 | // tslint:disable-next-line: no-empty 38 | function MockClass() {} 39 | expect(() => { 40 | value(MockClass, null); 41 | }).not.toThrow(); 42 | }); 43 | }); 44 | 45 | describe.skip('[DEPRECATED] @EntityPartitionKey()', () => { 46 | it.skip('should add a PartitionKey ', () => { 47 | @EntityPartitionKey('value') 48 | class MockClass {} 49 | 50 | const metadata = Reflect.getMetadata(AZURE_TABLE_ENTITY, MockClass); 51 | expect(metadata).toStrictEqual({ 52 | partitionKey: 'value', 53 | }); 54 | }); 55 | 56 | it.skip('should add a PartitionKey based on Fn', () => { 57 | @EntityPartitionKey(d => d.id + d.name) 58 | class MockClass { 59 | id = '1'; 60 | name = '2'; 61 | } 62 | 63 | const metadata = Reflect.getMetadata(AZURE_TABLE_ENTITY, MockClass); 64 | expect(metadata).toStrictEqual({ 65 | partitionKey: '12', 66 | }); 67 | }); 68 | }); 69 | 70 | describe.skip('[DEPRECATED] @EntityRowKey()', () => { 71 | it('should add a RowKey ', () => { 72 | @EntityRowKey('value') 73 | class MockClass {} 74 | 75 | const metadata = Reflect.getMetadata(AZURE_TABLE_ENTITY, MockClass); 76 | expect(metadata).toStrictEqual({ 77 | rowKey: 'value', 78 | }); 79 | }); 80 | }); 81 | 82 | [ 83 | { 84 | fn: EntityInt32, 85 | tsType: 'Number', 86 | }, 87 | { 88 | fn: EntityInt64, 89 | tsType: 'Number', 90 | }, 91 | { 92 | fn: EntityBinary, 93 | tsType: 'Blob', 94 | }, 95 | { 96 | fn: EntityBoolean, 97 | tsType: 'Boolean', 98 | }, 99 | { 100 | fn: EntityString, 101 | tsType: 'String', 102 | }, 103 | { 104 | fn: EntityDateTime, 105 | tsType: 'Date', 106 | }, 107 | ].map(decorator => { 108 | testDecorator(decorator.fn.name, decorator.fn, decorator.fn.name.replace('Entity', ''), decorator.tsType); 109 | }); 110 | }); 111 | 112 | function testDecorator(name: string, fn: DecoratorFn, edmType: string, tsType: string) { 113 | describe(`@${name}()`, () => { 114 | it(`should throw if property type is NOT ${edmType}`, () => { 115 | // tslint:disable-next-line: no-empty 116 | 117 | expect(() => { 118 | class MockClass { 119 | @fn() 120 | prop: undefined; 121 | } 122 | }).toThrow(); 123 | }); 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /sample/table-storage/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.utils.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { delay, retryWhen, scan } from 'rxjs/operators'; 4 | import { DEFAULT_DB_CONNECTION } from './cosmos-db.constants'; 5 | import { readFile } from 'fs/promises'; 6 | import { join } from 'path'; 7 | 8 | export async function getuserAgentSuffix(): Promise { 9 | try { 10 | const data = await readFile(join(__dirname, '..', '..', 'package.json'), 'utf8'); 11 | const json = await JSON.parse(data); 12 | if (json.name && json.version) { 13 | return `node.js/${process.version} (${process.platform}; ${process.arch}) ${json.name}/${json.version}`; 14 | } 15 | throw new Error('Missing required package.json properties'); 16 | } catch { 17 | return `node.js/${process.version} (${process.platform}; ${process.arch}) @nestjs/azure-database/0.0.0`; 18 | } 19 | } 20 | 21 | export function getModelToken(model: string) { 22 | return `${model}AzureCosmosDbModel`; 23 | } 24 | 25 | export function getConnectionToken(name?: string) { 26 | return name && name !== DEFAULT_DB_CONNECTION ? `${name}AzureCosmosDbConnection` : DEFAULT_DB_CONNECTION; 27 | } 28 | 29 | export function handleRetry(retryAttempts = 9, retryDelay = 3000): (source: Observable) => Observable { 30 | return (source: Observable) => 31 | source.pipe( 32 | // TODO: migrate from retryWhen(). 33 | retryWhen(e => 34 | e.pipe( 35 | scan((errorCount, error) => { 36 | Logger.error( 37 | `Unable to connect to the cosmos db database. Retrying (${errorCount + 1})...`, 38 | '', 39 | 'AzureCosmosDbModule', 40 | ); 41 | if (errorCount + 1 >= retryAttempts) { 42 | throw error; 43 | } 44 | return errorCount + 1; 45 | }, 0), 46 | delay(retryDelay), 47 | ), 48 | ), 49 | ); 50 | } 51 | 52 | /** 53 | * Pluralize function 54 | */ 55 | const pluralization = [ 56 | [/(m)an$/gi, '$1en'], 57 | [/(pe)rson$/gi, '$1ople'], 58 | [/(child)$/gi, '$1ren'], 59 | [/^(ox)$/gi, '$1en'], 60 | [/(ax|test)is$/gi, '$1es'], 61 | [/(octop|vir)us$/gi, '$1i'], 62 | [/(alias|status)$/gi, '$1es'], 63 | [/(bu)s$/gi, '$1ses'], 64 | [/(buffal|tomat|potat)o$/gi, '$1oes'], 65 | [/([ti])um$/gi, '$1a'], 66 | [/sis$/gi, 'ses'], 67 | [/(?:([^f])fe|([lr])f)$/gi, '$1$2ves'], 68 | [/(hive)$/gi, '$1s'], 69 | [/([^aeiouy]|qu)y$/gi, '$1ies'], 70 | [/(x|ch|ss|sh)$/gi, '$1es'], 71 | [/(matr|vert|ind)ix|ex$/gi, '$1ices'], 72 | [/([m|l])ouse$/gi, '$1ice'], 73 | [/(kn|w|l)ife$/gi, '$1ives'], 74 | [/(quiz)$/gi, '$1zes'], 75 | [/s$/gi, 's'], 76 | [/([^a-z])$/, '$1'], 77 | [/$/gi, 's'], 78 | ]; 79 | const rules = pluralization; 80 | 81 | /** 82 | * Uncountable words. 83 | * 84 | * These words are applied while processing the argument to `toCollectionName`. 85 | * @api public 86 | */ 87 | 88 | const uncountables = [ 89 | 'advice', 90 | 'energy', 91 | 'excretion', 92 | 'digestion', 93 | 'cooperation', 94 | 'health', 95 | 'justice', 96 | 'labour', 97 | 'machinery', 98 | 'equipment', 99 | 'information', 100 | 'pollution', 101 | 'sewage', 102 | 'paper', 103 | 'money', 104 | 'species', 105 | 'series', 106 | 'rain', 107 | 'rice', 108 | 'fish', 109 | 'sheep', 110 | 'moose', 111 | 'deer', 112 | 'news', 113 | 'expertise', 114 | 'status', 115 | 'media', 116 | ]; 117 | 118 | /*! 119 | * Pluralize function. 120 | * 121 | * @author TJ Holowaychuk (extracted from _ext.js_) 122 | * @param {String} string to pluralize 123 | * @api private 124 | */ 125 | 126 | export function pluralize(str: string) { 127 | let found: any[][]; 128 | str = str.toLowerCase(); 129 | // tslint:disable-next-line: no-bitwise 130 | if (!~uncountables.indexOf(str)) { 131 | found = rules.filter(rule => { 132 | return str.match(rule[0]); 133 | }); 134 | if (found[0]) { 135 | return str.replace(found[0][0], found[0][1]); 136 | } 137 | } 138 | return str; 139 | } 140 | -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db-core.module.ts: -------------------------------------------------------------------------------- 1 | import { CosmosClient } from '@azure/cosmos'; 2 | import { DynamicModule, Global, Module, Provider } from '@nestjs/common'; 3 | import { defer } from 'rxjs'; 4 | import { COSMOS_DB_MODULE_OPTIONS } from './cosmos-db.constants'; 5 | import { 6 | AzureCosmosDbModuleAsyncOptions, 7 | AzureCosmosDbOptions, 8 | AzureCosmosDbOptionsFactory, 9 | } from './cosmos-db.interface'; 10 | import { getConnectionToken, getuserAgentSuffix, handleRetry } from './cosmos-db.utils'; 11 | 12 | @Global() 13 | @Module({}) 14 | export class AzureCosmosDbCoreModule { 15 | static forRoot(options: AzureCosmosDbOptions): DynamicModule { 16 | const { dbName, retryAttempts, retryDelay, connectionName, ...cosmosDbOptions } = options; 17 | 18 | const cosmosConnectionName = getConnectionToken(connectionName); 19 | 20 | const connectionProvider = { 21 | provide: cosmosConnectionName, 22 | useFactory: async (): Promise => 23 | await defer(async () => { 24 | cosmosDbOptions.userAgentSuffix = await getuserAgentSuffix(); 25 | const client = new CosmosClient(cosmosDbOptions); 26 | const dbResponse = await client.databases.createIfNotExists({ 27 | id: dbName, 28 | }); 29 | return dbResponse.database; 30 | }) 31 | .pipe(handleRetry(retryAttempts, retryDelay)) 32 | 33 | // TODO: migrate from .toPromise(). 34 | .toPromise(), 35 | }; 36 | 37 | return { 38 | module: AzureCosmosDbCoreModule, 39 | providers: [connectionProvider], 40 | exports: [connectionProvider], 41 | }; 42 | } 43 | 44 | static forRootAsync(options: AzureCosmosDbModuleAsyncOptions): DynamicModule { 45 | const cosmosConnectionName = getConnectionToken(options.connectionName); 46 | 47 | const connectionProvider = { 48 | provide: cosmosConnectionName, 49 | useFactory: async (cosmosModuleOptions: AzureCosmosDbOptions): Promise => { 50 | const { dbName, retryAttempts, retryDelay, connectionName, ...cosmosOptions } = cosmosModuleOptions; 51 | 52 | return await defer(async () => { 53 | cosmosOptions.userAgentSuffix = await getuserAgentSuffix(); 54 | const client = new CosmosClient(cosmosOptions); 55 | const dbResponse = await client.databases.createIfNotExists({ 56 | id: dbName, 57 | }); 58 | return dbResponse.database; 59 | }) 60 | .pipe(handleRetry(retryAttempts, retryDelay)) 61 | 62 | // TODO: migrate from .toPromise(). 63 | .toPromise(); 64 | }, 65 | inject: [COSMOS_DB_MODULE_OPTIONS], 66 | }; 67 | const asyncProviders = this.createAsyncProviders(options); 68 | return { 69 | module: AzureCosmosDbCoreModule, 70 | imports: options.imports, 71 | providers: [...asyncProviders, connectionProvider], 72 | exports: [connectionProvider], 73 | }; 74 | } 75 | 76 | private static createAsyncProviders(options: AzureCosmosDbModuleAsyncOptions): Provider[] { 77 | if (options.useExisting || options.useFactory) { 78 | return [this.createAsyncOptionsProvider(options)]; 79 | } 80 | const useClass = options.useClass; 81 | return [ 82 | this.createAsyncOptionsProvider(options), 83 | { 84 | provide: useClass, 85 | useClass, 86 | }, 87 | ]; 88 | } 89 | 90 | private static createAsyncOptionsProvider(options: AzureCosmosDbModuleAsyncOptions): Provider { 91 | if (options.useFactory) { 92 | return { 93 | provide: COSMOS_DB_MODULE_OPTIONS, 94 | useFactory: options.useFactory, 95 | inject: options.inject || [], 96 | }; 97 | } 98 | // `as Type` is a workaround for microsoft/TypeScript#31603 99 | const inject = [options.useClass || options.useExisting]; 100 | return { 101 | provide: COSMOS_DB_MODULE_OPTIONS, 102 | useFactory: async (optionsFactory: AzureCosmosDbOptionsFactory) => 103 | await optionsFactory.createAzureCosmosDbOptions(), 104 | inject, 105 | }; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Logger, Module, Provider } from '@nestjs/common'; 2 | import { 3 | AZURE_TABLE_STORAGE_FEATURE_OPTIONS, 4 | AZURE_TABLE_STORAGE_MODULE_OPTIONS, 5 | AZURE_TABLE_STORAGE_NAME, 6 | } from './azure-table.constant'; 7 | import { 8 | AzureTableStorageFeatureOptions, 9 | AzureTableStorageModuleAsyncOptions, 10 | AzureTableStorageOptions, 11 | AzureTableStorageOptionsFactory, 12 | } from './azure-table.interface'; 13 | import { createRepositoryProviders } from './azure-table.providers'; 14 | import { AzureTableStorageRepository } from './azure-table.repository'; 15 | import { AzureTableStorageService } from './azure-table.service'; 16 | 17 | const logger = new Logger(`AzureTableStorageModule`); 18 | 19 | const PROVIDERS = [AzureTableStorageService, AzureTableStorageRepository]; 20 | const EXPORTS = [...PROVIDERS]; 21 | 22 | type EntityFn = /* Function */ { 23 | name: string; 24 | }; 25 | 26 | @Global() 27 | @Module({}) 28 | export class AzureTableStorageModule { 29 | constructor() { 30 | if (typeof process.env.AZURE_STORAGE_CONNECTION_STRING === 'undefined') { 31 | logger.error(`AZURE_STORAGE_CONNECTION_STRING is not defined in the environment variables`); 32 | } 33 | } 34 | 35 | static forRoot(options?: AzureTableStorageOptions): DynamicModule { 36 | return { 37 | module: AzureTableStorageModule, 38 | providers: [ 39 | ...PROVIDERS, 40 | { provide: AZURE_TABLE_STORAGE_MODULE_OPTIONS, useValue: options }, 41 | { 42 | provide: AZURE_TABLE_STORAGE_NAME, 43 | useValue: '', 44 | }, 45 | { 46 | provide: AZURE_TABLE_STORAGE_FEATURE_OPTIONS, 47 | useValue: {}, 48 | }, 49 | ], 50 | exports: [...EXPORTS, AZURE_TABLE_STORAGE_MODULE_OPTIONS], 51 | }; 52 | } 53 | 54 | static forRootAsync(options: AzureTableStorageModuleAsyncOptions): DynamicModule { 55 | return { 56 | module: AzureTableStorageModule, 57 | imports: options.imports, 58 | providers: [ 59 | { 60 | provide: AZURE_TABLE_STORAGE_NAME, 61 | useValue: '', 62 | }, 63 | { 64 | provide: AZURE_TABLE_STORAGE_FEATURE_OPTIONS, 65 | useValue: {}, 66 | }, 67 | ...PROVIDERS, 68 | ...this.createAsyncProviders(options), 69 | ], 70 | exports: [...EXPORTS, AZURE_TABLE_STORAGE_MODULE_OPTIONS], 71 | }; 72 | } 73 | 74 | private static createAsyncProviders(options: AzureTableStorageModuleAsyncOptions): Provider[] { 75 | if (options.useExisting || options.useFactory) { 76 | return [this.createAsyncOptionsProvider(options)]; 77 | } 78 | return [ 79 | this.createAsyncOptionsProvider(options), 80 | { 81 | provide: options.useClass, 82 | useClass: options.useClass, 83 | }, 84 | ]; 85 | } 86 | 87 | private static createAsyncOptionsProvider(options: AzureTableStorageModuleAsyncOptions): Provider { 88 | if (options.useFactory) { 89 | return { 90 | provide: AZURE_TABLE_STORAGE_MODULE_OPTIONS, 91 | useFactory: options.useFactory, 92 | inject: options.inject, 93 | }; 94 | } 95 | 96 | return { 97 | provide: AZURE_TABLE_STORAGE_MODULE_OPTIONS, 98 | useFactory: async (optionsFactory: AzureTableStorageOptionsFactory) => 99 | await optionsFactory.createAzureTableStorageOptions(), 100 | inject: [options.useExisting || options.useClass], 101 | }; 102 | } 103 | 104 | static forFeature( 105 | entity: EntityFn, 106 | featureOptions: AzureTableStorageFeatureOptions = { 107 | // use either the given table name or the entity name 108 | table: entity.name, 109 | createTableIfNotExists: false, 110 | }, 111 | ): DynamicModule { 112 | const repositoryProviders = createRepositoryProviders(entity); 113 | 114 | return { 115 | module: AzureTableStorageModule, 116 | providers: [ 117 | ...PROVIDERS, 118 | ...repositoryProviders, 119 | { 120 | provide: AZURE_TABLE_STORAGE_NAME, 121 | useValue: featureOptions.table || entity.name, 122 | }, 123 | { 124 | provide: AZURE_TABLE_STORAGE_FEATURE_OPTIONS, 125 | useValue: featureOptions, 126 | }, 127 | ], 128 | exports: [...EXPORTS, ...repositoryProviders], 129 | }; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/cosmos-db/cosmos-db.decorators.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { PartitionKeyKind, PartitionKeyDefinitionVersion } from '@azure/cosmos'; 3 | import { getConnectionToken, getModelToken } from './cosmos-db.utils'; 4 | 5 | export const AZURE_COSMOS_DB_ENTITY = 'cosmos-db:entity'; 6 | 7 | type AnnotationPropertyType = 'PartitionKey' | 'DateTime' | 'UniqueKey'; 8 | type HierarchicalPartitionKey = { 9 | id?: string; 10 | paths: string[]; 11 | version: PartitionKeyDefinitionVersion; 12 | kind: PartitionKeyKind; 13 | }; 14 | 15 | function validateType(annotationType: AnnotationPropertyType, target: object /* Function */, propertyKey?: string) { 16 | if (propertyKey) { 17 | // tslint:disable-next-line: ban-types 18 | const propertyType = Reflect.getMetadata('design:type', target, propertyKey) as () => void; 19 | 20 | let propertyTypeName = ''; 21 | if (annotationType === 'DateTime') { 22 | propertyTypeName = Date.name; 23 | } else if (annotationType === 'UniqueKey') { 24 | propertyTypeName = String.name; 25 | } else { 26 | throw new Error(`Type ${annotationType} is not supported.`); 27 | } 28 | 29 | if (propertyTypeName.toLowerCase().includes(propertyType.name.toLocaleLowerCase()) === false) { 30 | throw new Error( 31 | `EDM type of "${target.constructor.name}.${propertyKey}" is ${annotationType}. The equivalent of ${annotationType} is ${propertyTypeName}. ` + 32 | `"${propertyKey}" should be of type ${propertyTypeName}. Got ${propertyType.name}`, 33 | ); 34 | } 35 | } 36 | } 37 | 38 | function annotate(value: string | HierarchicalPartitionKey, type: AnnotationPropertyType) { 39 | return (target: object /* Function */, propertyKey?: string) => { 40 | // check if the property type matches the annotated type 41 | validateType(type, target, propertyKey); 42 | 43 | const isPropertyAnnotation = typeof propertyKey === 'string'; 44 | 45 | // define metadata on the parent level 46 | target = isPropertyAnnotation ? target.constructor : target; 47 | 48 | // get previous stored entity descriptor 49 | const storedEntityDescriptor = Reflect.getMetadata(AZURE_COSMOS_DB_ENTITY, target) || {}; 50 | let entityDescriptor = { 51 | ...storedEntityDescriptor, 52 | }; 53 | 54 | // Note: if propertyKey is truthy, we are then annotating a class property declaration 55 | if (isPropertyAnnotation) { 56 | /* 57 | merge previous entity descriptor and new descriptor: 58 | the new descriptor is a mapping of: 59 | - the annotated $propertyKey 60 | - and the required $type 61 | - we also assign any given $value (undefinde otherwise) 62 | */ 63 | 64 | entityDescriptor = { 65 | [propertyKey]: type, 66 | ...entityDescriptor, 67 | }; 68 | } else { 69 | // 70 | /** 71 | * Class annotation. 72 | * 73 | * we need to check for special $type: PartitionKey and RowKey 74 | * if detected, we create a new entry in the descriptor with: 75 | * - the $type as the propertyKey name (PartitionKey or RowKey) 76 | * 77 | * Example: 78 | * @PartitionKey('ContactID') 79 | * export class ContactEntity {} 80 | * 81 | * Would result into the following descriptor: 82 | * { PartitionKey: 'ContactID' } 83 | * 84 | */ 85 | 86 | const isPartitionKey = type === 'PartitionKey'; 87 | if (isPartitionKey) { 88 | if (typeof value === 'string') { 89 | entityDescriptor = { 90 | ...entityDescriptor, 91 | [type]: value || propertyKey, 92 | }; 93 | } else { 94 | entityDescriptor = { 95 | ...entityDescriptor, 96 | [type]: { 97 | paths: value.paths, 98 | version: value.version, 99 | kind: value.kind, 100 | }, 101 | }; 102 | } 103 | } 104 | } 105 | 106 | Reflect.defineMetadata(AZURE_COSMOS_DB_ENTITY, entityDescriptor, target); 107 | }; 108 | } 109 | 110 | export function CosmosPartitionKey(value: string | HierarchicalPartitionKey) { 111 | return annotate(value, 'PartitionKey'); 112 | } 113 | 114 | export function CosmosDateTime(value?: string) { 115 | return annotate(value, 'DateTime'); 116 | } 117 | 118 | export function CosmosUniqueKey(value?: string) { 119 | return annotate(value, 'UniqueKey'); 120 | } 121 | 122 | export const InjectModel = (model: any) => Inject(getModelToken(model.name)); 123 | 124 | export const InjectConnection = (name?: string) => Inject(getConnectionToken(name)); 125 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.decorators.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { ValueType } from './azure-table.interface'; 3 | import { getRepositoryToken } from './azure-table.providers'; 4 | 5 | export const AZURE_TABLE_ENTITY = 'azure-table-storage:entity'; 6 | 7 | type EntityFn = { 8 | name: string; 9 | }; 10 | 11 | type AnnotationPropertyType = 12 | // Note: PartitionKey has been renamed to partitionKey 13 | | 'partitionKey' 14 | // Note: RowKey has been renamed to rowKey 15 | | 'rowKey' 16 | | 'Int32' 17 | | 'Int64' 18 | | 'Binary' 19 | | 'Boolean' 20 | | 'String' 21 | | 'Guid' 22 | | 'Double' 23 | | 'DateTime'; 24 | 25 | function validateType(edmType: AnnotationPropertyType, target: object /* Function */, propertyKey?: string) { 26 | if (propertyKey) { 27 | // tslint:disable-next-line: ban-types 28 | const propertyType = Reflect.getMetadata('design:type', target, propertyKey) as () => void; 29 | 30 | let edmTypeName = ''; 31 | if (edmType === 'Int32' || edmType === 'Int64' || edmType === 'Double') { 32 | edmTypeName = Number.name; 33 | } else if (edmType === 'Boolean') { 34 | edmTypeName = Boolean.name; 35 | } else if (edmType === 'DateTime') { 36 | edmTypeName = Date.name; 37 | } else if (edmType === 'String' || edmType === 'Guid') { 38 | edmTypeName = String.name; 39 | } else if (edmType === 'Binary') { 40 | edmTypeName = Blob.name; 41 | } else { 42 | throw new Error(`Type ${edmType} is not supported.`); 43 | } 44 | 45 | if (edmTypeName.toLowerCase().includes(propertyType.name.toLocaleLowerCase()) === false) { 46 | throw new Error( 47 | `EDM type of "${target.constructor.name}.${propertyKey}" is ${edmType}. The equivalent of ${edmType} is ${edmTypeName}. ` + 48 | `"${propertyKey}" should be of type ${edmTypeName}. Got ${propertyType.name}`, 49 | ); 50 | } 51 | } 52 | } 53 | 54 | // NOTE: Class annotation (partitionKey or rowKey) have been deprecated. 55 | function annotate(value: ValueType | undefined, type: AnnotationPropertyType) { 56 | return (target: object /* Function */, propertyKey?: string) => { 57 | // check if the property type matches the annotated type 58 | validateType(type, target, propertyKey); 59 | 60 | const isPropertyAnnotation = typeof propertyKey === 'string'; 61 | 62 | // define metadata on the parent level 63 | target = isPropertyAnnotation ? target.constructor : target; 64 | 65 | // get previous stored entity descriptor 66 | const storedEntityDescriptor = Reflect.getMetadata(AZURE_TABLE_ENTITY, target) || {}; 67 | let entityDescriptor = { 68 | ...storedEntityDescriptor, 69 | }; 70 | 71 | if (typeof value === 'string') { 72 | // Do nothing 73 | } else if (typeof value === 'function') { 74 | value = value(new (target as any)()); 75 | } else { 76 | value = propertyKey; 77 | } 78 | 79 | // Note: if propertyKey is truthy, we are then annotating a class property declaration 80 | if (isPropertyAnnotation) { 81 | entityDescriptor = { 82 | [propertyKey]: { value, type }, 83 | ...entityDescriptor, 84 | }; 85 | } else { 86 | const isPartitionKey = type === 'partitionKey'; 87 | const isRowKey = type === 'rowKey'; 88 | if (isPartitionKey || isRowKey) { 89 | entityDescriptor = { 90 | ...entityDescriptor, 91 | [type]: value, 92 | }; 93 | } 94 | } 95 | 96 | Reflect.defineMetadata(AZURE_TABLE_ENTITY, entityDescriptor, target); 97 | }; 98 | } 99 | 100 | /** @deprecated */ 101 | export function EntityPartitionKey(value: ValueType) { 102 | return annotate(value, 'partitionKey'); 103 | } 104 | 105 | /** @deprecated */ 106 | export function EntityRowKey(value: ValueType) { 107 | return annotate(value, 'rowKey'); 108 | } 109 | 110 | export function EntityInt32(value?: string) { 111 | return annotate(value, 'Int32'); 112 | } 113 | 114 | export function EntityInt64(value?: string) { 115 | return annotate(value, 'Int64'); 116 | } 117 | 118 | export function EntityBinary(value?: string) { 119 | return annotate(value, 'Binary'); 120 | } 121 | 122 | export function EntityBoolean(value?: string) { 123 | return annotate(value, 'Boolean'); 124 | } 125 | 126 | export function EntityString(value?: string) { 127 | return annotate(value, 'String'); 128 | } 129 | 130 | export function EntityGuid(value?: string) { 131 | return annotate(value, 'Guid'); 132 | } 133 | 134 | export function EntityDouble(value?: string) { 135 | return annotate(value, 'Double'); 136 | } 137 | 138 | export function EntityDateTime(value?: string) { 139 | return annotate(value, 'DateTime'); 140 | } 141 | 142 | export const InjectRepository = (entity: EntityFn) => Inject(getRepositoryToken(entity)); 143 | -------------------------------------------------------------------------------- /lib/table-storage/azure-table.repository.ts: -------------------------------------------------------------------------------- 1 | import { DeleteTableEntityResponse, TableEntity, TableEntityQueryOptions } from '@azure/data-tables'; 2 | import { Inject, Injectable, Logger } from '@nestjs/common'; 3 | import { AZURE_TABLE_STORAGE_FEATURE_OPTIONS, AZURE_TABLE_STORAGE_NAME } from './azure-table.constant'; 4 | import { AzureTableStorageFeatureOptions } from './azure-table.interface'; 5 | import { AzureEntityMapper } from './azure-table.mapper'; 6 | import { AzureTableStorageService } from './azure-table.service'; 7 | 8 | const logger = new Logger(`AzureStorageRepository`); 9 | 10 | type PartitionRowKeyValuePair = { 11 | partitionKey: string; 12 | rowKey: string; 13 | }; 14 | 15 | @Injectable() 16 | export class AzureTableStorageRepository { 17 | constructor( 18 | private readonly manager: AzureTableStorageService, 19 | @Inject(AZURE_TABLE_STORAGE_NAME) private readonly tableName: string, 20 | @Inject(AZURE_TABLE_STORAGE_FEATURE_OPTIONS) private readonly options: AzureTableStorageFeatureOptions, 21 | ) {} 22 | 23 | get tableServiceClientInstance() { 24 | return this.manager.tableServiceClientInstance; 25 | } 26 | 27 | get tableClientInstance() { 28 | return this.manager.tableClientInstance; 29 | } 30 | 31 | async createTableIfNotExists(tableName?: string): Promise { 32 | logger.debug(`Create table: ${tableName} (if not exists)`); 33 | 34 | try { 35 | await this.tableServiceClientInstance.createTable(tableName); 36 | return true; 37 | } catch (error) { 38 | return this.handleRestErrors(error); 39 | } 40 | } 41 | 42 | async find(partitionKey: string, rowKey: string): Promise { 43 | logger.debug(`Looking for entity in table: ${this.tableName}`); 44 | logger.debug(`- partitionKey: ${partitionKey}`); 45 | logger.debug(`- rowKey: ${rowKey}`); 46 | 47 | try { 48 | const result = await this.manager.tableClientInstance.getEntity(partitionKey, rowKey); 49 | const mappedEntity = AzureEntityMapper.serialize(result); 50 | 51 | if (Object.entries(mappedEntity).length === 0) { 52 | logger.debug(`Failed to fetch entity`); 53 | return null; 54 | } 55 | 56 | logger.debug(`Entity fetched successfully`); 57 | return mappedEntity; 58 | } catch (error) { 59 | return this.handleRestErrors(error); 60 | } 61 | } 62 | 63 | async findAll(options: { queryOptions?: TableEntityQueryOptions } = {}): Promise { 64 | logger.debug(`Looking for entities in table: ${this.tableName}`); 65 | 66 | try { 67 | const records = this.tableClientInstance.listEntities({ 68 | queryOptions: options.queryOptions, 69 | }); 70 | 71 | const entities = []; 72 | for await (const entity of records) { 73 | entities.push(entity); 74 | } 75 | 76 | logger.debug(`Entities fetched successfully`); 77 | return entities; 78 | } catch (error) { 79 | return this.handleRestErrors(error); 80 | } 81 | } 82 | 83 | async create(entity: T): Promise { 84 | if (this.options.createTableIfNotExists) { 85 | const res = await this.createTableIfNotExists(this.tableName); 86 | if (res === null) { 87 | return null; 88 | } 89 | } 90 | 91 | logger.debug(`Creating entity in table: ${this.tableName}`); 92 | logger.debug(`- partitionKey: ${(entity as TableEntity).partitionKey}`); 93 | logger.debug(`- rowKey: ${(entity as TableEntity).rowKey}`); 94 | 95 | try { 96 | const result = await this.manager.tableClientInstance.createEntity(entity as PartitionRowKeyValuePair); 97 | logger.debug(`Entity created successfully`); 98 | return AzureEntityMapper.serialize(result); 99 | } catch (error) { 100 | return this.handleRestErrors(error); 101 | } 102 | } 103 | 104 | async update(partitionKey: string, rowKey: string, entity: T): Promise { 105 | logger.debug(`Updating entity in table: ${this.tableName}`); 106 | logger.debug(`- partitionKey: ${partitionKey}`); 107 | logger.debug(`- rowKey: ${rowKey}`); 108 | 109 | if (!entity.hasOwnProperty('rowKey')) { 110 | entity['rowKey'] = rowKey; 111 | } 112 | 113 | if (!entity.hasOwnProperty('partitionKey')) { 114 | entity['partitionKey'] = partitionKey; 115 | } 116 | 117 | try { 118 | const result = await this.manager.tableClientInstance.updateEntity(entity as PartitionRowKeyValuePair); 119 | 120 | logger.debug(`Entity updated successfully`); 121 | return AzureEntityMapper.serialize(result); 122 | } catch (error) { 123 | return this.handleRestErrors(error); 124 | } 125 | } 126 | 127 | async delete(partitionKey: string, rowKey: string): Promise { 128 | logger.debug(`Deleting entity in table: ${this.tableName}`); 129 | logger.debug(`- partitionKey: ${partitionKey}`); 130 | logger.debug(`- rowKey: ${rowKey}`); 131 | 132 | const result = await this.manager.tableClientInstance.deleteEntity(partitionKey, rowKey); 133 | 134 | logger.debug(`Entity deleted successfully`); 135 | return result; 136 | } 137 | 138 | private handleRestErrors(error: Error) { 139 | // TODO: figure out how to parse odata errors 140 | if (!error.message.startsWith('{')) { 141 | throw new Error(error.message); 142 | } 143 | 144 | const err = JSON.parse(error.message); 145 | 146 | switch (err['odata.error'].code) { 147 | case 'TableAlreadyExists': 148 | logger.error(`Error creating table. Table ${this.tableName} already exists.`); 149 | break; 150 | case 'TableNotFound': 151 | logger.error(`Error creating entity. Table ${this.tableName} Not Found. Is it created?`); 152 | break; 153 | case 'ResourceNotFound': 154 | logger.error(`Error processing entity. Entity not found.`); 155 | break; 156 | case 'InvalidInput': 157 | logger.error(`Error creating entity:`); 158 | err['odata.error'].message.value.split('\n').forEach((line: string) => { 159 | logger.error(line); 160 | }); 161 | break; 162 | case 'TableBeingDeleted': 163 | logger.error(`Error creating entity. Table ${this.tableName} is being deleted. Try again later.`); 164 | break; 165 | case 'EntityAlreadyExists': 166 | logger.error(`Error creating entity. Entity with the same partitionKey and rowKey already exists.`); 167 | break; 168 | default: 169 | logger.error(error); 170 | break; 171 | } 172 | 173 | return null; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /CONTRINBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nest 2 | 3 | We would love for you to contribute to Nest and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | 14 | 15 | 17 | 18 | ## Got a Question or Problem? 19 | 20 | **Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.** You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/nestjs) where the questions should be tagged with tag `nestjs`. 21 | 22 | Stack Overflow is a much better place to ask questions since: 23 | 24 | 25 | - questions and answers stay available for public viewing so your question / answer might help someone else 26 | - Stack Overflow's voting system assures that the best answers are prominently visible. 27 | 28 | To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow. 29 | 30 | If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter]. 31 | 32 | ## Found a Bug? 33 | If you find a bug in the source code, you can help us by 34 | [submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can 35 | [submit a Pull Request](#submit-pr) with a fix. 36 | 37 | ## Missing a Feature? 38 | You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub 39 | Repository. If you would like to *implement* a new feature, please submit an issue with 40 | a proposal for your work first, to be sure that we can use it. 41 | Please consider what kind of change it is: 42 | 43 | * For a **Major Feature**, first open an issue and outline your proposal so that it can be 44 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 45 | and help you to craft the change so that it is successfully accepted into the project. For your issue name, please prefix your proposal with `[discussion]`, for example "[discussion]: your feature idea". 46 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 47 | 48 | ## Submission Guidelines 49 | 50 | ### Submitting an Issue 51 | 52 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 53 | 54 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like: 55 | 56 | - version of NestJS used 57 | - 3rd-party libraries and their versions 58 | - and most importantly - a use-case that fails 59 | 60 | 64 | 65 | 66 | 67 | Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. 68 | 69 | You can file new issues by filling out our [new issue form](https://github.com/nestjs/nest/issues/new). 70 | 71 | 72 | ### Submitting a Pull Request (PR) 73 | Before you submit your Pull Request (PR) consider the following guidelines: 74 | 75 | 1. Search [GitHub](https://github.com/nestjs/nest/pulls) for an open or closed PR 76 | that relates to your submission. You don't want to duplicate effort. 77 | 79 | 1. Fork the nestjs/nest repo. 80 | 1. Make your changes in a new git branch: 81 | 82 | ```shell 83 | git checkout -b my-fix-branch master 84 | ``` 85 | 86 | 1. Create your patch, **including appropriate test cases**. 87 | 1. Follow our [Coding Rules](#rules). 88 | 1. Run the full Nest test suite, as described in the [developer documentation][dev-doc], 89 | and ensure that all tests pass. 90 | 1. Commit your changes using a descriptive commit message that follows our 91 | [commit message conventions](#commit). Adherence to these conventions 92 | is necessary because release notes are automatically generated from these messages. 93 | 94 | ```shell 95 | git commit -a 96 | ``` 97 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 98 | 99 | 1. Push your branch to GitHub: 100 | 101 | ```shell 102 | git push origin my-fix-branch 103 | ``` 104 | 105 | 1. In GitHub, send a pull request to `nestjs:master`. 106 | * If we suggest changes then: 107 | * Make the required updates. 108 | * Re-run the Nest test suites to ensure tests are still passing. 109 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 110 | 111 | ```shell 112 | git rebase master -i 113 | git push -f 114 | ``` 115 | 116 | That's it! Thank you for your contribution! 117 | 118 | #### After your pull request is merged 119 | 120 | After your pull request is merged, you can safely delete your branch and pull the changes 121 | from the main (upstream) repository: 122 | 123 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 124 | 125 | ```shell 126 | git push origin --delete my-fix-branch 127 | ``` 128 | 129 | * Check out the master branch: 130 | 131 | ```shell 132 | git checkout master -f 133 | ``` 134 | 135 | * Delete the local branch: 136 | 137 | ```shell 138 | git branch -D my-fix-branch 139 | ``` 140 | 141 | * Update your master with the latest upstream version: 142 | 143 | ```shell 144 | git pull --ff upstream master 145 | ``` 146 | 147 | ## Coding Rules 148 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 149 | 150 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 151 | 154 | * We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at 155 | **100 characters**. An automated formatter is available, see 156 | [DEVELOPER.md](docs/DEVELOPER.md#clang-format). 157 | 158 | ## Commit Message Guidelines 159 | 160 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 161 | readable messages** that are easy to follow when looking through the **project history**. But also, 162 | we use the git commit messages to **generate the Nest change log**. 163 | 164 | ### Commit Message Format 165 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 166 | format that includes a **type**, a **scope** and a **subject**: 167 | 168 | ``` 169 | (): 170 | 171 | 172 | 173 |