Skip to content

Commit

Permalink
Release 9.2.2
Browse files Browse the repository at this point in the history
- Fix to always include populate column names in select query
- Use readonly arrays where possible when building sql
- Update npms
  • Loading branch information
jgeurts committed Dec 27, 2021
1 parent 76abb20 commit 2143315
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 341 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## 9.2.2 - 2021-12-27

- Fix to always include populate column names in select query
- Use readonly arrays where possible when building sql
- Update npms

## 9.2.1 - 2021-11-16

- Update npms
Expand Down
496 changes: 195 additions & 301 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 18 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bigal",
"version": "9.2.1",
"version": "9.2.2",
"description": "A fast and lightweight orm for postgres and node.js, written in typescript.",
"main": "index.js",
"types": "index.d.ts",
Expand All @@ -19,44 +19,46 @@
"node": ">=12"
},
"dependencies": {
"@types/lodash": "^4.14.177",
"@types/node": "^16.11.7",
"@types/pg": "^8.6.1",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.5",
"@types/pg": "^8.6.3",
"lodash": "^4.17.21",
"pg": "8.7.1",
"postgres-pool": "^5.0.8"
},
"devDependencies": {
"@types/chai": "^4.2.22",
"@types/chai": "^4.3.0",
"@types/faker": "^5.5.9",
"@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"chai": "^4.3.4",
"eslint": "^8.2.0",
"eslint": "^8.5.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^15.0.0",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsdoc": "^37.0.3",
"eslint-plugin-mocha": "^9.0.0",
"eslint-plugin-jsdoc": "^37.4.0",
"eslint-plugin-mocha": "^10.0.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-security": "^1.4.0",
"faker": "^5.5.3",
"husky": "^7.0.4",
"lint-staged": "^12.0.2",
"markdownlint-cli": "^0.29.0",
"lint-staged": "^12.1.4",
"markdownlint-cli": "^0.30.0",
"mocha": "^9.1.3",
"npm-run-all": "^4.1.5",
"pinst": "^2.1.6",
"prettier": "^2.4.1",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"strict-event-emitter-types": "^2.0.0",
"ts-mockito": "^2.6.1",
"ts-node": "^10.4.0",
"typescript": "^4.4.4"
"typescript": "^4.5.4"
},
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"test": "mocha -r ts-node/register tests/**/*.tests.ts",
"lint:markdown": "prettier --write '*.md' '!(node_modules|dist)/**/*.md' && markdownlint '*.md' '!(node_modules|dist)/**/*.md' --config=.github/linters/.markdown-lint.yml --fix",
Expand Down
36 changes: 30 additions & 6 deletions src/ReadonlyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository
public findOne(args: FindOneArgs<T> | WhereQuery<T> = {}): FindOneResult<T, QueryResult<T>> {
const { stack } = new Error(`${this.model.name}.findOne()`);

let select: (string & keyof OmitFunctionsAndEntityCollections<T>)[] | undefined;
let select: Set<string & keyof OmitFunctionsAndEntityCollections<T>> | undefined;
let where: WhereQuery<T> = {};
let sort: SortObject<T> | string | null = null;
let poolOverride: Pool | undefined;
Expand All @@ -84,7 +84,10 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository

switch (name) {
case 'select':
select = value as (string & keyof OmitFunctionsAndEntityCollections<T>)[];
if (value) {
select = new Set(value as (string & keyof OmitFunctionsAndEntityCollections<T>)[]);
}

break;
case 'where':
where = value as WhereQuery<T>;
Expand Down Expand Up @@ -145,6 +148,15 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository
propertyName: TProperty,
options?: PopulateArgs<GetValueType<PickByValueType<T, Entity>[TProperty], Entity>>,
): FindOneResult<T, Omit<QueryResult<T>, TProperty> & PickAsPopulated<T, TProperty>> {
// Add the column if the property is a single relation and not included in the list of select columns
if (select && !select.has(propertyName as unknown as string & keyof OmitFunctionsAndEntityCollections<T>)) {
for (const column of modelInstance.model.columns) {
if ((column as ColumnModelMetadata).model && column.propertyName === propertyName) {
select.add(column.propertyName as string & keyof OmitFunctionsAndEntityCollections<T>);
}
}
}

populates.push({
propertyName,
where: options?.where,
Expand Down Expand Up @@ -193,7 +205,7 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository
const { query, params } = getSelectQueryAndParams({
repositoriesByModelNameLowered: modelInstance._repositoriesByModelNameLowered,
model: modelInstance.model,
select,
select: select ? Array.from(select) : undefined,
where,
sorts,
limit: 1,
Expand Down Expand Up @@ -247,7 +259,7 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository
public find(args: FindArgs<T> | WhereQuery<T> = {}): FindResult<T, QueryResult<T>> {
const { stack } = new Error(`${this.model.name}.find()`);

let select: (string & keyof OmitFunctionsAndEntityCollections<T>)[] | undefined;
let select: Set<string & keyof OmitFunctionsAndEntityCollections<T>> | undefined;
let where: WhereQuery<T> = {};
let sort: SortObject<T> | string | null = null;
let skip: number | null = null;
Expand All @@ -259,7 +271,10 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository

switch (name) {
case 'select':
select = value as (string & keyof OmitFunctionsAndEntityCollections<T>)[];
if (value) {
select = new Set(value as (string & keyof OmitFunctionsAndEntityCollections<T>)[]);
}

break;
case 'where':
where = value as WhereQuery<T>;
Expand Down Expand Up @@ -321,6 +336,15 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository
propertyName: TProperty,
options?: PopulateArgs<GetValueType<PickByValueType<T, Entity>[TProperty], Entity>>,
): FindResult<T, Omit<QueryResult<T>, TProperty> & PickAsPopulated<T, TProperty>> {
// Add the column if the property is a single relation and not included in the list of select columns
if (select && !select.has(propertyName as unknown as string & keyof OmitFunctionsAndEntityCollections<T>)) {
for (const column of modelInstance.model.columns) {
if ((column as ColumnModelMetadata).model && column.propertyName === propertyName) {
select.add(column.propertyName as string & keyof OmitFunctionsAndEntityCollections<T>);
}
}
}

populates.push({
propertyName,
where: options?.where,
Expand Down Expand Up @@ -387,7 +411,7 @@ export class ReadonlyRepository<T extends Entity> implements IReadonlyRepository
const { query, params } = getSelectQueryAndParams({
repositoriesByModelNameLowered: modelInstance._repositoriesByModelNameLowered,
model: modelInstance.model,
select,
select: select ? Array.from(select) : undefined,
where,
sorts,
skip: skip || 0,
Expand Down
35 changes: 19 additions & 16 deletions src/SqlHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { CreateUpdateParams, OmitFunctionsAndEntityCollections } from './ty

interface QueryAndParams {
query: string;
params: unknown[];
params: readonly unknown[];
}

/* eslint-disable @typescript-eslint/no-use-before-define */
Expand Down Expand Up @@ -42,9 +42,9 @@ export function getSelectQueryAndParams<T extends Entity>({
}: {
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
select?: (string & keyof OmitFunctionsAndEntityCollections<T>)[];
select?: readonly (string & keyof OmitFunctionsAndEntityCollections<T>)[];
where?: WhereQuery<T>;
sorts: OrderBy<T>[];
sorts: readonly OrderBy<T>[];
skip: number;
limit: number;
}): QueryAndParams {
Expand Down Expand Up @@ -169,7 +169,7 @@ export function getInsertQueryAndParams<T extends Entity>({
model: ModelMetadata<T>;
values: CreateUpdateParams<T> | CreateUpdateParams<T>[];
returnRecords?: boolean;
returnSelect?: (string & keyof OmitFunctionsAndEntityCollections<T>)[];
returnSelect?: readonly (string & keyof OmitFunctionsAndEntityCollections<T>)[];
}): QueryAndParams {
const entitiesToInsert = _.isArray(values) ? values : [values];
const columnsToInsert = [];
Expand Down Expand Up @@ -437,7 +437,7 @@ export function getDeleteQueryAndParams<T extends Entity>({
model: ModelMetadata<T>;
where?: WhereQuery<T>;
returnRecords?: boolean;
returnSelect?: (string & keyof OmitFunctionsAndEntityCollections<T>)[];
returnSelect?: readonly (string & keyof OmitFunctionsAndEntityCollections<T>)[];
}): QueryAndParams {
let query = `DELETE FROM "${model.tableName}"`;

Expand Down Expand Up @@ -473,26 +473,29 @@ export function getDeleteQueryAndParams<T extends Entity>({
* @returns {string} SQL columns
* @private
*/
export function getColumnsToSelect<T extends Entity>({ model, select }: { model: ModelMetadata<T>; select?: (string & keyof OmitFunctionsAndEntityCollections<T>)[] }): string {
export function getColumnsToSelect<T extends Entity>({ model, select }: { model: ModelMetadata<T>; select?: readonly (string & keyof OmitFunctionsAndEntityCollections<T>)[] }): string {
let selectColumns: Set<string>;
if (select) {
const { primaryKeyColumn } = model;

// Include primary key column if it's not defined
if (primaryKeyColumn && !select.includes(primaryKeyColumn.propertyName as string & keyof OmitFunctionsAndEntityCollections<T>)) {
select.push(primaryKeyColumn.propertyName as string & keyof OmitFunctionsAndEntityCollections<T>);
selectColumns = new Set(select);

// Ensure primary key column is specified
if (primaryKeyColumn) {
selectColumns.add(primaryKeyColumn.propertyName);
}
} else {
// eslint-disable-next-line no-param-reassign
select = [];
selectColumns = new Set();
for (const column of model.columns) {
if (!(column as ColumnCollectionMetadata).collection) {
select.push(column.propertyName as string & keyof OmitFunctionsAndEntityCollections<T>);
selectColumns.add(column.propertyName);
}
}
}

let query = '';
for (const [index, propertyName] of select.entries()) {
for (const [index, propertyName] of Array.from(selectColumns).entries()) {
const column = model.columnsByPropertyName[propertyName];
if (!column) {
throw new Error(`Unable to find column for property: ${propertyName} on ${model.tableName}`);
Expand Down Expand Up @@ -565,7 +568,7 @@ export function buildWhereStatement<T extends Entity>({
* @returns {string} SQL order by statement
* @private
*/
export function buildOrderStatement<T extends Entity>({ model, sorts }: { model: ModelMetadata<T>; sorts: OrderBy<T>[] }): string {
export function buildOrderStatement<T extends Entity>({ model, sorts }: { model: ModelMetadata<T>; sorts: readonly OrderBy<T>[] }): string {
if (_.isNil(sorts) || !_.some(sorts)) {
return '';
}
Expand Down Expand Up @@ -620,7 +623,7 @@ function buildWhere<T extends Entity>({
propertyName?: string;
comparer?: Comparer | string;
isNegated?: boolean;
value?: WhereClauseValue<string> | WhereClauseValue<T> | WhereQuery<T> | WhereQuery<T>[] | string;
value?: WhereClauseValue<string> | WhereClauseValue<T> | WhereQuery<T> | string | readonly WhereQuery<T>[];
params: unknown[];
}): string {
switch (comparer || propertyName) {
Expand Down Expand Up @@ -995,7 +998,7 @@ function buildOrOperatorStatement<T extends Entity>({
repositoriesByModelNameLowered: Record<string, IReadonlyRepository<Entity> | IRepository<Entity>>;
model: ModelMetadata<T>;
isNegated: boolean;
value: WhereQuery<T>[];
value: readonly WhereQuery<T>[];
params: unknown[];
}): string {
const orClauses = [];
Expand Down Expand Up @@ -1027,7 +1030,7 @@ interface ComparisonOperatorStatementParams<T extends Entity> {
propertyName: string;
comparer?: Comparer | string;
isNegated: boolean;
value?: string[] | WhereClauseValue<T> | string;
value?: WhereClauseValue<T> | string | readonly string[];
params: unknown[];
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/GetValueType.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type GetValueType<T, TValueType> = T extends TValueType[] ? (T extends (infer U)[] ? U : never) : T extends TValueType ? T : never;
export type GetValueType<T, TValueType> = T extends TValueType[] ? (T extends (infer U)[] ? Extract<U, TValueType> : never) : T extends TValueType ? T : never;
70 changes: 69 additions & 1 deletion tests/readonlyRepository.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ describe('ReadonlyRepository', () => {
}),
]);
should.exist(result);
result!.should.deep.equal(product);
result.should.deep.equal(product);

const [query, params] = capture(mockedPool.query).first();
query.should.equal('SELECT "id","name","sku","alias_names" AS "aliases","store_id" AS "store" FROM "products" WHERE "id"=$1 LIMIT 1');
Expand Down Expand Up @@ -578,6 +578,44 @@ describe('ReadonlyRepository', () => {
storeQuery.should.equal('SELECT "id","name" FROM "stores" WHERE "id"=$1');
storeQueryParams!.should.deep.equal([store.id]);
});
it('should support populating a single relation when column is missing from partial select', async () => {
const store = {
id: faker.datatype.number(),
name: `store - ${faker.datatype.uuid()}`,
};
const product = {
id: faker.datatype.number(),
name: `product - ${faker.datatype.uuid()}`,
store,
};

when(mockedPool.query(anyString(), anything())).thenResolve(
getQueryResult([
{
id: product.id,
name: product.name,
store: store.id,
},
]),
getQueryResult([store]),
);

const result = await ProductRepository.findOne({
select: ['name'],
}).populate('store', {
select: ['name'],
});
verify(mockedPool.query(anyString(), anything())).twice();
should.exist(result);
result!.should.deep.equal(product);

const [productQuery, productQueryParams] = capture(mockedPool.query).first();
productQuery.should.equal('SELECT "name","store_id" AS "store","id" FROM "products" LIMIT 1');
productQueryParams!.should.deep.equal([]);
const [storeQuery, storeQueryParams] = capture(mockedPool.query).second();
storeQuery.should.equal('SELECT "name","id" FROM "stores" WHERE "id"=$1');
storeQueryParams!.should.deep.equal([store.id]);
});
it('should support populating a single relation with partial select and order', async () => {
const store = {
id: faker.datatype.number(),
Expand Down Expand Up @@ -2066,6 +2104,36 @@ describe('ReadonlyRepository', () => {
storeQuery.should.equal('SELECT "id" FROM "stores" WHERE "id"=ANY($1::INTEGER[]) ORDER BY "name"');
storeQueryParams!.should.deep.equal([[store1.id, store2.id]]);
});
it('should support populating a single relation when column is missing from partial select', async () => {
when(mockedPool.query(anyString(), anything())).thenResolve(
getQueryResult([_.pick(product1, 'id', 'name', 'store'), _.pick(product2, 'id', 'name', 'store')]),
getQueryResult([_.pick(store1, 'id'), _.pick(store2, 'id')]),
);

const results = await ProductRepository.find({
select: ['name'],
}).populate('store', {
select: ['id'],
});
verify(mockedPool.query(anyString(), anything())).twice();
results.should.deep.equal([
{
..._.pick(product1, 'id', 'name'),
store: _.pick(store1, 'id'),
},
{
..._.pick(product2, 'id', 'name'),
store: _.pick(store2, 'id'),
},
]);

const [productQuery, productQueryParams] = capture(mockedPool.query).first();
productQuery.should.equal('SELECT "name","store_id" AS "store","id" FROM "products"');
productQueryParams!.should.deep.equal([]);
const [storeQuery, storeQueryParams] = capture(mockedPool.query).second();
storeQuery.should.equal('SELECT "id" FROM "stores" WHERE "id"=ANY($1::INTEGER[])');
storeQueryParams!.should.deep.equal([[store1.id, store2.id]]);
});
it('should support populating one-to-many collection', async () => {
when(mockedPool.query(anyString(), anything())).thenResolve(
getQueryResult([_.pick(store1, 'id', 'name'), _.pick(store2, 'id', 'name')]),
Expand Down

0 comments on commit 2143315

Please sign in to comment.