mirror of
https://github.com/swc-project/swc.git
synced 2024-12-22 13:11:31 +03:00
992 lines
27 KiB
TypeScript
992 lines
27 KiB
TypeScript
|
// Loaded from https://deno.land/x/denodb@v1.0.18/lib/model.ts
|
|||
|
|
|||
|
|
|||
|
import type {
|
|||
|
Operator,
|
|||
|
OrderByClauses,
|
|||
|
OrderDirection,
|
|||
|
QueryBuilder,
|
|||
|
QueryDescription,
|
|||
|
QueryType,
|
|||
|
} from "./query-builder.ts";
|
|||
|
import type { Database } from "./database.ts";
|
|||
|
import type { PivotModelSchema } from "./model-pivot.ts";
|
|||
|
import { camelCase } from "../deps.ts";
|
|||
|
import {
|
|||
|
DataTypes,
|
|||
|
FieldAlias,
|
|||
|
FieldOptions,
|
|||
|
FieldProps,
|
|||
|
FieldType,
|
|||
|
FieldTypeString,
|
|||
|
FieldValue,
|
|||
|
Values,
|
|||
|
} from "./data-types.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 | (() => FieldValue);
|
|||
|
};
|
|||
|
export type ModelPivotModels = { [modelName: string]: PivotModelSchema };
|
|||
|
export type FieldMatchingTable = { [clientField: string]: string };
|
|||
|
|
|||
|
export type ModelOptions = {
|
|||
|
queryBuilder: QueryBuilder;
|
|||
|
database: Database;
|
|||
|
};
|
|||
|
|
|||
|
export type AggregationResult = Model & {
|
|||
|
avg?: number;
|
|||
|
count?: number;
|
|||
|
max?: number;
|
|||
|
min?: number;
|
|||
|
sum?: number;
|
|||
|
};
|
|||
|
|
|||
|
export type ModelEventType =
|
|||
|
| "creating"
|
|||
|
| "created"
|
|||
|
| "updating"
|
|||
|
| "updated"
|
|||
|
| "deleting"
|
|||
|
| "deleted";
|
|||
|
|
|||
|
export type ModelEventListenerWithModel = (model: Model) => void;
|
|||
|
export type ModelEventListenerWithoutModel = (model?: Model) => void;
|
|||
|
export type ModelEventListener =
|
|||
|
| ModelEventListenerWithoutModel
|
|||
|
| ModelEventListenerWithModel;
|
|||
|
|
|||
|
export type ModelEventListeners = {
|
|||
|
[eventType in ModelEventType]?: ModelEventListener[];
|
|||
|
};
|
|||
|
|
|||
|
/** Model that can be used with a `Database`. */
|
|||
|
export class Model {
|
|||
|
[attribute: string]: FieldValue | Function
|
|||
|
|
|||
|
/** Table name as it should be saved in the database. */
|
|||
|
static table = "";
|
|||
|
|
|||
|
/** Should this model have `created_at` and `updated_at` fields by default. */
|
|||
|
static timestamps = false;
|
|||
|
|
|||
|
/** Model fields. */
|
|||
|
static fields: ModelFields = {};
|
|||
|
|
|||
|
/** 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;
|
|||
|
|
|||
|
/** Query builder instance. */
|
|||
|
private static _queryBuilder: QueryBuilder;
|
|||
|
|
|||
|
/** Database which this model will be attached to. */
|
|||
|
private static _database: Database;
|
|||
|
|
|||
|
/** Model primary key. Manually found through `_findPrimaryKey()`. */
|
|||
|
private static _primaryKey: string;
|
|||
|
|
|||
|
/** Model field matching, from database to client and vice versa. */
|
|||
|
private static _fieldMatching: {
|
|||
|
toDatabase: FieldMatchingTable;
|
|||
|
toClient: FieldMatchingTable;
|
|||
|
} = {
|
|||
|
toDatabase: {},
|
|||
|
toClient: {},
|
|||
|
};
|
|||
|
|
|||
|
/** Model current query being built. */
|
|||
|
private static _currentQuery: QueryBuilder;
|
|||
|
|
|||
|
/** Options this model was initialized with. */
|
|||
|
private static _options: ModelOptions;
|
|||
|
|
|||
|
/** Attached event listeners. */
|
|||
|
private static _listeners: ModelEventListeners = {};
|
|||
|
|
|||
|
/** Link a model to a database. Should not be called from a child model. */
|
|||
|
static _link(options: ModelOptions) {
|
|||
|
this._options = options;
|
|||
|
this._database = options.database;
|
|||
|
this._queryBuilder = options.queryBuilder;
|
|||
|
|
|||
|
this._fieldMatching = this._database._computeModelFieldMatchings(
|
|||
|
this.name,
|
|||
|
this.fields,
|
|||
|
this.timestamps,
|
|||
|
);
|
|||
|
|
|||
|
this._currentQuery = this._queryBuilder.queryForSchema(this);
|
|||
|
this._primaryKey = this._findPrimaryKey();
|
|||
|
}
|
|||
|
|
|||
|
/** Drop a model in the database. */
|
|||
|
static async drop() {
|
|||
|
const dropQuery = this._options.queryBuilder
|
|||
|
.queryForSchema(this)
|
|||
|
.table(this.table)
|
|||
|
.dropIfExists()
|
|||
|
.toDescription();
|
|||
|
|
|||
|
await this._options.database.query(dropQuery);
|
|||
|
|
|||
|
this._isCreatedInDatabase = false;
|
|||
|
}
|
|||
|
|
|||
|
/** Create a model in the database. Should not be called from a child model. */
|
|||
|
static async createTable() {
|
|||
|
if (this._isCreatedInDatabase) {
|
|||
|
throw new Error("This model has already been initialized.");
|
|||
|
}
|
|||
|
|
|||
|
const createQuery = this._options.queryBuilder
|
|||
|
.queryForSchema(this)
|
|||
|
.table(this.table)
|
|||
|
.createTable(
|
|||
|
this.formatFieldToDatabase(this.fields) as ModelFields,
|
|||
|
this.formatFieldToDatabase(this.defaults) as ModelDefaults,
|
|||
|
{
|
|||
|
withTimestamps: this.timestamps,
|
|||
|
ifNotExists: true,
|
|||
|
},
|
|||
|
)
|
|||
|
.toDescription();
|
|||
|
|
|||
|
await this._options.database.query(createQuery);
|
|||
|
|
|||
|
this._isCreatedInDatabase = true;
|
|||
|
}
|
|||
|
|
|||
|
/** Manually find the primary field by going through the schema fields. */
|
|||
|
private static _findPrimaryField(): FieldOptions {
|
|||
|
const field = Object.entries(this.fields).find(
|
|||
|
([_, fieldType]) => typeof fieldType === "object" && fieldType.primaryKey,
|
|||
|
);
|
|||
|
|
|||
|
return {
|
|||
|
name: field ? (this.formatFieldToDatabase(field[0]) as string) : "",
|
|||
|
type: field ? field[1] : DataTypes.INTEGER,
|
|||
|
defaultValue: 0,
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/** Manually find the primary key by going through the schema fields. */
|
|||
|
private static _findPrimaryKey(): string {
|
|||
|
return this._findPrimaryField().name;
|
|||
|
}
|
|||
|
|
|||
|
/** Return the model computed primary key. */
|
|||
|
static getComputedPrimaryKey(): string {
|
|||
|
if (!this._primaryKey) {
|
|||
|
this._primaryKey = this._findPrimaryKey();
|
|||
|
}
|
|||
|
|
|||
|
return this._primaryKey;
|
|||
|
}
|
|||
|
|
|||
|
/** Return the field type of the primary key. */
|
|||
|
static getComputedPrimaryType(): FieldTypeString {
|
|||
|
const field = this._findPrimaryField();
|
|||
|
|
|||
|
return typeof field.type === "object"
|
|||
|
? (field.type as any).type
|
|||
|
: field.type;
|
|||
|
}
|
|||
|
|
|||
|
/** Return the field properties of the primary key */
|
|||
|
static getComputedPrimaryProps(): FieldProps {
|
|||
|
const field = this._findPrimaryField();
|
|||
|
|
|||
|
return typeof field === "object" ? field.type : {};
|
|||
|
}
|
|||
|
|
|||
|
/** Build the current query and run it on the associated database. */
|
|||
|
private static async _runQuery(query: QueryDescription) {
|
|||
|
this._currentQuery = this._queryBuilder.queryForSchema(this);
|
|||
|
|
|||
|
if (query.type) {
|
|||
|
this._runEventListeners(query.type);
|
|||
|
}
|
|||
|
|
|||
|
const results = await this._database.query(query);
|
|||
|
|
|||
|
if (query.type) {
|
|||
|
this._runEventListeners(query.type, results);
|
|||
|
}
|
|||
|
|
|||
|
return results;
|
|||
|
}
|
|||
|
|
|||
|
/** Format a field or an object of fields, following a field matching table.
|
|||
|
* Defaulting to `defaultCase` or `field` otherwise. */
|
|||
|
private static _formatField(
|
|||
|
fieldMatching: FieldMatchingTable,
|
|||
|
field: string | { [fieldName: string]: any },
|
|||
|
defaultCase?: (field: string) => string,
|
|||
|
): string | { [fieldName: string]: any } {
|
|||
|
if (typeof field !== "string") {
|
|||
|
return Object.entries(field).reduce((prev: any, [fieldName, value]) => {
|
|||
|
prev[this._formatField(fieldMatching, fieldName) as string] = value;
|
|||
|
return prev;
|
|||
|
}, {}) as { [fieldName: string]: any };
|
|||
|
}
|
|||
|
|
|||
|
if (field in fieldMatching) {
|
|||
|
return fieldMatching[field];
|
|||
|
}
|
|||
|
|
|||
|
return defaultCase ? defaultCase(field) : field;
|
|||
|
}
|
|||
|
|
|||
|
/** Format field or an object of fields from client to database. */
|
|||
|
static formatFieldToDatabase(field: string | Object) {
|
|||
|
return this._formatField(this._fieldMatching.toDatabase, field);
|
|||
|
}
|
|||
|
|
|||
|
/** Format field or an object of fields from database to client. */
|
|||
|
static formatFieldToClient(field: string | Object) {
|
|||
|
return this._formatField(this._fieldMatching.toClient, field, camelCase);
|
|||
|
}
|
|||
|
|
|||
|
/** Add an event listener for a specific operation/hook.
|
|||
|
*
|
|||
|
* Flight.on('created', (model) => console.log('New model:', model));
|
|||
|
*/
|
|||
|
static on<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
eventType: ModelEventType,
|
|||
|
callback: ModelEventListener,
|
|||
|
) {
|
|||
|
if (!(eventType in this._listeners)) {
|
|||
|
this._listeners[eventType] = [];
|
|||
|
}
|
|||
|
|
|||
|
this._listeners[eventType]!.push(callback);
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Alias for `Model.on`, add an event listener for a specific operation/hook.
|
|||
|
*
|
|||
|
* Flight.addEventListener('created', (model) => console.log('New model:', model));
|
|||
|
*/
|
|||
|
static addEventListener<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
eventType: ModelEventType,
|
|||
|
callback: ModelEventListener,
|
|||
|
) {
|
|||
|
return this.on(eventType, callback);
|
|||
|
}
|
|||
|
|
|||
|
static removeEventListener(
|
|||
|
eventType: ModelEventType,
|
|||
|
callback: ModelEventListener,
|
|||
|
) {
|
|||
|
if (!(eventType in this._listeners)) {
|
|||
|
throw new Error(
|
|||
|
`There is no event listener for ${eventType}. You might be trying to remove a listener that you haven't added with Model.on('${eventType}', ...).`,
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
this._listeners[eventType] = this._listeners[eventType]!.filter((
|
|||
|
listener,
|
|||
|
) => listener !== callback);
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Run event listeners given a query type and results. */
|
|||
|
private static _runEventListeners(
|
|||
|
queryType: QueryType,
|
|||
|
instances?: Model | Model[],
|
|||
|
) {
|
|||
|
// -ing => present, -ed => past
|
|||
|
const isPastEvent = !!instances;
|
|||
|
|
|||
|
let eventType: ModelEventType;
|
|||
|
switch (queryType) {
|
|||
|
case "insert":
|
|||
|
eventType = isPastEvent ? "created" : "creating";
|
|||
|
break;
|
|||
|
|
|||
|
case "update":
|
|||
|
eventType = isPastEvent ? "updated" : "updating";
|
|||
|
break;
|
|||
|
|
|||
|
case "delete":
|
|||
|
eventType = isPastEvent ? "deleted" : "deleting";
|
|||
|
break;
|
|||
|
|
|||
|
default:
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
const listeners = this._listeners[eventType];
|
|||
|
|
|||
|
if (!listeners) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
for (const listener of listeners) {
|
|||
|
if (instances) {
|
|||
|
if (Array.isArray(instances)) {
|
|||
|
if (instances.length > 0) {
|
|||
|
instances.forEach(listener);
|
|||
|
} else {
|
|||
|
(listener as ModelEventListenerWithoutModel)();
|
|||
|
}
|
|||
|
} else {
|
|||
|
listener(instances);
|
|||
|
}
|
|||
|
} else {
|
|||
|
(listener as ModelEventListenerWithoutModel)();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/** Return the table name followed by a field name. Can also rename a field using `nameAs`.
|
|||
|
*
|
|||
|
* Flight.field("departure") => "flights.departure"
|
|||
|
*
|
|||
|
* Flight.field("id", "flight_id") => { flight_id: "flights.id" }
|
|||
|
*/
|
|||
|
static field(field: string): string;
|
|||
|
static field(field: string, nameAs: string): FieldAlias;
|
|||
|
static field(field: string, nameAs?: string): string | FieldAlias {
|
|||
|
const fullField = this.formatFieldToDatabase(
|
|||
|
`${this.table}.${field}`,
|
|||
|
) as string;
|
|||
|
|
|||
|
if (nameAs) {
|
|||
|
return { [nameAs]: fullField };
|
|||
|
}
|
|||
|
|
|||
|
return fullField;
|
|||
|
}
|
|||
|
|
|||
|
/** Run the current query. */
|
|||
|
static async get() {
|
|||
|
return this._runQuery(
|
|||
|
this._currentQuery.table(this.table).get().toDescription(),
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
/** Fetch all the model records.
|
|||
|
*
|
|||
|
* await Flight.all();
|
|||
|
*
|
|||
|
* await Flight.select("id").all();
|
|||
|
*/
|
|||
|
static async all() {
|
|||
|
return this.get() as Promise<Model[]>;
|
|||
|
}
|
|||
|
|
|||
|
/** Indicate which fields should be returned/selected from the query.
|
|||
|
*
|
|||
|
* await Flight.select("id").get();
|
|||
|
*
|
|||
|
* await Flight.select("id", "destination").get();
|
|||
|
*/
|
|||
|
static select<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
...fields: (string | FieldAlias)[]
|
|||
|
) {
|
|||
|
this._currentQuery.select(
|
|||
|
...fields.map((field) => this.formatFieldToDatabase(field)),
|
|||
|
);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Create one or multiple records in the current model.
|
|||
|
*
|
|||
|
* await Flight.create({ departure: "Paris", destination: "Tokyo" });
|
|||
|
*
|
|||
|
* await Flight.create([{ ... }, { ... }]);
|
|||
|
*/
|
|||
|
static async create(values: Values): Promise<Model>;
|
|||
|
static async create(values: Values[]): Promise<Model[]>;
|
|||
|
static async create(values: Values | Values[]) {
|
|||
|
const insertions = Array.isArray(values) ? values : [values];
|
|||
|
|
|||
|
const results = await this._runQuery(
|
|||
|
this._currentQuery.table(this.table).create(
|
|||
|
insertions.map((field) =>
|
|||
|
this.formatFieldToDatabase(field)
|
|||
|
) as Values[],
|
|||
|
).toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
if (!Array.isArray(values) && Array.isArray(results)) {
|
|||
|
return results[0];
|
|||
|
}
|
|||
|
|
|||
|
return results;
|
|||
|
}
|
|||
|
|
|||
|
/** Find one or multiple records based on the model primary key.
|
|||
|
*
|
|||
|
* await Flight.find("1");
|
|||
|
*/
|
|||
|
static async find(idOrIds: FieldValue): Promise<Model>;
|
|||
|
static async find(idOrIds: FieldValue[]): Promise<Model[]>;
|
|||
|
static async find(idOrIds: FieldValue | FieldValue[]) {
|
|||
|
const results = await this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.find(
|
|||
|
this.getComputedPrimaryKey(),
|
|||
|
Array.isArray(idOrIds) ? idOrIds : [idOrIds],
|
|||
|
)
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
return Array.isArray(idOrIds) ? results : (results as Model[])[0];
|
|||
|
}
|
|||
|
|
|||
|
/** Order query results based on a field name and an optional direction.
|
|||
|
*
|
|||
|
* await Flight.orderBy("departure").all();
|
|||
|
*
|
|||
|
* await Flight.orderBy("departure", "desc").all();
|
|||
|
*
|
|||
|
* await Flight.orderBy({ departure: "desc", destination: "asc" }).all();
|
|||
|
*/
|
|||
|
static orderBy<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
fieldOrFields: string | OrderByClauses,
|
|||
|
orderDirection: OrderDirection = "asc",
|
|||
|
) {
|
|||
|
if (typeof fieldOrFields === "string") {
|
|||
|
this._currentQuery.orderBy(
|
|||
|
this.formatFieldToDatabase(fieldOrFields) as string,
|
|||
|
orderDirection,
|
|||
|
);
|
|||
|
} else {
|
|||
|
for (
|
|||
|
const [field, orderDirectionField] of Object.entries(
|
|||
|
fieldOrFields,
|
|||
|
)
|
|||
|
) {
|
|||
|
this._currentQuery.orderBy(
|
|||
|
this.formatFieldToDatabase(field) as string,
|
|||
|
orderDirectionField,
|
|||
|
);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Group rows by a given field.
|
|||
|
*
|
|||
|
* await Flight.groupBy('departure').all();
|
|||
|
*/
|
|||
|
static groupBy<T extends ModelSchema>(this: T, field: string) {
|
|||
|
this._currentQuery.groupBy(this.formatFieldToDatabase(field) as string);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Similar to `limit`, limit the number of results returned from the query.
|
|||
|
*
|
|||
|
* await Flight.take(10).get();
|
|||
|
*/
|
|||
|
static take<T extends ModelSchema>(this: T, limit: number) {
|
|||
|
return this.limit(limit);
|
|||
|
}
|
|||
|
|
|||
|
/** Limit the number of results returned from the query.
|
|||
|
*
|
|||
|
* await Flight.limit(10).get();
|
|||
|
*/
|
|||
|
static limit<T extends ModelSchema>(this: T, limit: number) {
|
|||
|
this._currentQuery.limit(limit);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Return the first record that matches the current query.
|
|||
|
*
|
|||
|
* await Flight.where("id", ">", "1").first();
|
|||
|
*/
|
|||
|
static async first() {
|
|||
|
this.take(1);
|
|||
|
const results = await this.get();
|
|||
|
return (results as Model[])[0];
|
|||
|
}
|
|||
|
|
|||
|
/** Skip n values in the results.
|
|||
|
*
|
|||
|
* await Flight.offset(10).get();
|
|||
|
*
|
|||
|
* await Flight.offset(10).limit(2).get();
|
|||
|
*/
|
|||
|
static offset<T extends ModelSchema>(this: T, offset: number) {
|
|||
|
this._currentQuery.offset(offset);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Similar to `offset`, skip n values in the results.
|
|||
|
*
|
|||
|
* await Flight.skip(10).get();
|
|||
|
*
|
|||
|
* await Flight.skip(10).take(2).get();
|
|||
|
*/
|
|||
|
static skip<T extends ModelSchema>(this: T, offset: number) {
|
|||
|
return this.offset(offset);
|
|||
|
}
|
|||
|
|
|||
|
/** Add a `where` clause to your query.
|
|||
|
*
|
|||
|
* await Flight.where("id", "1").get();
|
|||
|
*
|
|||
|
* await Flight.where("id", ">", "1").get();
|
|||
|
*
|
|||
|
* await Flight.where({ id: "1", departure: "Paris" }).get();
|
|||
|
*/
|
|||
|
static where<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
fieldOrFields: string | Values,
|
|||
|
operatorOrFieldValue?: Operator | FieldValue,
|
|||
|
fieldValue?: FieldValue,
|
|||
|
) {
|
|||
|
if (typeof fieldOrFields === "string") {
|
|||
|
const whereOperator: Operator = typeof fieldValue !== "undefined"
|
|||
|
? (operatorOrFieldValue as Operator)
|
|||
|
: "=";
|
|||
|
|
|||
|
const whereValue: FieldValue = typeof fieldValue !== "undefined"
|
|||
|
? fieldValue
|
|||
|
: (operatorOrFieldValue as FieldValue);
|
|||
|
|
|||
|
if (whereValue !== undefined) {
|
|||
|
this._currentQuery.where(
|
|||
|
this.formatFieldToDatabase(fieldOrFields) as string,
|
|||
|
whereOperator,
|
|||
|
whereValue,
|
|||
|
);
|
|||
|
}
|
|||
|
} else {
|
|||
|
// TODO(eveningkid): cannot do multiple where with different operators
|
|||
|
// Need to find a great API for multiple where potentially with operators
|
|||
|
// .where({ name: 'John', age: { moreThan: 19 } })
|
|||
|
// and then format it using Knex .andWhere(...)
|
|||
|
|
|||
|
for (const [field, value] of Object.entries(fieldOrFields)) {
|
|||
|
if (value === undefined) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
this._currentQuery.where(
|
|||
|
this.formatFieldToDatabase(field) as string,
|
|||
|
"=",
|
|||
|
value,
|
|||
|
);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Update one or multiple records. Also update `updated_at` if `timestamps` is `true`.
|
|||
|
*
|
|||
|
* await Flight.where("departure", "Dublin").update("departure", "Tokyo");
|
|||
|
*
|
|||
|
* await Flight.where("departure", "Dublin").update({ destination: "Tokyo" });
|
|||
|
*/
|
|||
|
static async update(fieldOrFields: string | Values, fieldValue?: FieldValue) {
|
|||
|
let fieldsToUpdate: Values = {};
|
|||
|
|
|||
|
if (this.timestamps) {
|
|||
|
fieldsToUpdate[
|
|||
|
this.formatFieldToDatabase("updated_at") as string
|
|||
|
] = new Date();
|
|||
|
}
|
|||
|
|
|||
|
if (typeof fieldOrFields === "string") {
|
|||
|
fieldsToUpdate[
|
|||
|
this.formatFieldToDatabase(fieldOrFields) as string
|
|||
|
] = fieldValue!;
|
|||
|
} else {
|
|||
|
fieldsToUpdate = {
|
|||
|
...fieldsToUpdate,
|
|||
|
...(this.formatFieldToDatabase(fieldOrFields) as {
|
|||
|
[fieldName: string]: any;
|
|||
|
}),
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
return this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.update(fieldsToUpdate)
|
|||
|
.toDescription(),
|
|||
|
) as Promise<Model[]>;
|
|||
|
}
|
|||
|
|
|||
|
/** Delete a record by a primary key value.
|
|||
|
*
|
|||
|
* await Flight.deleteById("1");
|
|||
|
*/
|
|||
|
static async deleteById(id: FieldValue) {
|
|||
|
return this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.where(this.getComputedPrimaryKey(), "=", id)
|
|||
|
.delete()
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
/** Delete selected records.
|
|||
|
*
|
|||
|
* await Flight.where("destination", "Paris").delete();
|
|||
|
*/
|
|||
|
static async delete() {
|
|||
|
return this._runQuery(
|
|||
|
this._currentQuery.table(this.table).delete().toDescription(),
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
/** Join a table to the current query.
|
|||
|
*
|
|||
|
* await Flight.where(
|
|||
|
* Flight.field("departure"),
|
|||
|
* "Paris",
|
|||
|
* ).join(
|
|||
|
* Airport,
|
|||
|
* Airport.field("id"),
|
|||
|
* Flight.field("airportId"),
|
|||
|
* ).get()
|
|||
|
*/
|
|||
|
static join<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
joinTable: ModelSchema,
|
|||
|
originField: string,
|
|||
|
targetField: string,
|
|||
|
) {
|
|||
|
this._currentQuery.join(
|
|||
|
joinTable.table,
|
|||
|
joinTable.formatFieldToDatabase(originField) as string,
|
|||
|
this.formatFieldToDatabase(targetField) as string,
|
|||
|
);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Join a table with left outer statement to the current query.
|
|||
|
*
|
|||
|
* await Flight.where(
|
|||
|
* Flight.field("departure"),
|
|||
|
* "Paris",
|
|||
|
* ).leftOuterJoin(
|
|||
|
* Airport,
|
|||
|
* Airport.field("id"),
|
|||
|
* Flight.field("airportId"),
|
|||
|
* ).get()
|
|||
|
*/
|
|||
|
static leftOuterJoin<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
joinTable: ModelSchema,
|
|||
|
originField: string,
|
|||
|
targetField: string,
|
|||
|
) {
|
|||
|
this._currentQuery.leftOuterJoin(
|
|||
|
joinTable.table,
|
|||
|
joinTable.formatFieldToDatabase(originField) as string,
|
|||
|
this.formatFieldToDatabase(targetField) as string,
|
|||
|
);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Join a table with left statement to the current query.
|
|||
|
*
|
|||
|
* await Flight.where(
|
|||
|
* Flight.field("departure"),
|
|||
|
* "Paris",
|
|||
|
* ).leftJoin(
|
|||
|
* Airport,
|
|||
|
* Airport.field("id"),
|
|||
|
* Flight.field("airportId"),
|
|||
|
* ).get()
|
|||
|
*/
|
|||
|
static leftJoin<T extends ModelSchema>(
|
|||
|
this: T,
|
|||
|
joinTable: ModelSchema,
|
|||
|
originField: string,
|
|||
|
targetField: string,
|
|||
|
) {
|
|||
|
this._currentQuery.leftJoin(
|
|||
|
joinTable.table,
|
|||
|
joinTable.formatFieldToDatabase(originField) as string,
|
|||
|
this.formatFieldToDatabase(targetField) as string,
|
|||
|
);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Count the number of records of a model or filtered by a field name.
|
|||
|
*
|
|||
|
* await Flight.count();
|
|||
|
*
|
|||
|
* await Flight.where("destination", "Dublin").count();
|
|||
|
*/
|
|||
|
static async count(field: string = "*") {
|
|||
|
const value = await this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.count(this.formatFieldToDatabase(field) as string)
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
return (value as AggregationResult[])[0].count;
|
|||
|
}
|
|||
|
|
|||
|
/** Find the minimum value of a field from all the selected records.
|
|||
|
*
|
|||
|
* await Flight.min("flightDuration");
|
|||
|
*/
|
|||
|
static async min(field: string) {
|
|||
|
const value = await this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.min(this.formatFieldToDatabase(field) as string)
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
return (value as AggregationResult[])[0].min;
|
|||
|
}
|
|||
|
|
|||
|
/** Find the maximum value of a field from all the selected records.
|
|||
|
*
|
|||
|
* await Flight.max("flightDuration");
|
|||
|
*/
|
|||
|
static async max(field: string) {
|
|||
|
const value = await this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.max(this.formatFieldToDatabase(field) as string)
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
return (value as AggregationResult[])[0].max;
|
|||
|
}
|
|||
|
|
|||
|
/** Compute the sum of a field's values from all the selected records.
|
|||
|
*
|
|||
|
* await Flight.sum("flightDuration");
|
|||
|
*/
|
|||
|
static async sum(field: string) {
|
|||
|
const value = await this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.sum(this.formatFieldToDatabase(field) as string)
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
return (value as AggregationResult[])[0].sum;
|
|||
|
}
|
|||
|
|
|||
|
/** Compute the average value of a field's values from all the selected records.
|
|||
|
*
|
|||
|
* await Flight.avg("flightDuration");
|
|||
|
*
|
|||
|
* await Flight.where("destination", "San Francisco").avg("flightDuration");
|
|||
|
*/
|
|||
|
static async avg(field: string) {
|
|||
|
const value = await this._runQuery(
|
|||
|
this._currentQuery
|
|||
|
.table(this.table)
|
|||
|
.avg(this.formatFieldToDatabase(field) as string)
|
|||
|
.toDescription(),
|
|||
|
);
|
|||
|
|
|||
|
return (value as AggregationResult[])[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 ModelSchema>(
|
|||
|
this: T,
|
|||
|
model: ModelSchema,
|
|||
|
): Promise<Model | Model[]> {
|
|||
|
const currentWhereValue = this._findCurrentQueryWhereClause();
|
|||
|
|
|||
|
if (model.name in this.pivot) {
|
|||
|
const pivot = this.pivot[model.name];
|
|||
|
const pivotField = this.formatFieldToDatabase(
|
|||
|
pivot._pivotsFields[this.name],
|
|||
|
) as string;
|
|||
|
const pivotOtherModel = pivot._pivotsModels[model.name];
|
|||
|
const pivotOtherModelField = pivotOtherModel.formatFieldToDatabase(
|
|||
|
pivot._pivotsFields[model.name],
|
|||
|
) as string;
|
|||
|
|
|||
|
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 ModelSchema>(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] as FieldValue;
|
|||
|
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];
|
|||
|
}
|
|||
|
|
|||
|
/** Return the instance current value for its primary key. */
|
|||
|
private _getCurrentPrimaryKey() {
|
|||
|
const model = this.constructor as ModelSchema;
|
|||
|
return (this as any)[model.getComputedPrimaryKey()] as string;
|
|||
|
}
|
|||
|
|
|||
|
/** Create a new record for the model.
|
|||
|
*
|
|||
|
* const flight = new Flight();
|
|||
|
* flight.departure = "Toronto";
|
|||
|
* flight.destination = "Paris";
|
|||
|
* await flight.save();
|
|||
|
*/
|
|||
|
async save() {
|
|||
|
const model = this.constructor as ModelSchema;
|
|||
|
|
|||
|
const values: Values = {};
|
|||
|
for (const field of Object.keys(model.fields)) {
|
|||
|
if (this.hasOwnProperty(field)) {
|
|||
|
values[field] = (this as any)[field];
|
|||
|
} else if (model.defaults.hasOwnProperty(field)) {
|
|||
|
const defaultValue = model.defaults[field];
|
|||
|
|
|||
|
if (typeof defaultValue === "function") {
|
|||
|
values[field] = defaultValue();
|
|||
|
} else {
|
|||
|
values[field] = defaultValue;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const createdInstance = await model.create(values);
|
|||
|
|
|||
|
for (const field in createdInstance) {
|
|||
|
(this as any)[field] = (createdInstance as any)[field];
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Update this record using its current field values.
|
|||
|
*
|
|||
|
* flight.destination = "London";
|
|||
|
* await flight.update();
|
|||
|
*/
|
|||
|
async update() {
|
|||
|
const model = this.constructor as ModelSchema;
|
|||
|
const modelPK = model.getComputedPrimaryKey();
|
|||
|
|
|||
|
const values: Values = {};
|
|||
|
for (const field of Object.keys(model.fields)) {
|
|||
|
if (this.hasOwnProperty(field) && field !== modelPK) {
|
|||
|
values[field] = (this as any)[field];
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
await model.where(modelPK, this._getCurrentPrimaryKey()).update(
|
|||
|
values,
|
|||
|
);
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
/** Delete this record from the database.
|
|||
|
*
|
|||
|
* await flight.delete();
|
|||
|
*/
|
|||
|
async delete() {
|
|||
|
const model = this.constructor as ModelSchema;
|
|||
|
const PKCurrentValue = this._getCurrentPrimaryKey();
|
|||
|
|
|||
|
if (PKCurrentValue === undefined) {
|
|||
|
throw new Error(
|
|||
|
"This instance does not have a value for its primary key. It cannot be deleted.",
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
return model.deleteById(PKCurrentValue);
|
|||
|
}
|
|||
|
}
|