@thisisagile/easy-mongo
Version:
Add support for MongoDB
566 lines (556 loc) • 23 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc2) => {
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: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
AtlasSearchGateway: () => AtlasSearchGateway,
Collection: () => Collection,
FilterBuilder: () => FilterBuilder,
IncludeBuilder: () => IncludeBuilder,
MongoGateway: () => MongoGateway,
MongoProvider: () => MongoProvider,
SortBuilder: () => SortBuilder,
asc: () => asc,
desc: () => desc,
lucene: () => lucene,
stages: () => stages,
toFilters: () => toFilters,
toMongoType: () => toMongoType
});
module.exports = __toCommonJS(src_exports);
// src/Stages.ts
var import_easy2 = require("@thisisagile/easy");
// src/Utils.ts
var import_easy = require("@thisisagile/easy");
var toMongoType = (input) => (0, import_easy.choose)(input).type(import_easy.isIsoDateString, (i) => new import_easy.DateTime(i).toDate()).type(import_easy.isDateTime, (i) => i.toDate()).type(import_easy.isArray, (a) => a.map((i) => toMongoType(i))).type(import_easy.isDate, (d) => d).type(
import_easy.isObject,
(o) => Object.fromEntries(
(0, import_easy.meta)(o).entries().map(([k, i]) => [k, toMongoType(i)])
)
).else(input);
// src/Stages.ts
var asc = 1;
var desc = -1;
var FilterBuilder = class {
constructor(filters) {
this.filters = filters;
}
from = (q = {}) => stages.match.match(
(0, import_easy2.meta)(q).entries().reduce((acc, [key, value]) => ({ ...acc, ...(0, import_easy2.ifDefined)(this.filters[key], (f) => f(value)) }), {})
);
};
var SortBuilder = class {
constructor(sorts) {
this.sorts = sorts;
}
get keys() {
return Object.keys(this.sorts);
}
from = (s = {}, alt) => stages.sort.sort(this.sorts[s?.s ?? ""] ?? this.sorts[alt ?? ""]);
};
var IncludeBuilder = class {
constructor(includes) {
this.includes = includes;
}
get keys() {
return Object.keys(this.includes);
}
from = (i = {}, alt) => stages.project.include(...this.includes[i?.i ?? ""] ?? this.includes[alt ?? ""] ?? []);
};
var escapeRegex = (s) => s.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
var stages = {
root: "$$ROOT",
current: "$$CURRENT",
id: "_id",
decode: {
object: (f) => (0, import_easy2.use)(Object.entries(f)[0], ([k, v]) => (0, import_easy2.ofGet)(v, k)),
fields: (f) => Object.entries(f).reduce((res, [k, v]) => (0, import_easy2.on)(res, (r) => (0, import_easy2.ifDefined)((0, import_easy2.ofGet)(v, k), (nv) => r[k] = nv)), {}),
fieldsArrays: (f) => Object.entries(f).reduce((res, [k, v]) => (0, import_easy2.on)(res, (r) => r[k] = (0, import_easy2.use)((0, import_easy2.toArray)(v), (vs) => vs.map((v2) => (0, import_easy2.ofGet)(v2, k)))), {}),
id: (f) => (0, import_easy2.isString)(f) ? `$${(0, import_easy2.asString)(f)}` : (0, import_easy2.isPrimitive)(f) ? f : Object.entries(f).map(([k, v]) => (0, import_easy2.ofGet)(v, k))[0]
},
match: {
match: (f) => ({ $match: stages.decode.fields(f) }),
filter: (filters) => new FilterBuilder(filters),
or: (...filters) => ({ $or: (0, import_easy2.toArray)(filters).map((f) => stages.decode.object(f)) }),
gt: (value) => ({ $gt: value }),
gte: (value) => ({ $gte: value }),
lt: (value) => ({ $lt: value }),
lte: (value) => ({ $lte: value }),
isIn: (value, separator = ",") => ({ $in: (0, import_easy2.isString)(value) ? value.split(separator) : value }),
notIn: (value, separator = ",") => ({ $nin: (0, import_easy2.isString)(value) ? value.split(separator) : value }),
after: (date) => stages.match.gte(toMongoType(date)),
before: (date) => stages.match.lt(toMongoType(date)),
anywhere: (q) => ({ $regex: escapeRegex(q), $options: "i" }),
money: (currency, value) => (key) => ({
[`${key}.currency`]: currency.id,
...stages.decode.fields({ [`${key}.value`]: value })
})
},
sort: {
sort: ($sort) => (0, import_easy2.isPresent)($sort) ? { $sort } : void 0,
sorter: (sorts) => new SortBuilder(sorts),
asc: (key) => stages.sort.sort({ [key]: asc }),
desc: (key) => stages.sort.sort({ [key]: desc })
},
group: {
group: (fields) => ({
by: (by) => ({ $group: Object.assign({ _id: stages.decode.id(by) }, stages.decode.fields(fields)) })
}),
date: (format = "%Y-%m-%d") => (key) => ({ $dateToString: { date: `$${key}`, format } }),
count: () => ({ $count: {} }),
sum: (from) => (0, import_easy2.isDefined)(from) ? { $sum: `$${from}` } : { $sum: 1 },
avg: (from) => ({ $avg: `$${from}` }),
multiply: (...multiply) => ({ $multiply: multiply.map((m) => `$${m}`) }),
first: (from) => ({ $first: `$${from}` }),
last: (from) => ({ $last: `$${from}` }),
min: (from) => ({ $min: `$${from}` }),
max: (from) => ({ $max: `$${from}` }),
addToSet: (from) => ({ $addToSet: `$${from}` }),
push: (from = "$ROOT") => ({ $push: `$${from}` }),
size: (from) => ({ $size: `$${from}` })
},
search: {
search: (f) => (0, import_easy2.ifDefined)(stages.decode.id(f), ($search) => ({ $search })),
auto: (value) => (key) => (0, import_easy2.ifDefined)(value, (v) => ({ autocomplete: { path: key, query: [v] } })),
fuzzy: (value, maxEdits = 1) => (key) => (0, import_easy2.ifDefined)(value, (v) => ({
text: {
query: v,
path: key === "wildcard" ? { wildcard: "*" } : key,
fuzzy: { maxEdits }
}
}))
},
set: {
set: (f) => ({ $set: stages.decode.fields(f) }),
score: () => ({ $meta: "searchScore" })
},
skip: {
skip: (o = {}) => (0, import_easy2.ifDefined)(o.skip, { $skip: (0, import_easy2.asNumber)(o.skip) }),
take: (o = {}) => (0, import_easy2.ifDefined)(o.take, { $limit: (0, import_easy2.asNumber)(o.take) })
},
project: {
include: (...includes) => (0, import_easy2.ifNotEmpty)(includes, (es) => ({ $project: es.reduce((a, b) => ({ ...a, ...(0, import_easy2.isString)(b) ? { [b]: 1 } : b }), {}) })),
exclude: (...excludes) => (0, import_easy2.ifNotEmpty)(excludes, (es) => ({ $project: es.reduce((a, b) => ({ ...a, ...(0, import_easy2.isString)(b) ? { [b]: 0 } : b }), {}) })),
includes: (includes) => new IncludeBuilder(includes),
project: (project) => (0, import_easy2.ifDefined)(project, ($project) => ({ $project })),
date: (key, format) => ({ $toDate: `$${key}`, ...(0, import_easy2.ifDefined)(format, { format }) }),
duration: (from, to) => ({ $divide: [{ $subtract: [stages.project.date(from), stages.project.date(to)] }, 1e3] })
},
replaceWith: {
replaceWith: (f) => (0, import_easy2.ifDefined)(f, { $replaceWith: f }),
merge: (...objects) => (0, import_easy2.ifNotEmpty)(objects, (os) => ({ $mergeObjects: os })),
rootAnd: (...objects) => stages.replaceWith.merge(stages.root, ...objects),
currentAnd: (...objects) => stages.replaceWith.merge(stages.current, ...objects),
reroot: (prop) => ({ $replaceRoot: { newRoot: `$${prop}` } }),
concat: (...props) => (0, import_easy2.ifNotEmpty)(props, (ps) => ({ $concatArrays: ps.map((p) => `$${p}`) }))
},
facet: {
facet: (f) => ({ $facet: stages.decode.fieldsArrays(f) }),
unwind: (from) => (f) => ({ $unwind: `$${from ?? f}` }),
count: (from) => (f) => ({ $sortByCount: `$${from ?? f}` }),
data: () => []
},
unwind: {
unwind: (prop) => ({ $unwind: `$${prop}` })
}
};
// src/MongoGateway.ts
var import_easy3 = require("@thisisagile/easy");
var MongoGateway = class {
constructor(collection, provider = collection.provider) {
this.collection = collection;
this.provider = provider;
}
all(options) {
return this.provider.all(options).then((js) => js.map((j) => this.collection.in(j)));
}
byId(id) {
return this.provider.byId(id).then((j) => (0, import_easy3.ifDefined)(j, this.collection.in(j)));
}
by(key, value, options) {
return this.provider.by(key, value, options).then((js) => js.map((j) => this.collection.in(j)));
}
byIds(...ids) {
return this.find(this.collection.id.isIn(...ids));
}
find(q, options) {
return this.provider.find((0, import_easy3.asJson)(q), options).then((js) => js.map((j) => this.collection.in(j)));
}
search(q, options) {
return this.find(this.collection.google(q), options);
}
filter(options) {
return this.all(options);
}
exists(id) {
return this.provider.byId(id).then((i) => (0, import_easy3.isDefined)(i));
}
aggregate(...filters) {
return this.provider.aggregate((0, import_easy3.toArray)(...filters).filter(import_easy3.isPresent));
}
count(...filters) {
return this.aggregate(...filters, { $count: "total" }).then((d) => d.first()?.total ?? 0);
}
match(f) {
return this.aggregate(stages.match.match(f));
}
add(item) {
return this.provider.add(this.collection.out(item)).then((j) => this.collection.in(j));
}
update(item) {
return this.provider.update(this.collection.out(item)).then((j) => this.collection.in(j));
}
remove(id) {
return this.provider.remove(id);
}
};
// src/Lucene.ts
var import_easy4 = require("@thisisagile/easy");
var should = (query, def) => (0, import_easy4.entries)(query).mapDefined(([k, v]) => def[k]?.(v, query)?.should);
var must = (query, def) => (0, import_easy4.entries)(query).mapDefined(([k, v]) => def[k]?.(v, query)?.must);
var mustNot = (query, def) => (0, import_easy4.entries)(query).mapDefined(([k, v]) => def[k]?.(v, query)?.mustNot);
var filter = (query, def) => (0, import_easy4.entries)(query).mapDefined(([k, v]) => def[k]?.(v, query)?.filter);
var lucene = {
clause: (c) => (0, import_easy4.entries)(c).reduce((res, [k, v]) => res.add((0, import_easy4.isFunction)(v) ? v(k) : v), (0, import_easy4.toList)()),
clauses: (cs) => (0, import_easy4.toArray)(cs).flatMap((c) => lucene.clause(c)),
compound: (query, def, wildcard = true) => (0, import_easy4.ifNotEmpty)(
(0, import_easy4.entries)({
should: should(query, def),
filter: filter(query, def),
mustNot: mustNot(query, def),
must: must(query, def)
}).filter(([_, v]) => v.length > 0),
(e) => e.reduce((res, [k, v]) => (0, import_easy4.on)(res, (r) => r[k] = lucene.clauses(v)), should(query, def).length > 0 ? { minimumShouldMatch: 1 } : {}),
() => (0, import_easy4.ifTrue)(wildcard, () => ({
should: lucene.clauses([{ r: { exists: { path: "id" } } }]),
minimumShouldMatch: 0
}))
),
search: (c, index) => ({
$search: {
...(0, import_easy4.ifDefined)(index, { index }),
compound: (0, import_easy4.entries)(c).reduce((res, [k, v]) => (0, import_easy4.on)(res, (r) => r[k] = lucene.clauses(v)), {})
}
}),
searchWithDef: (query, options, count = "total", index) => {
const sort = (0, import_easy4.entries)(query).mapDefined(([k, v]) => options[k]?.(v, query)?.sort).first();
return {
$search: {
...(0, import_easy4.ifDefined)(index, { index }),
compound: lucene.compound(query, options),
...(0, import_easy4.ifDefined)(sort, { sort }),
count: { type: count }
}
};
},
searchMeta: (query, def, count = "total", index) => ({
$searchMeta: {
...(0, import_easy4.ifDefined)(index, { index }),
...(0, import_easy4.ifTrue)(
!(0, import_easy4.isEmptyObject)(lucene.facets(def)),
{
facet: {
operator: {
compound: lucene.compound(query, def)
},
facets: lucene.facets(def)
}
},
{ compound: lucene.compound(query, def) }
),
count: { type: count }
}
}),
exists: () => (path) => ({ exists: { path } }),
equals: (value) => (path) => ({ equals: { path, value: value === 1 } }),
text: (value, fuzzy) => (path) => (0, import_easy4.ifDefined)(value, (v) => ({
text: {
path: path === "wildcard" ? { wildcard: "*" } : path,
query: v,
...(0, import_easy4.ifDefined)(fuzzy, { fuzzy })
}
})),
wildcard: (value, allowAnalyzedField = true) => (path) => ({
wildcard: {
path: path === "wildcard" ? { wildcard: "*" } : path,
query: (0, import_easy4.ifDefined)(value, value, "*"),
allowAnalyzedField
}
}),
lt: (value) => (path) => (0, import_easy4.ifDefined)(value, (lt) => ({ range: { path, lt } })),
lte: (value) => (path) => (0, import_easy4.ifDefined)(value, (lte) => ({ range: { path, lte } })),
gt: (value) => (path) => (0, import_easy4.ifDefined)(value, (gt) => ({ range: { path, gt } })),
gte: (value) => (path) => (0, import_easy4.ifDefined)(value, (gte) => ({ range: { path, gte } })),
after: (date) => lucene.gte(toMongoType(date)),
before: (date) => lucene.lt(toMongoType(date)),
between: (after, before, includeLimit) => (path) => {
const upperLimit = includeLimit ? { lte: toMongoType(before) } : { lt: toMongoType(before) };
return {
range: {
path,
gte: toMongoType(after),
...upperLimit
}
};
},
facets: (def) => (0, import_easy4.entries)(def).filter(([k, v]) => (0, import_easy4.isDefined)(v(k)?.facet)).map(([k, v]) => ({ [k]: v(k)?.facet })).reduce((acc, v) => ({ ...acc, ...v }), {}),
facet: {
string: (path, numBuckets = 1e3) => ({
type: "string",
path,
numBuckets
}),
number: (path, boundaries, alt) => ({
type: "number",
path,
boundaries,
...(0, import_easy4.ifDefined)(alt, (a) => ({ default: a }))
}),
date: (path, boundaries, alt) => ({
type: "date",
path,
boundaries: boundaries.mapDefined((b) => b.toDate()),
...(0, import_easy4.ifDefined)(alt, (a) => ({ default: a }))
})
}
};
// src/AtlasSearchGateway.ts
var import_easy5 = require("@thisisagile/easy");
var { skip, take } = stages.skip;
var { replaceWith } = stages.replaceWith;
var { searchWithDef, searchMeta, facets } = lucene;
var toFilters = (facets2) => (0, import_easy5.entries)(facets2).map(([k, fs]) => ({
label: (0, import_easy5.text)(k).title.toString(),
field: k,
values: fs.map((f) => toValue(f))
}));
var toValue = (f) => ({ label: f._id ?? "unknown", value: f._id ?? "unknown", count: f.count });
var AtlasSearchGateway = class extends MongoGateway {
constructor(collection, searchDef, sortDef = {}, provider = collection.provider) {
super(collection, provider);
this.searchDef = searchDef;
this.sortDef = sortDef;
}
query(query, additionalStages = []) {
return (0, import_easy5.tuple2)(
this.aggregate(
searchWithDef(query, this.searchDef),
skip({ skip: query?.skip ?? 0 }),
take({ take: query?.take ?? 250 }),
...additionalStages
),
this.aggregate(
searchMeta(query, this.searchDef),
replaceWith({
total: "$count.total",
facets: Object.keys(facets(this.searchDef)).reduce((acc, k) => ({ ...acc, [k]: `$facet.${k}.buckets` }), {})
})
)
).then(([data, meta3]) => ({ data, meta: meta3.first() })).then(
({ data, meta: meta3 }) => (0, import_easy5.toPageList)(data, {
total: (0, import_easy5.asNumber)(meta3?.total, 0),
skip: (0, import_easy5.asNumber)(query?.skip, 0),
take: (0, import_easy5.asNumber)(query?.take, 250),
sorts: Object.keys(this.sortDef),
filters: toFilters(meta3.facets)
})
);
}
};
// src/Collection.ts
var import_easy7 = require("@thisisagile/easy");
// src/MongoProvider.ts
var import_easy6 = require("@thisisagile/easy");
var import_mongodb = require("mongodb");
var omitId = (j) => import_easy6.json.delete(j, "_id");
function omitOptions(obj) {
const { maxTimeMS, ...rest } = obj ?? {};
return rest;
}
var MongoProvider = class _MongoProvider {
constructor(coll) {
this.coll = coll;
}
static clients = {};
static destroyAll() {
return Promise.all((0, import_easy6.entries)(_MongoProvider.clients).map(([u, c]) => c.then((c2) => c2.close()).then(() => delete _MongoProvider.clients[u]))).then(
() => void 0
);
}
static connect(u, db) {
return import_mongodb.MongoClient.connect(u, {
auth: {
username: (0, import_easy6.asString)(db.options?.user),
password: (0, import_easy6.asString)(db.options?.password)
},
...db.options?.maxPoolSize && { maxPoolSize: db.options?.maxPoolSize },
...db.options?.minPoolSize && { minPoolSize: db.options?.minPoolSize },
...db.options?.maxIdleTimeMS && { maxIdleTimeMS: db.options?.maxIdleTimeMS },
...db.options?.socketTimeoutMS && { socketTimeoutMS: db.options?.socketTimeoutMS }
}).then((c) => {
c.on("error", () => delete _MongoProvider.clients[u]);
c.on("close", () => delete _MongoProvider.clients[u]);
return c;
}).catch((err) => {
delete _MongoProvider.clients[u];
return Promise.reject(err);
});
}
cluster() {
return (0, import_easy6.use)(
this.coll.db,
(db) => (0, import_easy6.when)(db.options?.cluster).not.isDefined.reject(import_easy6.Exception.IsNotValid.because("Missing cluster in database options.")).then((c) => _MongoProvider.clients[c] ?? (_MongoProvider.clients[c] = _MongoProvider.connect(c, db)))
);
}
collection() {
return this.cluster().then((c) => c.db(this.coll.db.name)).then((db) => db.collection((0, import_easy6.asString)(this.coll)));
}
toMongoJson(query) {
return toMongoType((0, import_easy6.asJson)(query));
}
withTimeout(options) {
return { ...options, maxTimeMS: options?.maxTimeMS ?? this.coll.db?.options?.queryTimeoutMS ?? 3e4 };
}
find(query, options) {
return (0, import_easy6.tuple3)(this.collection(), this.toMongoJson(query), this.toFindOptions(options)).then(
([c, q, o]) => (0, import_easy6.tuple2)(
c.find(q, o),
(0, import_easy6.ifTrue)(o.total, () => c.countDocuments(q, { maxTimeMS: this.withTimeout(options).maxTimeMS }))
)
).then(([res, total]) => this.toArray(res, { ...omitOptions(options), total }));
}
all(options) {
return this.find({}, options);
}
byId(id, options) {
return this.collection().then((c) => c.findOne(this.toMongoJson({ id }), this.toFindOptions(options)));
}
by(key, value, options) {
return this.find({ [key]: value }, options);
}
group(qs, options) {
return this.aggregate(qs, options);
}
aggregate(qs, options) {
return this.collection().then(
(c) => c.aggregate(
qs.map((q) => this.toMongoJson(q)),
this.withTimeout(options)
)
).then((res) => this.toArray(res));
}
add(item) {
return this.collection().then((c) => c.insertOne(omitId(item))).then(() => omitId(item));
}
update(item) {
return this.collection().then((c) => c.updateOne(this.toMongoJson({ id: item.id }), { $set: omitId(item) })).then(() => this.byId(item.id));
}
remove(id) {
return this.collection().then((c) => c.deleteOne(this.toMongoJson({ id }))).then((d) => d.acknowledged);
}
count(query, options) {
return this.collection().then((c) => c.countDocuments(this.toMongoJson(query ?? {}), this.withTimeout(options)));
}
createIndex(indexes, options) {
return this.collection().then((c) => c.createIndex(this.toIndexSpecification(indexes), this.toCreateIndexesOptions(options)));
}
createPartialIndex(indexes, filter2, options) {
return this.createIndex(indexes, { ...options, filter: filter2 });
}
createTextIndex(indexes, options) {
const ii = (0, import_easy6.toArray)(indexes).reduce((i, f) => ({ ...i, [(0, import_easy6.asString)(f)]: "text" }), {});
return this.createIndex(ii, { unique: false, ...options });
}
toFindOptions(options) {
return {
limit: (0, import_easy6.asNumber)(options?.take ?? 250),
...options?.skip && { skip: (0, import_easy6.asNumber)(options?.skip) },
...options?.sorts && { sort: options?.sorts } || options?.sort && { sort: this.coll.sort(...options?.sort ?? []) },
total: (0, import_easy6.isDefined)(options?.skip) || (0, import_easy6.isDefined)(options?.take),
projection: options?.projection ?? { _id: 0 },
maxTimeMS: this.withTimeout(options).maxTimeMS
};
}
toIndexSpecification(index) {
return (0, import_easy6.choose)(index).type(import_easy6.isField, (f) => f.property).type(import_easy6.isSortCondition, (s) => s.toJSON()).type(import_easy6.isArray, (aa) => aa.map((a) => this.toIndexSpecification(a))).else((i) => i);
}
toCreateIndexesOptions(options) {
return {
unique: options?.unique ?? true,
...options?.languageOverride && { language_override: options.languageOverride },
...options?.languageDefault && { default_language: options.languageDefault },
...options?.filter && { partialFilterExpression: toMongoType((0, import_easy6.asJson)(options.filter)) }
};
}
toArray(cursor, options) {
return cursor.toArray().then((r) => (0, import_easy6.toPageList)(r, options));
}
};
// src/Collection.ts
var Collection = class extends import_easy7.Mapper {
map = {
...import_easy7.mappings,
field: (name, options) => new import_easy7.Field(name, options)
};
id = this.map.field("id", { dflt: import_easy7.toUuid });
constructor(options = { startFrom: "source" }) {
super(options);
}
get db() {
return import_easy7.Database.Default;
}
get provider() {
return new MongoProvider(this);
}
where = (...conditions) => new import_easy7.LogicalCondition(
"and",
conditions.map((c) => (0, import_easy7.ofGet)(c, this))
).toJSON();
match = (condition) => ({ $match: (0, import_easy7.ofGet)(condition, this).toJSON() });
group = (...conditions) => new import_easy7.LogicalCondition(
"group",
conditions.map((c) => (0, import_easy7.ofGet)(c, this))
).toJSON();
google = (value) => (0, import_easy7.toCondition)("$text", "search", value);
search = (key) => this.map.field((0, import_easy7.asString)(key));
sort = (...conditions) => conditions.reduce((cs, c) => {
cs[c.key] = c.value;
return cs;
}, {});
out(to = {}) {
return toMongoType(super.out(to));
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AtlasSearchGateway,
Collection,
FilterBuilder,
IncludeBuilder,
MongoGateway,
MongoProvider,
SortBuilder,
asc,
desc,
lucene,
stages,
toFilters,
toMongoType
});
//# sourceMappingURL=index.js.map