swc/crates/swc_bundler/tests/.cache/deno/9f5cdcfcb2729cc827f25c504e9bc61290f17beb.ts
2021-11-09 20:42:49 +09:00

992 lines
27 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}
}