Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/db-providers/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ export class Repository<T extends IndexableObject> implements IRepository<T> {
}

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}
Expand All @@ -93,7 +98,7 @@ export class Repository<T extends IndexableObject> implements IRepository<T> {
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;
Expand All @@ -109,12 +114,12 @@ export class Repository<T extends IndexableObject> implements IRepository<T> {

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)]);
Expand Down
17 changes: 12 additions & 5 deletions src/decorators/column.decorator.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -40,26 +40,33 @@ export interface ColumnMeta {
*/
export interface ColumnMetadata extends ColumnMeta {
propertyKey: string;

originalPropertyKey: string;
colNameConverter?: Converter<string>;
}

export function Column(meta: ColumnMeta, colNameConverter?: Converter<string>): 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<string>): (meta: ColumnMeta) => PropertyDecorator {
export function makeColumnDecorator(colNameConverter: Converter<string>): (meta: ColumnMeta) => PropertyDecorator {
return (meta: ColumnMeta) => {
return Column(meta, colNameConverter);
}
}

export const SnakeCaseColumn = makeColumnDecorator(snakeCase);

export function getColumnMetaForEntity(ctor: Function) {
return extractMeta<ColumnMetadata[]>(columnMetaSymbol, ctor) || [];
}
22 changes: 21 additions & 1 deletion src/models/test.entities.ts
Original file line number Diff line number Diff line change
@@ -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<TestEntity>({
Expand All @@ -22,6 +22,26 @@ export class TestEntity {
public message!: string;
}

@Entity<TestSnakeEntity>({
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<GameScore>({
keyspace: 'games',
table: 'user_scores',
Expand Down
23 changes: 17 additions & 6 deletions src/schema-gen/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
keyspace: string,
table: string,
table: string,
columnMeta: ColumnMetadata[],
tablePrimaryKeys: CandidateKeys<T>[],
mvConfig: TypedMaterializedViewConfig<T>)
{
Expand All @@ -31,7 +41,7 @@ function generateMaterializedViewSchema<T>(
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;
Expand Down Expand Up @@ -65,7 +75,7 @@ export function generateSchemaForType<T>(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 = '';
Expand All @@ -77,6 +87,7 @@ export function generateSchemaForType<T>(ctor: Function) {
generateMaterializedViewSchema(
entityMeta.keyspace,
entityMeta.table,
columnMeta,
entityMeta.partitionKeys.concat(entityMeta.clusteringKeys || []),
config
)}`
Expand Down
66 changes: 66 additions & 0 deletions src/test/repository.custom-column.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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<T>', () => {
describe('get()', () => {
let client: Client;
let repository: Repository<TestSnakeEntity>;
let testEntity: TestSnakeEntity;

beforeAll(async () => {
client = new Client({ contactPoints: ['127.0.0.1'] });
await client.connect();

repository = new Repository<TestSnakeEntity>(client, TestSnakeEntity);

const keyspace = `
CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = {
'class' : 'SimpleStrategy',
'replication_factor' : 1
};
`;

const table = generateSchemaForType<TestSnakeEntity>(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<TestSnakeEntity>(client, TestSnakeEntity);

let results = await repo.getFromPartition({
accountId: 'Contra',
id: 'WonderPanda',
solutionId: '2018',
});

if (isError<Partial<TestSnakeEntity>[], MissingPartitionKeys>(results)) {
throw new Error('Expected database results but got none');
} else {
expect(results[0]).toEqual(testEntity);
}
});
});
});
30 changes: 16 additions & 14 deletions src/test/repository.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>', () => {
describe('get()', () => {
let client: Client;
let repository: Repository<GameScore>;
let repository: Repository<TestEntity>;

beforeAll(async () => {
client = new Client({ contactPoints: ['127.0.0.1'] });
await client.connect();

repository = new Repository<GameScore>(client, GameScore);
repository = new Repository<TestEntity>(client, TestEntity);

const keyspace = `
CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = {
Expand All @@ -23,8 +24,7 @@ describe('Given a Repository<T>', () => {
};
`;

//const table = generateEntityTableSchema<GameScore>(GameScore) || 'error';
const table = '';
const table = generateSchemaForType<TestEntity>(TestEntity) || 'error';

await client.execute(keyspace);
await client.execute(table);
Expand All @@ -36,23 +36,25 @@ describe('Given a Repository<T>', () => {
})

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<GameScore>(client, GameScore);
const repo = new Repository<TestEntity>(client, TestEntity);

let results = await repo.getFromPartition({
user: 'WonderPanda'
accountId: 'Contra',
id: 'WonderPanda',
solutionId: '2018',

});
});
});
Expand Down