esix
Version:
A really slick ORM for MongoDB.
727 lines (717 loc) • 18.7 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
BaseModel: () => BaseModel,
QueryBuilder: () => QueryBuilder,
connectionHandler: () => connectionHandler
});
module.exports = __toCommonJS(index_exports);
// src/base-model.ts
var import_reflect_metadata = require("reflect-metadata");
// src/query-builder.ts
var changeCase = __toESM(require("change-case"), 1);
var import_mongodb2 = require("mongodb");
var import_percentile = __toESM(require("percentile"), 1);
var import_pluralize = __toESM(require("pluralize"), 1);
// src/connection-handler.ts
var import_mongo_mock = __toESM(require("mongo-mock"), 1);
var import_mongodb = require("mongodb");
// src/env.ts
function env(key, defaultValue = "") {
const value = process.env[key];
return value || defaultValue;
}
__name(env, "env");
// src/connection-handler.ts
var ConnectionHandler = class ConnectionHandler2 {
static {
__name(this, "ConnectionHandler");
}
client;
/**
* Use this if you want to manually close the open connections. This can be useful if
* you want to gracefully terminate connections in response to a signal.
*/
async closeConnections() {
if (!this.client) {
return;
}
await this.client.close();
this.client = void 0;
}
/**
* Returns a connection to the database. Connections are being pooled
* behind the scenes so there is no need to get multiple connections.
*/
async getConnection() {
return this.getDatabase();
}
async createClient() {
const adapterName = env("DB_ADAPTER", "default").toLowerCase();
const url = env("DB_URL", "mongodb://127.0.0.1:27017/");
const deprecatedPoolSizeEnv = env("DB_POOL_SIZE", "10");
const maxPoolSizeEnv = env("DB_MAX_POOL_SIZE", deprecatedPoolSizeEnv);
const maxPoolSize = parseInt(maxPoolSizeEnv, 10);
const adapters = {
default: import_mongodb.MongoClient,
mock: import_mongo_mock.default.MongoClient
};
if (!adapters.hasOwnProperty(adapterName)) {
const validAdapterNames = Object.keys(adapters).map((name) => `'${name}'`).join(", ");
throw new Error(`${adapterName} is not a valid adapter name. Must be one of ${validAdapterNames}.`);
}
const adapter = adapters[adapterName];
const client = await adapter.connect(url, {
maxPoolSize
});
return client;
}
async getDatabase() {
if (!this.client) {
this.client = await this.createClient();
}
const databaseName = env("DB_DATABASE", "");
return this.client.db(databaseName);
}
};
var connectionHandler = new ConnectionHandler();
// src/sanitize.ts
function sanitize(input) {
if (!isObject(input)) {
return input;
}
if (Array.isArray(input)) {
return input.map((value) => sanitize(value));
}
const keys = Object.keys(input);
return keys.reduce((carry, key) => {
if (isString(key) && key.startsWith("$")) {
return carry;
}
return {
...carry,
[key]: sanitize(input[key])
};
}, {});
}
__name(sanitize, "sanitize");
function isObject(x) {
return typeof x === "object" && x !== null;
}
__name(isObject, "isObject");
function isString(x) {
return typeof x === "string" || x instanceof String;
}
__name(isString, "isString");
// src/query-builder.ts
function isString2(x) {
return typeof x === "string";
}
__name(isString2, "isString");
function normalizeName(className) {
return (0, import_pluralize.default)(changeCase.kebabCase(className));
}
__name(normalizeName, "normalizeName");
function normalizeAttributes(originalAttributes) {
const attributes = {
...originalAttributes
};
if (!attributes.id) {
attributes.id = new import_mongodb2.ObjectId().toHexString();
}
if (attributes.hasOwnProperty("id")) {
attributes._id = attributes.id;
delete attributes.id;
}
if (!attributes["createdAt"]) {
attributes.createdAt = Date.now();
}
if (!attributes["updatedAt"]) {
attributes.updatedAt = null;
}
return attributes;
}
__name(normalizeAttributes, "normalizeAttributes");
var QueryBuilder = class {
static {
__name(this, "QueryBuilder");
}
ctor;
query = {};
queryLimit;
queryOffset;
queryOrder;
constructor(ctor) {
this.ctor = ctor;
}
/**
* Direct access to Mongo's aggregation functions.
*
* @param stages
* @returns The result of the aggregations
*/
async aggregate(stages) {
return this.useCollection(async (collection) => {
const cursor = await collection.aggregate(stages);
return cursor.toArray();
});
}
/**
* Returns the average of all the values for the given key.
*
* @param key
*/
async average(key) {
const values = await this.pluck(key);
if (values.length === 0) {
return 0;
}
if (!isNumberArray(values)) {
throw new Error(`All values returned for ${String(key)} are not numbers. Please check your data.`);
}
const sum = values.reduce((sum2, value) => sum2 + value, 0);
return sum / values.length;
}
/**
* Returns the number of documents matching the given query.
*
* Example
*
* ```
* const numberOfPayingCustomers = await Customer.where('hasPaidTheLastInvoice', true).count();
* ```
*/
async count() {
const count = await this.useCollection((collection) => {
return collection.count(this.query);
});
return count;
}
/**
* Creates a new document with the given attributes.
*
* @internal
*/
async create(attributes) {
const normalizedAttributes = normalizeAttributes(attributes);
return this.useCollection(async (collection) => {
const { insertedId } = await collection.insertOne(normalizedAttributes);
return insertedId;
});
}
/**
* Deletes the Models matching the current query options.
*
* @returns Returns the number of models deleted.
*/
async delete() {
const ids = await this.pluck("id");
return this.useCollection(async (collection) => {
if (ids.length === 0) {
return 0;
}
if (ids.length === 1) {
const [id] = ids;
const { deletedCount: deletedCount2 } = await collection.deleteOne({
_id: id
});
return deletedCount2;
}
const { deletedCount } = await collection.deleteMany({
_id: {
$in: ids
}
});
return deletedCount;
});
}
/**
* Returns the model with the given id or null if there is no matching model.
*/
async find(id) {
return this.useCollection(async (collection) => {
let objectId;
try {
objectId = import_mongodb2.ObjectId.createFromHexString(id);
} catch (error) {
}
const query = objectId ? {
$or: [
{
_id: objectId
},
{
_id: sanitize(id)
}
]
} : {
_id: sanitize(id)
};
const document = await collection.findOne(query);
if (!document) {
return null;
}
return this.createInstance(document);
});
}
/**
* Returns the first model matching the query options.
*
* @internal
*/
async findOne(query) {
return this.useCollection(async (collection) => {
const document = await collection.findOne(sanitize(query));
if (!document) {
return null;
}
return this.createInstance(document);
});
}
/**
* Returns the first model matching the query options.
*/
async first() {
this.queryLimit = 1;
const models = await this.execute();
if (models.length === 0) {
return null;
}
return models[0];
}
/**
* Returns an array of models matching the query options.
*/
async get() {
return this.execute();
}
/**
* Limits the number of models returned.
*
* @param length
*/
limit(length) {
this.queryLimit = length;
return this;
}
/**
* Returns the largest value for the given key.
*
* @param key
*/
async max(key) {
const values = await this.pluck(key);
if (!isNumberArray(values)) {
throw new Error(`All values returned for ${String(key)} are not numbers. Please check your data.`);
}
return Math.max(...values);
}
/**
* Returns the smallest value for the given key.
*
* @param key
*/
async min(key) {
const values = await this.pluck(key);
if (!isNumberArray(values)) {
throw new Error(`All values returned for ${String(key)} are not numbers. Please check your data.`);
}
return Math.min(...values);
}
/**
* Sorts the models by the given key.
*
* @param key The key you want to sort by.
* @param order Defaults to ascending order.
*/
orderBy(key, order = "asc") {
if (!this.queryOrder) {
this.queryOrder = {};
}
this.queryOrder[key] = order === "asc" ? 1 : -1;
return this;
}
/**
* Returns the nth percentile of all the values for the given key.
*
* @param key
* @param n
*/
async percentile(key, n) {
const values = await this.pluck(key);
if (values.length === 0) {
return 0;
}
if (!isNumberArray(values)) {
throw new Error(`All values returned for ${String(key)} are not numbers. Please check your data.`);
}
const p = (0, import_percentile.default)(n, values);
return typeof p === "number" ? p : p[0];
}
/**
* The pluck method retrieves all of the values for a given key.
*
* You may also specify how you wish the resulting collection to be keyed.
*
* Example
* ```
* await Posts.where('categoryId', 2).pluck('id');
* // => [ '1', '2', '3' ]
*/
async pluck(key) {
const records = await this.execute({
[key]: 1
});
const values = records.map((record) => record[key]);
return values;
}
/**
* Persist the provided attributes.
*
* @param attributes
* @internal
*/
async save(attributes) {
attributes = normalizeAttributes(sanitize(attributes));
const id = attributes._id;
return this.useCollection(async (collection) => {
const filter = {
_id: id
};
const options = {
upsert: true
};
await collection.updateOne(filter, {
$set: attributes
}, options);
return id;
});
}
/**
* Skips the first `length` models. Useful for pagination.
*
* @param length
*/
skip(length) {
this.queryOffset = length;
return this;
}
/**
* Returns the sum of all the values for the given key.
*
* @param key
*/
async sum(key) {
const values = await this.pluck(key);
if (!isNumberArray(values)) {
throw new Error(`All values returned for ${String(key)} are not numbers. Please check your data.`);
}
return values.reduce((sum, value) => sum + value, 0);
}
where(queryOrKey, value) {
const query = isString2(queryOrKey) ? {
[queryOrKey]: value
} : queryOrKey;
this.query = {
...this.query,
...sanitize(query)
};
return this;
}
/**
* Returns all the models with `fieldName` in the array of `values`.
*
* @param fieldName
* @param values
*/
whereIn(fieldName, values) {
if (fieldName === "id") {
fieldName = "_id";
}
const query = {
[fieldName]: {
$in: sanitize(values)
}
};
this.query = {
...this.query,
...query
};
return this;
}
createInstance(document) {
const instance = new this.ctor();
for (const prop in document) {
if (prop === "_id") {
continue;
}
instance[prop] = document[prop];
}
const id = isString2(document._id) ? document._id : document._id.toHexString();
instance.id = id;
return instance;
}
execute(fields) {
return this.useCollection(async (collection) => {
let cursor = fields ? collection.find(this.query, fields) : collection.find(this.query);
if (this.queryOrder) {
cursor = cursor.sort(this.queryOrder);
}
if (this.queryOffset) {
cursor = cursor.skip(this.queryOffset);
}
if (this.queryLimit) {
cursor = cursor.limit(this.queryLimit);
}
const documents = await cursor.toArray();
const records = documents.filter((document) => document).map((document) => this.createInstance(document));
return records;
});
}
async useCollection(block) {
const collectionName = normalizeName(this.ctor.name);
const connection = await connectionHandler.getConnection();
const collection = await connection.collection(collectionName);
const result = await block(collection);
return result;
}
};
function isNumberArray(array) {
return array.every((item) => typeof item === "number");
}
__name(isNumberArray, "isNumberArray");
// src/base-model.ts
var import_change_case = require("change-case");
var BaseModel = class {
static {
__name(this, "BaseModel");
}
createdAt = 0;
id = "";
updatedAt = null;
/**
* Returns all models.
*
* Example
* ```
* const posts = await BlogPost.all();
* ```
*/
static async all() {
return new QueryBuilder(this).where({}).get();
}
/**
* Creates a new model with the given attributes. The Id will be automatically generated
* if none is provided.
*
* Example
* ```
* const post = await BlogPost.create({ title: 'My First Blog Post!' });
* ```
*
* @param attributes
*/
static async create(attributes) {
const queryBuilder = new QueryBuilder(this);
const instance = new this();
const defaultValues = Object.getOwnPropertyNames(instance).reduce((acc, key) => {
acc[key] = instance[key];
return acc;
}, {});
const attributesWithDefaults = {
...defaultValues,
...attributes
};
const id = await queryBuilder.create(attributesWithDefaults);
const model = await queryBuilder.findOne({
_id: id
});
if (!model) {
throw new Error("Failed to create model.");
}
return model;
}
/**
* Returns the model with the given id.
*
* Example
* ```
* const post = await BlogPost.find('5f5a41cc3eb990709eafda43');
* ```
*
* @param id
*/
static async find(id) {
return new QueryBuilder(this).find(id);
}
/**
* Returns the first model matching where the `key` matches `value`.
*
* Example
* ```
* const user = await User.findBy('email', 'john.smith@company.com');
* ```
*
* @param key
* @param value
*/
static async findBy(key, value) {
return new QueryBuilder(this).findOne({
[key]: value
});
}
/**
* Limits the number of models returned.
*
* @param length
*/
static limit(length) {
return new QueryBuilder(this).limit(length);
}
/**
* Specifies the order the models are returned.
*
* Example
* ```
* const posts = await BlogPost.orderBy('publishedAt', 'desc').get();
* ```
*
* @param key
* @param order
*/
static orderBy(key, order = "asc") {
return new QueryBuilder(this).orderBy(key, order);
}
/**
* Returns an array of values for the given key.
*
* Example
* ```
* const titles = await BlogPost.pluck('title');
* ```
*
* @param key
*/
static pluck(key) {
return new QueryBuilder(this).pluck(key);
}
/**
* Skips {length} number of models.
*
* @param length
*/
static skip(length) {
return new QueryBuilder(this).skip(length);
}
/**
* Returns a QueryBuilder where `key` matches `value`.
*
* Example
* ```
* const posts = await BlogPost.where('status', 'published').get();
* ```
*
* @param key
* @param value
*/
static where(key, value) {
return new QueryBuilder(this).where(key, value);
}
/**
* Returns models where `key` is in the array of `values`.
*
* Example
* ```
* const comments = await Comment.whereIn('postId', [1, 2, 3]).get();
* ```
*
* @param fieldName
* @param values
*/
static whereIn(fieldName, values) {
const queryBuilder = new QueryBuilder(this);
return queryBuilder.whereIn(fieldName, values);
}
/**
* Deletes the model from the database.
*
* Example
* ```
* await post.delete();
* ```
*/
async delete() {
const queryBuilder = new QueryBuilder(this.constructor);
return queryBuilder.where({
_id: this.id
}).limit(1).delete();
}
hasMany(ctor, foreignKey, localKey) {
const queryBuilder = new QueryBuilder(ctor);
foreignKey = foreignKey || (0, import_change_case.camelCase)(`${this.constructor.name}Id`);
localKey = localKey || "id";
return queryBuilder.where(foreignKey, this[localKey]);
}
/**
* Persists the current changes to the database.
*
* Example
* ```
* const post = new Post();
*
* post.title = 'My Second Blog Post!';
*
* await post.save();
* ```
*/
async save() {
const queryBuilder = new QueryBuilder(this.constructor);
if (this.id) {
this.updatedAt = Date.now();
} else {
this.createdAt = Date.now();
}
const attributes = {
...this
};
const id = await queryBuilder.save(attributes);
if (!this.id) {
this.id = id;
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BaseModel,
QueryBuilder,
connectionHandler
});