Skip to content

Commit

Permalink
implement queries on joins and fix nested joins (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
Emily committed Feb 5, 2019
1 parent 4c3e4e8 commit 63bda5d
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 47 deletions.
3 changes: 2 additions & 1 deletion packages/adapter-postgres/package.json
Expand Up @@ -40,6 +40,7 @@
},
"devDependencies": {
"@types/pg": "^7.4.11",
"fewer": "^0.1.0"
"fewer": "^0.1.0",
"@fewer/sq": "^0.1.0"
}
}
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`load associations postgres can join users <-> posts 1`] = `
exports[`join associations postgres can join users <-> posts 1`] = `
Array [
Object {
"first_name": "Emily",
Expand Down Expand Up @@ -28,3 +28,16 @@ Array [
},
]
`;

exports[`join associations postgres nested joins 1`] = `
Array [
Object {
"first_name": "Emily",
"id": "1",
"last_name": "Dobervich",
"subtitle": null,
"title": "How to use Fewer",
"user_id": "1",
},
]
`;
39 changes: 38 additions & 1 deletion packages/adapter-postgres/src/__integration__/joins.test.ts
Expand Up @@ -30,7 +30,7 @@ function getSchemaAndRepos(database: Database) {
}
}

describe('load associations', () => {
describe('join associations', () => {
describe('postgres', () => {
let database: Database<AdapterInstance>;
let adapter: AdapterInstance;
Expand Down Expand Up @@ -98,5 +98,42 @@ describe('load associations', () => {
const results = await dbTypes.Users.join('posts', dbTypes.userPosts);
expect(results.sort((a, b) => a.id - b.id)).toMatchSnapshot();
});

it('nested joins', async () => {
const dbTypes = getSchemaAndRepos(database);

const emilyId = await dbTypes.Users.create({
first_name: 'Emily',
last_name: 'Dobervich',
});

const jordanId = await dbTypes.Users.create({
first_name: 'Jordan',
last_name: 'Gensler',
});

const fooId = await dbTypes.Users.create({
first_name: 'Foo',
last_name: 'Bar',
});

await dbTypes.Posts.create({
user_id: emilyId.id,
title: 'How to use Fewer',
});

await dbTypes.Posts.create({
user_id: jordanId.id,
title: 'Abusing Typescript',
});

await dbTypes.Posts.create({
user_id: jordanId.id,
title: 'Ten Typescript Type Tricks',
});

const results = await dbTypes.Users.join('posts', dbTypes.userPosts.join('user', dbTypes.belongsToUser)).where({ posts: { user: { first_name: 'Emily' } } });
expect(results.sort((a, b) => a.id - b.id)).toMatchSnapshot();
});
});
});
58 changes: 44 additions & 14 deletions packages/adapter-postgres/src/index.ts
Expand Up @@ -7,6 +7,8 @@ import fieldTypes from './fieldTypes';
// TODO: Allow custom methods to be defined on the adapter rather than having this here:
import rawQuery from './rawQuery';
import infos from './infos';
import { PostgresSelect } from 'squel';
import { SelectJoin } from '@fewer/sq';

type FieldTypes = typeof fieldTypes;

Expand All @@ -19,6 +21,46 @@ async function ensureMigrationTable(db: Client) {
)`);
}

function applyJoins(table: string, prefix: string, select: PostgresSelect, joins: { [key: string]: SelectJoin }) {
for (const key in joins) {
const alias = `${prefix}_${key}`;
const join = joins[key];
select.join(join.tableName, alias, `${alias}.${join.keys[1]} = ${table}.${join.keys[0]}`);

const subJoins = join.select.context.joins;

if (subJoins) {
applyJoins(alias, alias, select, subJoins);
}
}
}

function applyWheres(table: string, prefix: string, select: PostgresSelect, joins: { [key: string]: SelectJoin } | undefined, wheres: object[]) {
for (const where of wheres) {
for (const [fieldName, matcher] of Object.entries(where)) {
if (joins && fieldName in joins) {
const alias = `${prefix}_${fieldName}`;
const nestedJoins = joins[fieldName].select.context.joins;
const nestedWhere = [Object.entries(matcher).reduce((result: any, [k, v]) => {
if (nestedJoins && k in nestedJoins) {
result[k] = v;
} else {
result[`${alias}.${k}`] = v;
}
return result;
}, {})]
applyWheres(alias, alias, select, joins[fieldName].select.context.joins, nestedWhere);
} else {
if (Array.isArray(matcher)) {
select.where(`${fieldName} IN ?`, matcher);
} else {
select.where(`${fieldName} = ?`, matcher);
}
}
}
}
}

