Skip to content

Commit

Permalink
Add relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
eveningkid committed Jun 2, 2020
1 parent bf1b903 commit 33b0c07
Show file tree
Hide file tree
Showing 9 changed files with 604 additions and 45 deletions.
386 changes: 358 additions & 28 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/data-types.ts
Expand Up @@ -61,3 +61,5 @@ export const DATA_TYPES: Fields = {
JSON: "json",
JSONB: "jsonb",
};

export const DataTypes = DATA_TYPES;
14 changes: 14 additions & 0 deletions lib/helpers/fields.ts
Expand Up @@ -18,6 +18,20 @@ export function addFieldToSchema(
let instruction;

if (typeof fieldOptions.type === "object") {
if (fieldOptions.type.relationship) {
const relationshipPKName = fieldOptions.type.relationship.model
.getComputedPrimaryKey();

table.integer(fieldOptions.name);
table.foreign(fieldOptions.name).references(
fieldOptions.type.relationship.model.field(
relationshipPKName,
),
).onDelete("CASCADE");

return;
}

const fieldNameArgs: [string | number | (string | number)[]] = [
fieldOptions.name,
];
Expand Down
8 changes: 6 additions & 2 deletions lib/model-initializer.ts
Expand Up @@ -19,14 +19,18 @@ export class ModelInitializer {
// the following queries should be done within a transaction.

if (initializationOptions.initOptions.drop) {
const dropQuery = initializationOptions.queryBuilder.query().table(
const dropQuery = initializationOptions.queryBuilder.queryForSchema(
initializationOptions.model,
).table(
initializationOptions.model.table,
).dropIfExists().toDescription();

await initializationOptions.database.query(dropQuery);
}

const createQuery = initializationOptions.queryBuilder.query().table(
const createQuery = initializationOptions.queryBuilder.queryForSchema(
initializationOptions.model,
).table(
initializationOptions.model.table,
).createTable(
initializationOptions.model.fields,
Expand Down
8 changes: 8 additions & 0 deletions lib/model-pivot.ts
@@ -0,0 +1,8 @@
import { Model, ModelSchema } from "./model.ts";

export type PivotModelSchema = typeof PivotModel;

export class PivotModel extends Model {
static _pivotsModels: { [modelName: string]: ModelSchema } = {};
static _pivotsFields: { [modelName: string]: string } = {};
}
130 changes: 121 additions & 9 deletions lib/model.ts
Expand Up @@ -10,12 +10,14 @@ import {
FieldType,
} from "./query-builder.ts";
import { Database, SyncOptions } from "./database.ts";
import { PivotModelSchema } from "./model-pivot.ts";

/** Represents a Model class, not an instance. */
export type ModelSchema = typeof Model;

export type ModelFields = { [key: string]: FieldType };
export type ModelDefaults = { [field: string]: FieldValue };
export type ModelPivotModels = { [modelName: string]: PivotModelSchema };

export type ModelOptions = {
queryBuilder: QueryBuilder;
Expand All @@ -37,6 +39,9 @@ export class Model {
/** Default values for the model fields. */
static defaults: ModelDefaults = {};

/** Pivot table to use for a given model. */
static pivot: ModelPivotModels = {};

/** If the model has been created in the database. */
private static _isCreatedInDatabase: boolean = false;

Expand All @@ -62,7 +67,7 @@ export class Model {
this._options = options;
this._database = options.database;
this._queryBuilder = options.queryBuilder;
this._currentQuery = this._queryBuilder.query();
this._currentQuery = this._queryBuilder.queryForSchema(this);
this._primaryKey = this._findPrimaryKey();
}

Expand Down Expand Up @@ -96,10 +101,19 @@ export class Model {

/** Build the current query and run it on the associated database. */
private static async _runQuery(query: QueryDescription) {
this._currentQuery = this._queryBuilder.query();
this._currentQuery = this._queryBuilder.queryForSchema(this);
return this._database.query(query);
}

/** Return the model computed primary key. */
static getComputedPrimaryKey() {
if (!this._primaryKey) {
this._findPrimaryKey();
}

return this._primaryKey;
}

/** Return the table name followed by a field name. Can also rename a field using `nameAs`.
*
* Flight.field("departure") => "flights.departure"
Expand Down Expand Up @@ -149,7 +163,10 @@ export class Model {
*
* await Flight.select("id", "destination").get();
*/
static select(...fields: (string | FieldAlias)[]) {
static select<T extends typeof Model>(
this: T,
...fields: (string | FieldAlias)[]
) {
this._currentQuery.select(...fields);
return this;
}
Expand All @@ -175,7 +192,7 @@ export class Model {
static async find(idOrIds: FieldValue | FieldValue[]) {
return this._runQuery(
this._currentQuery.table(this.table).find(
this._primaryKey,
this.getComputedPrimaryKey(),
Array.isArray(idOrIds) ? idOrIds : [idOrIds],
).toDescription(),
);
Expand All @@ -187,7 +204,8 @@ export class Model {
*
* await Flight.orderBy("departure", "desc").all();
*/
static orderBy(
static orderBy<T extends typeof Model>(
this: T,
field: string,
orderDirection: OrderDirection = "asc",
) {
Expand All @@ -199,7 +217,7 @@ export class Model {
*
* await Flight.take(10).get();
*/
static take(limit: number) {
static take<T extends typeof Model>(this: T, limit: number) {
this._currentQuery.limit(limit);
return this;
}
Expand All @@ -222,7 +240,8 @@ export class Model {
*
* await Flight.where({ id: "1", departure: "Paris" }).get();
*/
static where(
static where<T extends typeof Model>(
this: T,
fieldOrFields: string | Values,
operatorOrFieldValue?: Operator | FieldValue,
fieldValue?: FieldValue,
Expand Down Expand Up @@ -289,7 +308,7 @@ export class Model {
static async deleteById(id: FieldValue) {
return this._runQuery(
this._currentQuery.table(this.table).where(
this._primaryKey,
this.getComputedPrimaryKey(),
"=",
id,
).delete().toDescription(),
Expand Down Expand Up @@ -317,7 +336,8 @@ export class Model {
* Flight.field("airportId"),
* ).get()
*/
static join(
static join<T extends typeof Model>(
this: T,
joinTable: ModelSchema,
originField: string,
targetField: string,
Expand Down Expand Up @@ -407,4 +427,96 @@ export class Model {

return value[0].avg;
}

/** Find associated values for the given model for one-to-many and many-to-many relationships.
*
* class Airport {
* static flights() {
* return this.hasMany(Flight);
* }
* }
*
* Airport.where("id", "1").flights();
*/
static hasMany<T extends typeof Model>(
this: T,
model: ModelSchema,
): Promise<any[]> {
const currentWhereValue = this._findCurrentQueryWhereClause();

if (model.name in this.pivot) {
const pivot = this.pivot[model.name];
const pivotField = pivot._pivotsFields[this.name];
const pivotOtherModel = pivot._pivotsModels[model.name];
const pivotOtherModelField = pivot._pivotsFields[model.name];

return pivot.where(pivot.field(pivotField), currentWhereValue).join(
pivotOtherModel,
pivotOtherModel.field(pivotOtherModel.getComputedPrimaryKey()),
pivot.field(pivotOtherModelField),
).get();
}

const foreignKeyName = this._findModelForeignKeyField(model);
this._currentQuery = this._queryBuilder.queryForSchema(this);
return model.where(foreignKeyName, currentWhereValue).all();
}

/** Find associated values for the given model for one-to-one and one-to-many relationships. */
static async hasOne<T extends typeof Model>(this: T, model: ModelSchema) {
const currentWhereValue = this._findCurrentQueryWhereClause();
const FKName = this._findModelForeignKeyField(model);

if (!FKName) {
const currentModelFKName = this._findModelForeignKeyField(this, model);
const currentModelValue = await this.where(
this.getComputedPrimaryKey(),
currentWhereValue,
).first();
const currentModelFKValue = currentModelValue[currentModelFKName];
return model.where(model.getComputedPrimaryKey(), currentModelFKValue)
.first();
}

return model.where(FKName, currentWhereValue).first();
}

/** Look for the current query's where clause for this model's primary key. */
private static _findCurrentQueryWhereClause() {
if (!this._currentQuery._query.wheres) {
throw new Error("The current query does not have any where clause.");
}

const where = this._currentQuery._query.wheres.find((where) => {
return where.field === this.getComputedPrimaryKey();
});

if (!where) {
throw new Error(
"The current query does not have any where clause for this model primary key.",
);
}

return where.value;
}

/** Look for a `fieldName: Relationships.belongsTo(forModel)` field for a given `model`. */
private static _findModelForeignKeyField(
model: ModelSchema,
forModel: ModelSchema = this,
): string {
const modelFK: [string, FieldType] | undefined = Object.entries(
model.fields,
).find(([, type]) => {
return (typeof type === "object")
? type.relationship?.model === forModel
: false;
});

if (!modelFK) {
return "";
}

return modelFK[0];
}
}
28 changes: 23 additions & 5 deletions lib/query-builder.ts
@@ -1,6 +1,7 @@
import { SQLQueryBuilder } from "../deps.ts";
import { FieldTypeString } from "./data-types.ts";
import { ModelFields, ModelDefaults } from "./model.ts";
import { ModelFields, ModelDefaults, ModelSchema } from "./model.ts";
import { Relationship } from "./relationships.ts";

export type FieldValue = number | string | boolean | Date;
export type Values = { [key: string]: FieldValue };
Expand All @@ -14,6 +15,7 @@ export type FieldType = FieldTypeString | {
precision?: number;
scale?: number;
values?: (number | string)[];
relationship?: Relationship;
};
export type FieldAlias = { [k: string]: string };

Expand Down Expand Up @@ -56,6 +58,7 @@ export type OrderByClause = {
};

export type QueryDescription = {
schema?: ModelSchema;
type?: QueryType;
table?: string;
orderBy?: OrderByClause;
Expand All @@ -81,8 +84,13 @@ export class QueryBuilder {
_query: QueryDescription = {};

/** Create a fresh new query. */
query(): QueryBuilder {
return new QueryBuilder();
queryForSchema(schema: ModelSchema): QueryBuilder {
return new QueryBuilder().schema(schema);
}

schema(schema: ModelSchema) {
this._query.schema = schema;
return this;
}

toDescription(): QueryDescription {
Expand Down Expand Up @@ -172,11 +180,21 @@ export class QueryBuilder {
this._query.wheres = [];
}

this._query.wheres.push({
const whereClause = {
field,
operator,
value,
});
};

const existingWhereForFieldIndex = this._query.wheres.findIndex((where) =>
where.field === field
);

if (existingWhereForFieldIndex === -1) {
this._query.wheres.push(whereClause);
} else {
this._query.wheres[existingWhereForFieldIndex] = whereClause;
}

return this;
}
Expand Down

0 comments on commit 33b0c07

Please sign in to comment.