Skip to content

Commit

Permalink
Release v7.1.0
Browse files Browse the repository at this point in the history
* Fix `select` typings for populate() calls
* Changed `Entity` to be an abstract class rather than an interface

NOTE: This is a pretty big breaking change, but v7.0.0 was less than 24h old and was broken, so leaving this as a minor version change.
  • Loading branch information
jgeurts committed Feb 25, 2021
1 parent 024e9a7 commit a6a6018
Show file tree
Hide file tree
Showing 43 changed files with 745 additions and 1,395 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### 7.1.0
* Fix `select` typings for populate() calls
* Changed `Entity` to be an abstract class rather than an interface

NOTE: This is a pretty big breaking change, but v7.0.0 was less than 24h old and was broken, so leaving this as a
minor version change.

### 7.0.0
* Add generic types to select and where. #72 Thanks @krislefeber!
* Add debug environment variable to print sql to console. #73 Thanks @krislefeber!
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ $ npm install pg postgres-pool bigal

#### Defining database models

Model definitions need to extend `Entity`.

```typescript
import { Entity } from 'bigal';
import { column, primaryColumn, table } from 'bigal/decorators';
Expand All @@ -37,7 +39,7 @@ import { ProductCategory } from './ProductCategory';
@table({
name: 'products',
})
export class Product implements Entity {
export class Product extends Entity {
@primaryColumn({ type: 'integer' })
public id!: number;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bigal",
"version": "7.0.0",
"version": "7.1.0",
"description": "A fast and lightweight orm for postgres and node.js, written in typescript.",
"main": "index.js",
"types": "index.d.ts",
Expand Down
7 changes: 5 additions & 2 deletions src/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export type EntityFieldValue = boolean[] | Date | number[] | Record<string, unknown> | string[] | boolean | number | string | unknown | null;

// NOTE: Using declaration merging so that IsValueOfType can identify classes that extend Entity, while
// not having __bigAlEntity carry over/transpile to subclassed objects
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export abstract class Entity {}
export interface Entity {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[index: string]: any;
__bigAlEntity: true;
}

export interface EntityStatic<T extends Entity> {
Expand Down
3 changes: 2 additions & 1 deletion src/IReadonlyRepository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Entity } from './Entity';
import type { ModelMetadata } from './metadata';
import type { CountResult, FindArgs, FindOneArgs, FindOneResult, FindResult, WhereQuery } from './query';

export interface IReadonlyRepository<T> {
export interface IReadonlyRepository<T extends Entity> {
readonly model: ModelMetadata<T>;

/**
Expand Down
33 changes: 17 additions & 16 deletions src/ReadonlyRepository.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import _ from 'lodash';
import type { Pool } from 'postgres-pool';

import type { EntityFieldValue, EntityStatic } from './Entity';
import type { Entity, EntityFieldValue, EntityStatic } from './Entity';
import type { IReadonlyRepository } from './IReadonlyRepository';
import type { IRepository } from './IRepository';
import type { ColumnCollectionMetadata, ColumnModelMetadata, ColumnTypeMetadata, ModelMetadata } from './metadata';
import type { CountResult, FindArgs, FindOneArgs, FindOneResult, FindResult, OrderBy, PaginateOptions, PopulateArgs, Sort, WhereQuery, SortObject } from './query';
import { getCountQueryAndParams, getSelectQueryAndParams } from './SqlHelper';
import type { GetPropertyType } from './types/GetPropertyType';
import type { GetValueType, PickByValueType } from './types';

export interface IRepositoryOptions<T> {
export interface IRepositoryOptions<T extends Entity> {
modelMetadata: ModelMetadata<T>;
type: EntityStatic<T>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
pool: Pool;
readonlyPool?: Pool;
}

export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository<T> {
private readonly _modelMetadata: ModelMetadata<T>;

protected _type: EntityStatic<T>;
Expand All @@ -26,7 +26,7 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {

protected _readonlyPool: Pool;

protected _repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
protected _repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;

protected _floatProperties: string[] = [];

Expand Down Expand Up @@ -94,9 +94,9 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {

interface Populates {
propertyName: string;
where?: WhereQuery<unknown>;
where?: WhereQuery<Entity>;
select?: string[];
sort?: SortObject<unknown> | string;
sort?: SortObject<Entity> | string;
skip?: number;
limit?: number;
}
Expand Down Expand Up @@ -127,15 +127,15 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
* @param {string|number} [options.skip] - Number of records to skip
* @param {string|number} [options.limit] - Number of results to return
*/
populate<TProperty extends string & keyof T>(
populate<TProperty extends string & keyof PickByValueType<T, Entity>>(
propertyName: TProperty,
{
where: populateWhere, //
select: populateSelect,
sort: populateSort,
skip: populateSkip,
limit: populateLimit,
}: PopulateArgs<GetPropertyType<T, TProperty>> = {},
}: PopulateArgs<GetValueType<PickByValueType<T, Entity>[TProperty], Entity>> = {},
): FindOneResult<T> {
populates.push({
propertyName,
Expand Down Expand Up @@ -207,7 +207,7 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
select: populate.select,
where: populateWhere,
sort: populate.sort,
} as FindOneArgs<T>);
} as FindOneArgs<Entity>);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring result does not have index signature for known field (populate.propertyName)
Expand Down Expand Up @@ -238,7 +238,8 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
}

if (collectionColumn.through) {
const throughRepository = modelInstance._repositoriesByModelNameLowered[collectionColumn.through.toLowerCase()];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const throughRepository = modelInstance._repositoriesByModelNameLowered[collectionColumn.through.toLowerCase()] as IReadonlyRepository<any>;
if (!throughRepository) {
throw new Error(`Unable to find repository for multi-map collection: ${collectionColumn.through}. From ${column.target}#${populate.propertyName}`);
}
Expand All @@ -260,7 +261,7 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
(async function populateMultiMulti(): Promise<void> {
if (relatedModelColumn) {
const mapRecords = await throughRepository.find({
select: [relatedModelColumn.via] as (string & keyof T)[],
select: [relatedModelColumn.via],
where: {
[collectionColumn.via]: id,
} as WhereQuery<T>,
Expand All @@ -280,7 +281,7 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
sort: populate.sort,
skip: populate.skip,
limit: populate.limit,
} as FindArgs<T>);
} as FindArgs<Entity>);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring result does not have index signature for known field (populate.propertyName)
Expand All @@ -302,7 +303,7 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
sort: populate.sort,
skip: populate.skip,
limit: populate.limit,
} as FindArgs<T>);
} as FindArgs<Entity>);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring result does not have index signature for known field (populate.propertyName)
Expand Down Expand Up @@ -602,7 +603,7 @@ export class ReadonlyRepository<T> implements IReadonlyRepository<T> {
} else if (_.isObject(sorts)) {
for (const [propertyName, order] of Object.entries(sorts)) {
let descending = false;
if (order === -1 || order === '-1' || /desc/i.test(`${order as string}`)) {
if (order === -1 || /desc/i.test(`${order}`)) {
descending = true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class Repository<T extends Entity> extends ReadonlyRepository<T> implemen
}

let returnRecords = true;
let returnSelect: string[] | undefined;
let returnSelect: (string & keyof T)[] | undefined;
if (options) {
if ((options as DoNotReturnRecords).returnRecords === false) {
returnRecords = false;
Expand Down
61 changes: 32 additions & 29 deletions src/SqlHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ export function getSelectQueryAndParams<T extends Entity>({
skip,
limit,
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
select?: string[];
select?: (string & keyof T)[];
where?: WhereQuery<T>;
sorts: OrderBy<T>[];
skip: number;
Expand Down Expand Up @@ -125,7 +125,7 @@ export function getCountQueryAndParams<T extends Entity>({
model,
where,
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
where?: WhereQuery<T>;
}): QueryAndParams {
Expand Down Expand Up @@ -164,11 +164,11 @@ export function getInsertQueryAndParams<T extends Entity>({
returnRecords = true,
returnSelect,
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
values: Partial<Entity> | Partial<Entity>[];
values: Partial<T> | Partial<T>[];
returnRecords?: boolean;
returnSelect?: Extract<keyof Entity, string>[];
returnSelect?: (string & keyof T)[];
}): QueryAndParams {
const entitiesToInsert = _.isArray(values) ? values : [values];
const columnsToInsert = [];
Expand All @@ -194,11 +194,13 @@ export function getInsertQueryAndParams<T extends Entity>({
let includePropertyName = false;
for (const entity of entitiesToInsert) {
// If there is a default value for the property and it is not defined, use the default
if (hasDefaultValue && _.isUndefined(entity[column.propertyName])) {
entity[column.propertyName] = defaultValue;
if (hasDefaultValue && _.isUndefined(entity[column.propertyName as string & keyof T])) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - string is not assignable to T[string & keyof T] | undefined
entity[column.propertyName as string & keyof T] = defaultValue;
}

if (_.isUndefined(entity[column.propertyName])) {
if (_.isUndefined(entity[column.propertyName as string & keyof T])) {
if (column.required) {
throw new Error(`Create statement for "${model.name}" is missing value for required field: ${column.propertyName}`);
}
Expand All @@ -225,7 +227,7 @@ export function getInsertQueryAndParams<T extends Entity>({

for (const [entityIndex, entity] of entitiesToInsert.entries()) {
let value;
const entityValue = entity[column.propertyName] as EntityFieldValue;
const entityValue = entity[column.propertyName as string & keyof T] as EntityFieldValue;
if (_.isNil(entityValue)) {
value = 'NULL';
} else {
Expand All @@ -243,7 +245,7 @@ export function getInsertQueryAndParams<T extends Entity>({
throw new Error(`Unable to find primary key column for ${relatedModelName} when inserting ${model.name}.${column.propertyName} value.`);
}

const primaryKeyValue = (entityValue as Partial<Entity>)[relatedModelPrimaryKey.propertyName] as EntityFieldValue;
const primaryKeyValue = (entityValue as Partial<T>)[relatedModelPrimaryKey.propertyName as string & keyof T] as EntityFieldValue;
if (_.isNil(primaryKeyValue)) {
throw new Error(`Undefined primary key value for hydrated object value for "${column.propertyName}" on "${model.name}"`);
}
Expand Down Expand Up @@ -309,17 +311,18 @@ export function getUpdateQueryAndParams<T extends Entity>({
returnRecords = true,
returnSelect,
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
where: WhereQuery<T>;
values: Partial<Entity>;
values: Partial<T>;
returnRecords?: boolean;
returnSelect?: Extract<keyof Entity, string>[];
returnSelect?: (string & keyof T)[];
}): QueryAndParams {
for (const column of model.updateDateColumns) {
if (_.isUndefined(values[column.propertyName])) {
// eslint-disable-next-line no-param-reassign
values[column.propertyName] = new Date();
if (_.isUndefined(values[column.propertyName as string & keyof T])) {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/ban-ts-comment
// @ts-ignore - Date is not assignable to T[string & keyof T]
values[column.propertyName as string & keyof T] = new Date();
}
}

Expand Down Expand Up @@ -351,7 +354,7 @@ export function getUpdateQueryAndParams<T extends Entity>({
throw new Error(`Unable to find primary key column for ${relatedModelName} when inserting ${model.name}.${column.propertyName} value.`);
}

const primaryKeyValue = (value as Partial<Entity>)[relatedModelPrimaryKey.propertyName] as EntityFieldValue;
const primaryKeyValue = (value as Partial<T>)[relatedModelPrimaryKey.propertyName as string & keyof T] as EntityFieldValue;
if (_.isNil(primaryKeyValue)) {
throw new Error(`Undefined primary key value for hydrated object value for "${column.propertyName}" on "${model.name}"`);
}
Expand All @@ -376,7 +379,7 @@ export function getUpdateQueryAndParams<T extends Entity>({
}

for (const column of model.versionColumns) {
if (!_.isUndefined(values[column.propertyName])) {
if (!_.isUndefined(values[column.propertyName as string & keyof T])) {
if (!isFirstProperty) {
query += ',';
}
Expand Down Expand Up @@ -429,11 +432,11 @@ export function getDeleteQueryAndParams<T extends Entity>({
returnRecords = true,
returnSelect,
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
where?: WhereQuery<T>;
returnRecords?: boolean;
returnSelect?: Extract<keyof Entity, string>[];
returnSelect?: (string & keyof T)[];
}): QueryAndParams {
let query = `DELETE FROM "${model.tableName}"`;

Expand Down Expand Up @@ -469,20 +472,20 @@ export function getDeleteQueryAndParams<T extends Entity>({
* @returns {string} SQL columns
* @private
*/
export function getColumnsToSelect<T extends Entity>({ model, select }: { model: ModelMetadata<T>; select?: Extract<keyof Entity, string>[] }): string {
export function getColumnsToSelect<T extends Entity>({ model, select }: { model: ModelMetadata<T>; select?: (string & keyof T)[] }): string {
if (select) {
const { primaryKeyColumn } = model;

// Include primary key column if it's not defined
if (primaryKeyColumn && !select.includes(primaryKeyColumn.propertyName)) {
select.push(primaryKeyColumn.propertyName);
if (primaryKeyColumn && !select.includes(primaryKeyColumn.propertyName as string & keyof T)) {
select.push(primaryKeyColumn.propertyName as string & keyof T);
}
} else {
// eslint-disable-next-line no-param-reassign
select = [];
for (const column of model.columns) {
if (!(column as ColumnCollectionMetadata).collection) {
select.push(column.propertyName);
select.push(column.propertyName as string & keyof T);
}
}
}
Expand Down Expand Up @@ -524,7 +527,7 @@ export function buildWhereStatement<T extends Entity>({
where,
params = [],
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
where?: WhereQuery<T>;
params?: unknown[];
Expand Down Expand Up @@ -611,7 +614,7 @@ function buildWhere<T extends Entity>({
value,
params = [],
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
propertyName?: string;
comparer?: Comparer | string;
Expand Down Expand Up @@ -768,7 +771,7 @@ function buildWhere<T extends Entity>({
throw new Error(`Unable to find primary key column for ${column.model} specified in where clause for ${model.name}.${column.propertyName}`);
}

const primaryKeyValue = (value as Partial<Entity>)[relatedModelPrimaryKey.propertyName] as EntityFieldValue;
const primaryKeyValue = (value as Partial<T>)[relatedModelPrimaryKey.propertyName as string & keyof T] as EntityFieldValue;
if (!_.isNil(primaryKeyValue)) {
// Treat `value` as a hydrated object
return buildWhere({
Expand Down Expand Up @@ -953,7 +956,7 @@ function buildOrOperatorStatement<T extends Entity>({
value,
params = [],
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<T> | IRepository<T>>;
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
isNegated: boolean;
value: number[] | string[];
Expand Down
3 changes: 2 additions & 1 deletion src/query/CountResult.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ChainablePromiseLike } from '../ChainablePromiseLike';
import type { Entity } from '../Entity';

import type { WhereQuery } from './WhereQuery';

export interface CountResult<TEntity> extends ChainablePromiseLike<number> {
export interface CountResult<TEntity extends Entity> extends ChainablePromiseLike<number> {
where(args: WhereQuery<TEntity>): CountResult<TEntity> | number;
}
4 changes: 3 additions & 1 deletion src/query/CreateUpdateOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Entity } from '../Entity';

import type { DoNotReturnRecords } from './DoNotReturnRecords';
import type { ReturnSelect } from './ReturnSelect';

export type CreateUpdateOptions<T> = DoNotReturnRecords | ReturnSelect<T>;
export type CreateUpdateOptions<T extends Entity> = DoNotReturnRecords | ReturnSelect<T>;
Loading

0 comments on commit a6a6018

Please sign in to comment.