From acfe9d31c9cb96f0d16c2a1654bfb130efee4993 Mon Sep 17 00:00:00 2001 From: Joshua Williams Date: Sat, 23 Mar 2024 00:39:57 -0500 Subject: [PATCH] feat: add table.delete and dynamorm.deleteTables method --- __tests__/create-table.spec.ts | 46 +++++++++++++++++++ __tests__/db.spec.ts | 9 +--- __tests__/fixtures/db.ts | 2 +- __tests__/fixtures/test-table.ts | 26 +++++++++++ __tests__/jestGlobalSetup.ts | 17 +++++++ __tests__/model.spec.ts | 15 ++++--- __tests__/query.spec.ts | 76 +++++++++++++++++--------------- __tests__/table.spec.ts | 29 +----------- jest.config.js | 3 +- src/dynamorm.ts | 16 ++++++- src/table.ts | 32 ++++++++++++-- src/types.ts | 3 +- 12 files changed, 187 insertions(+), 87 deletions(-) create mode 100644 __tests__/create-table.spec.ts create mode 100644 __tests__/fixtures/test-table.ts create mode 100644 __tests__/jestGlobalSetup.ts diff --git a/__tests__/create-table.spec.ts b/__tests__/create-table.spec.ts new file mode 100644 index 0000000..fd3fedc --- /dev/null +++ b/__tests__/create-table.spec.ts @@ -0,0 +1,46 @@ +import {DynamormException} from '../src/exceptions'; +import {TestTable, db, client} from './fixtures/test-table'; + +jest.useFakeTimers() + +describe('Create / Delete Table', () => { + + describe('Dynamorm container instance', () => { + it('should create tables from container', async () => { + await expect(db.createTables).rejects.not.toThrow(DynamormException); + }) + + it('should delete tables from container', async () => { + await expect(db.deleteTables).rejects.not.toThrow(DynamormException); + }) + }) + + describe('Table instance', () => { + let table; + + beforeEach(() => { + table = new TestTable(client); + }) + + it('should create database table', async () => { + const createTable = async () => { + setTimeout(async () => await table.create(), 200) + } + expect(createTable).not.toThrow(); + }) + + it('should create database table if not exists', async () => { + const createTable = async () => { + setTimeout(async () => await table.create('IF_NOT_EXISTS'), 200) + } + expect(createTable).not.toThrow(DynamormException); + }) + + it('should delete table', async () => { + const deleteTable = async () => { + setTimeout(async () => await table.delete(), 200) + } + expect(deleteTable).not.toThrow(DynamormException); + }) + }) +}) diff --git a/__tests__/db.spec.ts b/__tests__/db.spec.ts index 9fe2fed..bc2c5c1 100644 --- a/__tests__/db.spec.ts +++ b/__tests__/db.spec.ts @@ -97,13 +97,6 @@ describe('app', () => { }) - describe('table creation', () => { - it('should create all tables', async () => { - const result = await db.createTables(); - expect(result).toBeInstanceOf(Array); - }) - }) - describe('save item', () => { it('should save item', () => { const model = db.model('AuthorEntity'); @@ -125,7 +118,7 @@ describe('app', () => { const expectedAttributes = { title: 'Southern Sweets', author: 'dev@studiowebfx.com', - image: [ 'http://images.com/logo.png' ] + image: [ 'logo.png' ] } expect(model).toBeInstanceOf(Model); expect(attributes).toMatchObject(expectedAttributes) diff --git a/__tests__/fixtures/db.ts b/__tests__/fixtures/db.ts index 0893715..273107f 100644 --- a/__tests__/fixtures/db.ts +++ b/__tests__/fixtures/db.ts @@ -11,6 +11,6 @@ const configuration = process.env.ENVIRONMENT == 'local' ? {endpoint: "http://lo tables: [AuthorTable, CookbookTable, RecipeTable], models: [CookbookModel, AuthorModel] }) -class DB {} +export class DB {} export default DynamormFactory.create(DB) diff --git a/__tests__/fixtures/test-table.ts b/__tests__/fixtures/test-table.ts new file mode 100644 index 0000000..b2297c6 --- /dev/null +++ b/__tests__/fixtures/test-table.ts @@ -0,0 +1,26 @@ +import {DynamoDBClient} from '@aws-sdk/client-dynamodb'; +import {attribute, dynamorm, DynamormFactory, Entity, table as TableDecorator, Table} from '../../index'; + +export const client = new DynamoDBClient({endpoint: 'http://localhost:8000'}); + +export class TestEntity extends Entity { + @attribute() + pk: string; + @attribute() + sk: string; +} + +@TableDecorator({ + name: 'TestTable', + primaryKey: {pk: 'pk', sk: 'sk'}, + entity: TestEntity +}) +export class TestTable extends Table {} + +@dynamorm({ + tables: [TestTable], + client +}) +class DB {} + +export const db = DynamormFactory.create(DB); diff --git a/__tests__/jestGlobalSetup.ts b/__tests__/jestGlobalSetup.ts new file mode 100644 index 0000000..1a34c33 --- /dev/null +++ b/__tests__/jestGlobalSetup.ts @@ -0,0 +1,17 @@ +import db from './fixtures/db' + +export default async function() { + await db.createTables('DROP_IF_EXISTS'); + const model = db.model('Cookbooks') + + await model.fill({ + title: "Southern Savories", + description: 'Southern Cookbook', + author: 'dev@studiowebfx.com', + image: ['logo.png'] + }).save(); + await model.set('title', 'Southern Smothered').save(); + await model.set('title', 'Southern Fried').save(); + await model.set('title', 'Southern Sweets').save(); + +} diff --git a/__tests__/model.spec.ts b/__tests__/model.spec.ts index ab380b0..369b771 100644 --- a/__tests__/model.spec.ts +++ b/__tests__/model.spec.ts @@ -4,6 +4,7 @@ import Model from "../src/model"; const config = {endpoint: 'http://localhost:8000'}; const client = new DynamoDBClient(config); + describe('model', () => { let model: any; @@ -140,7 +141,7 @@ describe('model', () => { it('should save', async () => { model.fill({ title: "Southern Savories", - author: 'com.joshua360@gmail.com', + author: 'dev@studiowebfx.com', image: ['logo.png'] }); const result = await model.save(); @@ -150,10 +151,10 @@ describe('model', () => { describe('find', () => { it('should get item by primary key', async () => { - const result = await model.find('Southern Savories','com.joshua360@gmail.com'); + const result = await model.find('Southern Savories','dev@studiowebfx.com'); expect(result).toBeInstanceOf(Model) expect(result.title).toEqual('Southern Savories') - expect(result.author).toEqual('com.joshua360@gmail.com') + expect(result.author).toEqual('dev@studiowebfx.com') }) }) @@ -161,10 +162,10 @@ describe('model', () => { it('should delete item by primary key', async () => { await model.fill({ title: 'Southern Savories', - author: 'com.joshua360@gmail.com', + author: 'dev@studiowebfx.com', image: ['image.png'] }).save(); - const result = await model.delete('Southern Savories','com.joshua360@gmail.com'); + const result = await model.delete('Southern Savories','dev@studiowebfx.com'); expect(result).toBe(true); }) }) @@ -172,10 +173,10 @@ describe('model', () => { describe('update', () => { it('should update item', async () => { model.fill({ - title: 'Southern Smothered', + title: 'Southern Crunch', author: 'dev@studiowebfx.com', description: 'Another Cookbook', - image: ["http://images.com/logo.png", "http://images.com/logo2.png"] + image: ["logo.png", "logo2.png"] }) const updateModel = async () => await model.update(); expect(updateModel).not.toThrow() diff --git a/__tests__/query.spec.ts b/__tests__/query.spec.ts index ba8adfe..d022b46 100644 --- a/__tests__/query.spec.ts +++ b/__tests__/query.spec.ts @@ -1,47 +1,18 @@ import QueryBuilder from '../src/query'; -import db from './fixtures/db'; -import {Model} from '../index'; -import {QueryException} from '../src/exceptions'; +import { DB } from './fixtures/db'; +import {Model, DynamormFactory} from '../index'; +import {DynamormException, QueryException} from '../src/exceptions'; +import {DynamormIoC} from '../src/types'; describe('Query', () => { let query: QueryBuilder; + let db: DynamormIoC; beforeEach(() => { + db = DynamormFactory.create(DB); query = new QueryBuilder(db); }) - describe('Delete', () => { - - beforeEach( async () => { - const cookbook = db.model('Cookbooks'); - await cookbook.fill({ - title: 'Haggis Cookbook', - author: 'no author', - image: ['cover.png'] - }).save(); - }); - - it('should fail to delete if sort key is not set', async () => { - const deleteCookbook = async () => { - await query - .table('Cookbooks') - .where('title', '=', 'Haggis Cookbook') - .delete() - } - - await expect(deleteCookbook).rejects.toThrowError(QueryException) - }); - - it('should delete item from table', async () => { - await query - .table('Cookbooks') - .where('title', '=', 'Haggis Cookbook') - .and('author', '=', 'no author') - .delete() - }); - - }) - describe('Select', () => { it('should select specified attributes', async () => { const model = await query @@ -80,6 +51,41 @@ describe('Query', () => { }) }) + describe('Delete', () => { + + beforeEach( async () => { + const cookbook = db.model('Cookbooks'); + await cookbook.fill({ + title: 'Haggis Cookbook', + author: 'no author', + image: ['cover.png'] + }).save(); + }); + + it('should fail to delete if sort key is not set', async () => { + const deleteCookbook = async () => { + await query + .table('Cookbooks') + .where('title', '=', 'Haggis Cookbook') + .delete() + } + + await expect(deleteCookbook).rejects.toThrowError(QueryException) + }); + + it('should delete item from table', async () => { + const deleteItem = async () => { + await query + .table('Cookbooks') + .where('title', '=', 'Haggis Cookbook') + .and('author', '=', 'no author') + .delete() + } + expect(deleteItem).not.toThrow(DynamormException) + }); + + }) + describe('Operators', () => { it('AND Logical Operator', async () => { const collection = await query diff --git a/__tests__/table.spec.ts b/__tests__/table.spec.ts index 7f36f6a..f2d98ae 100644 --- a/__tests__/table.spec.ts +++ b/__tests__/table.spec.ts @@ -1,5 +1,5 @@ import db from "./fixtures/db"; -import {attribute, Entity, Table, table as TableDecorator} from "../index"; +import { Table } from "../index"; import { CookbookTable } from "./fixtures/tables"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; @@ -35,19 +35,6 @@ describe('table', () => { }) describe('Create Table', () => { - class TestEntity extends Entity { - @attribute() - pk: string; - @attribute() - sk: string; - } - - @TableDecorator({ - name: 'TestTable', - primaryKey: {pk: 'pk', sk: 'sk'}, - entity: TestEntity - }) - class TestTable extends Table {} it('should output CreateCommandInput', () => { const expectedCommandInput = { @@ -66,17 +53,7 @@ describe('table', () => { expect(createCommandInput).toMatchObject(expectedCommandInput) }) - it('should create database table', async () => { - table = new CookbookTable(client); - const result = await table.create(); - expect(result).toHaveProperty('TableDescription'); - }) - it('should create database table if not exists', async () => { - table = new TestTable(client); - const result = await table.create('IF_NOT_EXISTS'); - expect(result).toHaveProperty('TableDescription'); - }) }) it('should getPrimaryKeyDefinition', () => { @@ -88,10 +65,6 @@ describe('table', () => { expect(expectedDefinition).toMatchObject(primaryKeyDefinition) }) - it('should create table from table instance', async () => { - const result = await table.create(); - expect(result).toHaveProperty('KeySchema'); - }) it('should get information about existing table', async () => { table = new CookbookTable(client); const tableInfo = await table.describe(); diff --git a/jest.config.js b/jest.config.js index 56992f5..e9a51d0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,5 +2,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - testMatch: [ "**/__tests__/**/*.(spec|test).ts" ] + testMatch: [ "**/__tests__/**/*.(spec|test).ts" ], + globalSetup: './__tests__/jestGlobalSetup.ts' }; diff --git a/src/dynamorm.ts b/src/dynamorm.ts index 4450329..fee3d30 100644 --- a/src/dynamorm.ts +++ b/src/dynamorm.ts @@ -5,7 +5,7 @@ import { } from "@aws-sdk/client-dynamodb"; import Model from "./model"; import {Entity} from "../index"; -import {DynamormIoC, EntityConstructor, ModelConstructor, TableConstructor} from "./types"; +import {CreateTableOption, DynamormIoC, EntityConstructor, ModelConstructor, TableConstructor} from "./types"; import QueryBuilder from './query'; /** * @todo throw error if tables or client is undefined @@ -50,16 +50,28 @@ export class DynamoRM implements DynamormIoC { } } - public async createTables(): Promise { + public async createTables(option?: CreateTableOption): Promise { const results = []; for ( let Constructor of this.tables ) { const table = new Constructor(this.client); + if (option) { + const exists = await table.exists(); + if (option == 'IF_NOT_EXISTS' && exists) continue; + if (option == 'DROP_IF_EXISTS' && exists) await table.delete(); + } const result = await table.create(Constructor) results.push(result); } return results; } + public async deleteTables() { + for (let Constructor of this.tables ) { + const table = new Constructor(this.client); + await table.delete(Constructor); + } + } + getModels(): Array { return this.models; } diff --git a/src/table.ts b/src/table.ts index 63b1921..4520e2a 100644 --- a/src/table.ts +++ b/src/table.ts @@ -1,10 +1,16 @@ import {CreateTableOption, EntityConstructor, PrimaryKey, PrimaryKeyDefinition} from "./types"; import { CreateTableCommand, - CreateTableCommandInput, CreateTableCommandOutput, DescribeTableCommand, DescribeTableCommandOutput, + CreateTableCommandInput, + CreateTableCommandOutput, DeleteTableCommand, + DeleteTableCommandInput, + DescribeTableCommand, + DescribeTableCommandOutput, DynamoDBClient, KeySchemaElement, - KeyType, ResourceInUseException, ResourceNotFoundException + KeyType, + ResourceInUseException, + ResourceNotFoundException } from "@aws-sdk/client-dynamodb"; import {DynamormException, PrimaryKeyException} from "./exceptions"; @@ -147,9 +153,16 @@ export default class Table { return Key; } - public async createIfNotExists() { - + public async exists(): Promise { + let tableInfo: DescribeTableCommandOutput; + try { + tableInfo = await this.describe(); + } catch (e) { + return true; + } + return !!tableInfo; } + public async create(option?: CreateTableOption) { if (option) { const tableDescription = await this.describe(); @@ -174,6 +187,17 @@ export default class Table { return response; } + public async delete() { + const input: DeleteTableCommandInput = { + TableName: this.getName() + } + const command: DeleteTableCommand = new DeleteTableCommand(input); + try { + await this.client.send(command); + } catch (e) { + throw new DynamormException('Failed to delete table ' + this.getName()); + } + } /** * @description Gets table formation. If table has not been created will return undefined */ diff --git a/src/types.ts b/src/types.ts index 56a6fca..2e6a9d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,7 +80,8 @@ export type ModelOptions = { } export type DynamormIoC = { - createTables: () => Promise, + createTables: (options?: CreateTableOption) => Promise, + deleteTables: () => Promise, getClient: () => DynamoDBClient, getTable: (tableName: string) => TableConstructor, getTables: () => TableConstructor[],