export const Adapter = createAdapter<TableTypes, FieldTypes, ConnectionConfig, Client>({
fieldTypes,

Expand Down Expand Up @@ -52,23 +94,11 @@ export const Adapter = createAdapter<TableTypes, FieldTypes, ConnectionConfig, C
}

const joins = context.joins;

if (joins) {
for (const key in joins) {
const join = joins[key];
select.join(join.tableName, key, `${key}.${join.keys[1]} = ${context.table}.${join.keys[0]}`);
}
applyJoins(context.table, '', select, joins);
}

for (const where of context.wheres) {
for (const [fieldName, matcher] of Object.entries(where)) {
if (Array.isArray(matcher)) {
select.where(`${fieldName} IN ?`, matcher);
} else {
select.where(`${fieldName} = ?`, matcher);
}
}
}
applyWheres(context.table, '', select, joins, context.wheres);

const results = await db.query(select.toString());
const loads = context.loads;
Expand Down
19 changes: 10 additions & 9 deletions packages/fewer/__tests__/Repository/queryBuilding.test.ts
Expand Up @@ -5,6 +5,7 @@ import {
createSchema,
} from '../../src';
import { database } from '../../__typeval__/mocks'
import { INTERNAL_TYPES } from '../../src/types';

interface User {
id: number;
Expand Down Expand Up @@ -37,7 +38,7 @@ const belongsToUser = createBelongsTo(Users, 'userId');
describe('queryBuilding', () => {
describe('where', () => {
it('simple usage results in a correct SQ select', () => {
const result = Users.where({ firstName: 'Emily' }).toSqSelect();
const result = Users.where({ firstName: 'Emily' })[INTERNAL_TYPES.TO_SQ_SELECT]();
expect(result).toEqual({
context: {
plucked: [],
Expand All @@ -51,7 +52,7 @@ describe('queryBuilding', () => {
const result = Users
.where({firstName: 'Emily'})
.where({lastName: 'Dobervich'})
.toSqSelect();
[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -66,7 +67,7 @@ describe('queryBuilding', () => {
});

it('value in array results in a correct SQ select', () => {
const result = Users.where({firstName: ['Emily', 'Jordan']}).toSqSelect();
const result = Users.where({firstName: ['Emily', 'Jordan']})[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -82,7 +83,7 @@ describe('queryBuilding', () => {

describe('limit', () => {
it('builds a correct SQ select', () => {
const result = Users.limit(5).toSqSelect();
const result = Users.limit(5)[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -97,7 +98,7 @@ describe('queryBuilding', () => {

describe('offset', () => {
it('builds a correct SQ select', () => {
const result = Users.offset(5).toSqSelect();
const result = Users.offset(5)[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -112,7 +113,7 @@ describe('queryBuilding', () => {

describe('load', () => {
it('builds a correct SQ select', () => {
const result = Users.load('posts', userPosts).toSqSelect();
const result = Users.load('posts', userPosts)[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -136,7 +137,7 @@ describe('queryBuilding', () => {
});

it('with association query builds a correct SQ select', () => {
const result = Users.load('posts', userPosts.where({title: 'How to Use Fewer'})).toSqSelect();
const result = Users.load('posts', userPosts.where({title: 'How to Use Fewer'}))[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -162,7 +163,7 @@ describe('queryBuilding', () => {
it('with nested association builds a correct SQ select', () => {
const result = Users
.load('posts', userPosts.load('user', belongsToUser))
.toSqSelect();
[INTERNAL_TYPES.TO_SQ_SELECT]();

expect(result).toEqual({
context: {
Expand All @@ -176,7 +177,7 @@ describe('queryBuilding', () => {
wheres: [],
loads: {
'user': {
keys: ['id', 'userId'],
keys: ['userId', 'id'],
select: {
context: {
plucked: [],
Expand Down
2 changes: 1 addition & 1 deletion packages/fewer/__typeval__/fail/brokenJoins.errors.json
Expand Up @@ -10,6 +10,6 @@
"column": 21
}
},
"errorMessage": "Type '{ subtitle: string; incorrect: number; }' is not assignable to type 'WhereForType<Merge<{ subtitle?: string | undefined; } & { userId: number; title: string; }>>'.\n Object literal may only specify known properties, and 'incorrect' does not exist in type 'WhereForType<Merge<{ subtitle?: string | undefined; } & { userId: number; title: string; }>>'."
"errorMessage": "Type '{ subtitle: string; incorrect: number; }' is not assignable to type 'Merge<WhereForType<Merge<{ subtitle?: string | undefined; } & { userId: number; title: string; }>> & {}>'.\n Object literal may only specify known properties, and 'incorrect' does not exist in type 'Merge<WhereForType<Merge<{ subtitle?: string | undefined; } & { userId: number; title: string; }>> & {}>'."
}
]
10 changes: 10 additions & 0 deletions packages/fewer/__typeval__/pass/associations.ts
Expand Up @@ -46,6 +46,16 @@ async function main() {
},
});

// Query through nested join:
Users.join('posts', userPosts.join('user', belongsToUser))
.where({
posts: {
user: {
firstName: 'Emily',
}
}
});

typeval.acceptsString(user.firstName);
typeval.accepts<{ title: string; subtitle?: string; userId: number }[]>(
user.posts,
Expand Down
21 changes: 18 additions & 3 deletions packages/fewer/src/Association/index.ts
Expand Up @@ -46,6 +46,7 @@ export class Association<
SelectionSet,
keyof LoadAssociations
>;
readonly [INTERNAL_TYPES.JOINS]: JoinAssociations;

/**
* The type of relationship that the association represents, such as "hasMany", and "belongsTo".
Expand All @@ -64,7 +65,7 @@ export class Association<
this.foreignKey = foreignKey;
}

toSqSelect(): Select {
[INTERNAL_TYPES.TO_SQ_SELECT](): Select {
return this.selectQuery();
}

Expand Down Expand Up @@ -203,10 +204,17 @@ export class Association<
true,
SchemaType
> {
let keys: [string, string];
if (association.type === 'belongsTo') {
keys = [association.foreignKey, 'id'];
} else {
keys = ['id', association.foreignKey];
}

return new Association(
this.type,
this.associate,
this.selectQuery().load(name, ['id', this.foreignKey], association.toSqSelect()),
this.selectQuery().load(name, keys, association[INTERNAL_TYPES.TO_SQ_SELECT]()),
this.foreignKey,
);
}
Expand Down Expand Up @@ -243,10 +251,17 @@ export class Association<
Chained,
SchemaType
> {
let keys: [string, string];
if (association.type === 'belongsTo') {
keys = [association.foreignKey, 'id'];
} else {
keys = ['id', association.foreignKey];
}

return new Association(
this.type,
this.associate,
this.selectQuery().join(name, ['id', association.foreignKey], association.getTableName()),
this.selectQuery().join(name, keys, association.getTableName(), association[INTERNAL_TYPES.TO_SQ_SELECT]()),
this.foreignKey
);
}
Expand Down
20 changes: 17 additions & 3 deletions packages/fewer/src/Repository/index.ts
Expand Up @@ -74,7 +74,7 @@ export class Repository<
this.queryType = queryType;
}

toSqSelect(): Select {
[INTERNAL_TYPES.TO_SQ_SELECT](): Select {
return this.selectQuery();
}

Expand Down Expand Up @@ -385,9 +385,16 @@ export class Repository<
JoinAssociations,
QueryType
> {
let keys: [string, string];
if (association.type === 'belongsTo') {
keys = [association.foreignKey, 'id'];
} else {
keys = ['id', association.foreignKey];
}

return new Repository(
this.schemaTable,
this.selectQuery().load(name, ['id', association.foreignKey], association.toSqSelect()),
this.selectQuery().load(name, keys, association[INTERNAL_TYPES.TO_SQ_SELECT]()),
this.pipes,
this.queryType,
);
Expand Down Expand Up @@ -424,9 +431,16 @@ export class Repository<
JoinAssociations & { [P in Name]: JoinAssociation },
QueryType
> {
let keys: [string, string];
if (association.type === 'belongsTo') {
keys = [association.foreignKey, 'id'];
} else {
keys = ['id', association.foreignKey];
}

return new Repository(
this.schemaTable,
this.selectQuery().join(name, ['id', association.foreignKey], association.getTableName()),
this.selectQuery().join(name, keys, association.getTableName(), association[INTERNAL_TYPES.TO_SQ_SELECT]()),
this.pipes,
this.queryType,
);
Expand Down

0 comments on commit 63bda5d

Please sign in to comment.