UNPKG

esix

Version:

A really slick ORM for MongoDB.

727 lines (717 loc) 18.7 kB
"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 });