diff --git a/src/db-providers/repository.ts b/src/db-providers/repository.ts index 9b48646..3dc1a86 100644 --- a/src/db-providers/repository.ts +++ b/src/db-providers/repository.ts @@ -71,10 +71,15 @@ export class Repository implements IRepository { } const whereClause = keyMap.map((x, i) => { + const colMeta = this.columnMeta.find(y => y.originalPropertyKey === x.key); + if(!colMeta) { + throw new Error('colmeta does not exist'); + } + return i > 0 - ? ` AND ${x.key} = ?` - : `${x.key} = ?` - }).join(''); /* ? */ + ? ` AND ${colMeta.propertyKey} = ?` + : `${colMeta.propertyKey} = ?` + }).join(''); const queryText = normalizeQueryText(` SELECT * FROM ${this.metadata.keyspace}.${this.metadata.table} @@ -93,7 +98,7 @@ export class Repository implements IRepository { columnMeta.forEach(meta => { // TODO: Need to run these through proper deserializers based on their underlying type // Ie. timeuuid will need to be converted back to a Date etc - result[meta.propertyKey] = row[meta.propertyKey.toLowerCase()] + result[meta.originalPropertyKey] = row[meta.propertyKey.toLowerCase()] }); return result; @@ -109,12 +114,12 @@ export class Repository implements IRepository { for (let prop in entity) { // get the colType for this property - let columnMeta = this.columnMeta.find(x => x.propertyKey === prop); + let columnMeta = this.columnMeta.find(x => x.originalPropertyKey === prop); if (!columnMeta) { throw new Error(`Missing column meta for key: ${prop}`); } - serializedEntity[prop] = serialize(columnMeta.colType, entity[prop], columnMeta.dataType); + serializedEntity[columnMeta.propertyKey] = serialize(columnMeta.colType, entity[prop], columnMeta.dataType); } await this.client.execute(query, [JSON.stringify(serializedEntity)]); diff --git a/src/decorators/column.decorator.ts b/src/decorators/column.decorator.ts index ef7a1e7..62e46d8 100644 --- a/src/decorators/column.decorator.ts +++ b/src/decorators/column.decorator.ts @@ -1,4 +1,4 @@ -import { extend } from '../core/utils'; +import { extend, snakeCase } from '../core/utils'; import { extractMeta, setMeta } from '../core/reflection'; import { DataType, Converter } from '../core/domain'; @@ -40,26 +40,33 @@ export interface ColumnMeta { */ export interface ColumnMetadata extends ColumnMeta { propertyKey: string; - + originalPropertyKey: string; + colNameConverter?: Converter; } export function Column(meta: ColumnMeta, colNameConverter?: Converter): PropertyDecorator { return (target, propertyKey) => { - propertyKey = propertyKey.toString(); + const newPropertyKey = colNameConverter ? colNameConverter(propertyKey.toString()) : propertyKey.toString(); const columnMeta = getColumnMetaForEntity(target.constructor) || []; setMeta(columnMetaSymbol, columnMeta.concat( - extend(meta, { propertyKey })), target.constructor); + extend(meta, { + propertyKey: newPropertyKey, + colNameConverter, + originalPropertyKey: propertyKey.toString() + })), target.constructor); } } -function makeColumnDecorator(colNameConverter: Converter): (meta: ColumnMeta) => PropertyDecorator { +export function makeColumnDecorator(colNameConverter: Converter): (meta: ColumnMeta) => PropertyDecorator { return (meta: ColumnMeta) => { return Column(meta, colNameConverter); } } +export const SnakeCaseColumn = makeColumnDecorator(snakeCase); + export function getColumnMetaForEntity(ctor: Function) { return extractMeta(columnMetaSymbol, ctor) || []; } diff --git a/src/models/test.entities.ts b/src/models/test.entities.ts index 189733b..c02d96c 100644 --- a/src/models/test.entities.ts +++ b/src/models/test.entities.ts @@ -1,5 +1,5 @@ import { Entity } from "../decorators/entity.decorator"; -import { Column } from "../decorators/column.decorator"; +import { Column, SnakeCaseColumn } from "../decorators/column.decorator"; import { types } from 'cassandra-driver'; @Entity({ @@ -22,6 +22,26 @@ export class TestEntity { public message!: string; } +@Entity({ + keyspace: 'test', + table: 'complex_things', + partitionKeys: ['accountId', 'solutionId', 'id'], + clusteringKeys: [] +}) +export class TestSnakeEntity { + @SnakeCaseColumn({ colType: 'text' }) + public accountId!: string; + + @SnakeCaseColumn({ colType: 'text' }) + public solutionId!: string; + + @SnakeCaseColumn({ colType: 'text' }) + public id!: string; + + @SnakeCaseColumn({ colType: 'text' }) + public message!: string; +} + @Entity({ keyspace: 'games', table: 'user_scores', diff --git a/src/schema-gen/generator.ts b/src/schema-gen/generator.ts index 0188929..18f429f 100644 --- a/src/schema-gen/generator.ts +++ b/src/schema-gen/generator.ts @@ -6,17 +6,27 @@ import { GameScore } from "../models/test.entities"; import { writeFile } from 'async-file'; import * as path from 'path'; -function generatePrimaryKey(partitionKeys: string[], clusteringKeys: string[]) { +function generatePrimaryKey(columnMeta: ColumnMetadata[], partitionKeys: string[], clusteringKeys: string[]) { + // console.log(columnMeta); const clusteringKeysText = clusteringKeys.length ? `, ${commaSeparatedSpacedString(clusteringKeys)}` - : ''; + : ''; + + const transformedKeys = partitionKeys.map(pKey => { + const meta = columnMeta.find(meta => meta.originalPropertyKey === pKey); + if(!meta) { + throw new Error('Unexpected Property Key'); + } + return meta.propertyKey; + }); - return `PRIMARY KEY ((${partitionKeys.join(', ')})${clusteringKeysText})` + return `PRIMARY KEY ((${transformedKeys.join(', ')})${clusteringKeysText})` } function generateMaterializedViewSchema( keyspace: string, - table: string, + table: string, + columnMeta: ColumnMetadata[], tablePrimaryKeys: CandidateKeys[], mvConfig: TypedMaterializedViewConfig) { @@ -31,7 +41,7 @@ function generateMaterializedViewSchema( CREATE MATERIALIZED VIEW ${keyspace}.${mvConfig.name} AS SELECT ${selectColumnsText} FROM ${table} WHERE ${injectAllButLastString(primaryKeysWhere, ' AND ')} - ${generatePrimaryKey(mvConfig.partitionKeys, clusteringKeys)}; + ${generatePrimaryKey(columnMeta, mvConfig.partitionKeys, clusteringKeys)}; `; return mvSchema; @@ -65,7 +75,7 @@ export function generateSchemaForType(ctor: Function) { const tableSchema = `CREATE TABLE IF NOT EXISTS ${entityMeta.keyspace}.${entityMeta.table} ( ${columnPropsText.join(' ')} - ${generatePrimaryKey(entityMeta.partitionKeys, entityMeta.clusteringKeys || [])} + ${generatePrimaryKey(columnMeta, entityMeta.partitionKeys, entityMeta.clusteringKeys || [])} );`; let mvSchema = ''; @@ -77,6 +87,7 @@ export function generateSchemaForType(ctor: Function) { generateMaterializedViewSchema( entityMeta.keyspace, entityMeta.table, + columnMeta, entityMeta.partitionKeys.concat(entityMeta.clusteringKeys || []), config )}` diff --git a/src/test/repository.custom-column.e2e-spec.ts b/src/test/repository.custom-column.e2e-spec.ts new file mode 100644 index 0000000..b586944 --- /dev/null +++ b/src/test/repository.custom-column.e2e-spec.ts @@ -0,0 +1,66 @@ +import 'reflect-metadata'; +import { Repository, MissingPartitionKeys, normalizeQueryText } from '../db-providers/repository'; +import { isError } from 'ts-errorflow'; +import { Entity } from '../decorators/entity.decorator'; +import { Client } from 'cassandra-driver'; +import { TestSnakeEntity } from '../models/test.entities'; +import { generateSchemaForType } from '../schema-gen/generator'; + +describe('Given a Repository', () => { + describe('get()', () => { + let client: Client; + let repository: Repository; + let testEntity: TestSnakeEntity; + + beforeAll(async () => { + client = new Client({ contactPoints: ['127.0.0.1'] }); + await client.connect(); + + repository = new Repository(client, TestSnakeEntity); + + const keyspace = ` + CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = { + 'class' : 'SimpleStrategy', + 'replication_factor' : 1 + }; + `; + + const table = generateSchemaForType(TestSnakeEntity) || 'error'; + // console.log(table); + await client.execute(keyspace); + await client.execute(table); + + testEntity = new TestSnakeEntity(); + testEntity.accountId = 'Contra'; + testEntity.id = 'WonderPanda'; + testEntity.message = '9001'; + testEntity.solutionId = '2018'; + }); + + afterAll(async () => { + await client.execute('DROP KEYSPACE test'); + client.shutdown(); + }) + + it ('should insert the entity', async () => { + await repository.insert(testEntity); + }); + + + it('should execute the correct query for retrieving one or more entities by partition key', async () => { + const repo = new Repository(client, TestSnakeEntity); + + let results = await repo.getFromPartition({ + accountId: 'Contra', + id: 'WonderPanda', + solutionId: '2018', + }); + + if (isError[], MissingPartitionKeys>(results)) { + throw new Error('Expected database results but got none'); + } else { + expect(results[0]).toEqual(testEntity); + } + }); + }); +}); \ No newline at end of file diff --git a/src/test/repository.e2e-spec.ts b/src/test/repository.e2e-spec.ts index 31a02c1..56814c9 100644 --- a/src/test/repository.e2e-spec.ts +++ b/src/test/repository.e2e-spec.ts @@ -3,18 +3,19 @@ import { Repository, MissingPartitionKeys, normalizeQueryText } from '../db-prov import { isError } from 'ts-errorflow'; import { Entity } from '../decorators/entity.decorator'; import { Client } from 'cassandra-driver'; -import { GameScore } from '../models/test.entities'; +import { TestEntity } from '../models/test.entities'; +import { generateSchemaForType } from '../schema-gen/generator'; describe('Given a Repository', () => { describe('get()', () => { let client: Client; - let repository: Repository; + let repository: Repository; beforeAll(async () => { client = new Client({ contactPoints: ['127.0.0.1'] }); await client.connect(); - repository = new Repository(client, GameScore); + repository = new Repository(client, TestEntity); const keyspace = ` CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = { @@ -23,8 +24,7 @@ describe('Given a Repository', () => { }; `; - //const table = generateEntityTableSchema(GameScore) || 'error'; - const table = ''; + const table = generateSchemaForType(TestEntity) || 'error'; await client.execute(keyspace); await client.execute(table); @@ -36,23 +36,25 @@ describe('Given a Repository', () => { }) it ('should insert the entity', async () => { - const entity = new GameScore(); - entity.gameTitle = 'Contra'; - entity.user = 'WonderPanda'; - entity.score = 9001; - entity.year = 2018; - entity.month = 10; - entity.day = 19; + const entity = new TestEntity(); + entity.accountId = 'Contra'; + entity.id = 'WonderPanda'; + entity.message = '9001'; + entity.solutionId = '2018'; + await repository.insert(entity); }); it('should execute the correct query for retrieving one or more entities by partition key', async () => { - const repo = new Repository(client, GameScore); + const repo = new Repository(client, TestEntity); let results = await repo.getFromPartition({ - user: 'WonderPanda' + accountId: 'Contra', + id: 'WonderPanda', + solutionId: '2018', + }); }); });