@primate/mongodb 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/private/ColumnTypes.d.ts +1 -1
- package/lib/private/MongoDB.d.ts +42 -0
- package/lib/private/MongoDB.js +376 -0
- package/lib/private/typemap.d.ts +1 -1
- package/lib/private/typemap.js +1 -13
- package/lib/public/index.d.ts +2 -2
- package/lib/public/index.js +2 -2
- package/package.json +19 -12
- package/lib/private/Database.d.ts +0 -45
- package/lib/private/Database.js +0 -147
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type TypedArray from "@rcompat/type
|
|
1
|
+
import type { TypedArray } from "@rcompat/type";
|
|
2
2
|
import type { Binary, Decimal128, ObjectId } from "mongodb";
|
|
3
3
|
type Param = bigint | Binary | boolean | Date | Decimal128 | number | ObjectId | string | TypedArray;
|
|
4
4
|
type Validate<T extends {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { As, DataDict, DB, Sort, With } from "@primate/core/db";
|
|
2
|
+
import type { Dict } from "@rcompat/type";
|
|
3
|
+
import type { StoreSchema } from "pema";
|
|
4
|
+
declare const schema: import("pema").ObjectType<{
|
|
5
|
+
database: import("pema").StringType;
|
|
6
|
+
host: import("pema").DefaultType<import("pema").StringType, "localhost">;
|
|
7
|
+
password: import("pema").OptionalType<import("pema").StringType>;
|
|
8
|
+
port: import("pema").DefaultType<import("pema").UintType<"u32">, 27017>;
|
|
9
|
+
username: import("pema").OptionalType<import("pema").StringType>;
|
|
10
|
+
}>;
|
|
11
|
+
export default class MongoDB implements DB {
|
|
12
|
+
#private;
|
|
13
|
+
static config: typeof schema.input;
|
|
14
|
+
constructor(config?: typeof schema.input);
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
get schema(): {
|
|
17
|
+
create: (_as: As, _store: StoreSchema) => Promise<void>;
|
|
18
|
+
delete: (name: string) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
create<O extends Dict>(as: As, record: Dict): Promise<O>;
|
|
21
|
+
read(as: As, args: {
|
|
22
|
+
count: true;
|
|
23
|
+
where: DataDict;
|
|
24
|
+
with?: never;
|
|
25
|
+
}): Promise<number>;
|
|
26
|
+
read(as: As, args: {
|
|
27
|
+
where: DataDict;
|
|
28
|
+
fields?: string[];
|
|
29
|
+
limit?: number;
|
|
30
|
+
sort?: Sort;
|
|
31
|
+
with?: With;
|
|
32
|
+
}): Promise<Dict[]>;
|
|
33
|
+
update(as: As, args: {
|
|
34
|
+
set: DataDict;
|
|
35
|
+
where: DataDict;
|
|
36
|
+
}): Promise<number>;
|
|
37
|
+
delete(as: As, args: {
|
|
38
|
+
where: DataDict;
|
|
39
|
+
}): Promise<number>;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=MongoDB.d.ts.map
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import typemap from "#typemap";
|
|
2
|
+
import common from "@primate/core/db";
|
|
3
|
+
import E from "@primate/core/db/error";
|
|
4
|
+
import assert from "@rcompat/assert";
|
|
5
|
+
import is from "@rcompat/is";
|
|
6
|
+
import { MongoClient, ObjectId } from "mongodb";
|
|
7
|
+
import p from "pema";
|
|
8
|
+
const schema = p({
|
|
9
|
+
database: p.string,
|
|
10
|
+
host: p.string.default("localhost"),
|
|
11
|
+
password: p.string.optional(),
|
|
12
|
+
port: p.uint.port().default(27017),
|
|
13
|
+
username: p.string.optional(),
|
|
14
|
+
});
|
|
15
|
+
function is_object_id(x) {
|
|
16
|
+
return x instanceof ObjectId;
|
|
17
|
+
}
|
|
18
|
+
function get_limit(limit) {
|
|
19
|
+
return limit ?? 0; // 0 = no limit
|
|
20
|
+
}
|
|
21
|
+
function get_sort(sort) {
|
|
22
|
+
if (sort === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
const entries = Object.entries(sort);
|
|
25
|
+
if (entries.length === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [k, dir] of entries) {
|
|
29
|
+
out[k] = dir.toLowerCase() === "desc" ? -1 : 1;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function get_projection(pk, fields) {
|
|
34
|
+
if (fields === undefined || fields.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
const out = {};
|
|
37
|
+
const has_pk = pk !== null && fields.includes(pk);
|
|
38
|
+
// MongoDB always includes _id unless explicitly excluded
|
|
39
|
+
if (!has_pk)
|
|
40
|
+
out._id = 0;
|
|
41
|
+
for (const field of fields) {
|
|
42
|
+
const key = field === pk ? "_id" : field;
|
|
43
|
+
out[key] = 1;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
async function bind_value(key, value) {
|
|
48
|
+
if (value === null)
|
|
49
|
+
return null;
|
|
50
|
+
return await typemap[key].bind(value);
|
|
51
|
+
}
|
|
52
|
+
function unbind_value(key, value) {
|
|
53
|
+
return typemap[key].unbind(value);
|
|
54
|
+
}
|
|
55
|
+
function like_to_regex(pattern) {
|
|
56
|
+
return "^" + pattern
|
|
57
|
+
.replace(/\\%/g, "<<PERCENT>>")
|
|
58
|
+
.replace(/\\_/g, "<<UNDERSCORE>>")
|
|
59
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
60
|
+
.replace(/%/g, ".*")
|
|
61
|
+
.replace(/_/g, ".")
|
|
62
|
+
.replace(/<<PERCENT>>/g, "%")
|
|
63
|
+
.replace(/<<UNDERSCORE>>/g, "_") + "$";
|
|
64
|
+
}
|
|
65
|
+
export default class MongoDB {
|
|
66
|
+
static config;
|
|
67
|
+
#factory;
|
|
68
|
+
#database;
|
|
69
|
+
#client;
|
|
70
|
+
constructor(config) {
|
|
71
|
+
const { host, port, database } = schema.parse(config);
|
|
72
|
+
const params = "directConnection=true&replicaSet=rs0";
|
|
73
|
+
const client = new MongoClient(`mongodb://${host}:${port}?${params}`);
|
|
74
|
+
this.#database = database;
|
|
75
|
+
this.#factory = async () => {
|
|
76
|
+
await client.connect();
|
|
77
|
+
return client;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async #collection(name) {
|
|
81
|
+
this.#client ??= await this.#factory();
|
|
82
|
+
return this.#client.db(this.#database).collection(name);
|
|
83
|
+
}
|
|
84
|
+
async close() {
|
|
85
|
+
await this.#client?.close();
|
|
86
|
+
}
|
|
87
|
+
get schema() {
|
|
88
|
+
return {
|
|
89
|
+
create: async (_as, _store) => {
|
|
90
|
+
// MongoDB is schemaless, noop
|
|
91
|
+
},
|
|
92
|
+
delete: async (name) => {
|
|
93
|
+
const collection = await this.#collection(name);
|
|
94
|
+
await collection.drop().catch(() => { }); // ignore if doesn't exist
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
#to_mongo_pk(field, pk) {
|
|
99
|
+
return field === pk ? "_id" : field;
|
|
100
|
+
}
|
|
101
|
+
#from_mongo_pk(field, pk) {
|
|
102
|
+
return field === "_id" && pk !== null ? pk : field;
|
|
103
|
+
}
|
|
104
|
+
async #bind(as, object) {
|
|
105
|
+
const pk = as.pk;
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [field, value] of Object.entries(object)) {
|
|
108
|
+
const mongo_field = this.#to_mongo_pk(field, pk);
|
|
109
|
+
const datatype = as.types[field];
|
|
110
|
+
if (value === null) {
|
|
111
|
+
out[mongo_field] = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (is.dict(value)) {
|
|
115
|
+
const ops = Object.entries(value);
|
|
116
|
+
if (ops.length === 0)
|
|
117
|
+
throw E.operator_empty(field);
|
|
118
|
+
for (const [op, op_value] of ops) {
|
|
119
|
+
const existing = (out[mongo_field] ?? {});
|
|
120
|
+
let next;
|
|
121
|
+
switch (op) {
|
|
122
|
+
case "$like":
|
|
123
|
+
next = { $regex: like_to_regex(String(op_value)) };
|
|
124
|
+
break;
|
|
125
|
+
case "$ilike":
|
|
126
|
+
next = { $regex: like_to_regex(String(op_value)), $options: "i" };
|
|
127
|
+
break;
|
|
128
|
+
case "$ne":
|
|
129
|
+
case "$gt":
|
|
130
|
+
case "$gte":
|
|
131
|
+
case "$lt":
|
|
132
|
+
case "$lte":
|
|
133
|
+
next = { [op]: await bind_value(datatype, op_value) };
|
|
134
|
+
break;
|
|
135
|
+
case "$after":
|
|
136
|
+
next = { $gt: await bind_value(datatype, op_value) };
|
|
137
|
+
break;
|
|
138
|
+
case "$before":
|
|
139
|
+
next = { $lt: await bind_value(datatype, op_value) };
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
throw E.operator_unknown(field, op);
|
|
143
|
+
}
|
|
144
|
+
out[mongo_field] = { ...existing, ...next };
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (field === pk) {
|
|
149
|
+
const type = as.types[pk];
|
|
150
|
+
if (type === "string" && ObjectId.isValid(value)) {
|
|
151
|
+
out._id = new ObjectId(value);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
out._id = value;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
out[mongo_field] = await bind_value(datatype, value);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
#unbind(as, doc) {
|
|
163
|
+
const pk = as.pk;
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const [field, value] of Object.entries(doc)) {
|
|
166
|
+
const user_field = this.#from_mongo_pk(field, pk);
|
|
167
|
+
const datatype = as.types[user_field];
|
|
168
|
+
if (value === null || value === undefined) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (field === "_id") {
|
|
172
|
+
// handle ObjectId to string for PK, other keep as-is
|
|
173
|
+
out[user_field] = is_object_id(value) ? value.toString() : value;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
out[user_field] = unbind_value(datatype, value);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
async #generate_pk(as) {
|
|
181
|
+
const pk = as.pk;
|
|
182
|
+
const type = as.types[pk];
|
|
183
|
+
const collection = await this.#collection(as.table);
|
|
184
|
+
if (type === "string")
|
|
185
|
+
return new ObjectId();
|
|
186
|
+
// for numeric types, find max and increment
|
|
187
|
+
const pipeline = [{ $group: { _id: null, max: { $max: "$_id" } } }];
|
|
188
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
189
|
+
const max = results.length === 0 ? 0 : results[0].max ?? 0;
|
|
190
|
+
if (common.BIGINT_STRING_TYPES.includes(type))
|
|
191
|
+
return BigInt(max) + 1n;
|
|
192
|
+
return Number(max) + 1;
|
|
193
|
+
}
|
|
194
|
+
async create(as, record) {
|
|
195
|
+
assert.dict(record);
|
|
196
|
+
const pk = as.pk;
|
|
197
|
+
const collection = await this.#collection(as.table);
|
|
198
|
+
const doc = {};
|
|
199
|
+
let pk_value = null;
|
|
200
|
+
if (pk !== null) {
|
|
201
|
+
const type = as.types[pk];
|
|
202
|
+
if (pk in record) {
|
|
203
|
+
pk_value = record[pk];
|
|
204
|
+
doc._id = type === "string" && ObjectId.isValid(pk_value)
|
|
205
|
+
? new ObjectId(pk_value)
|
|
206
|
+
: pk_value;
|
|
207
|
+
}
|
|
208
|
+
else if (as.generate_pk !== false) {
|
|
209
|
+
const generated = await this.#generate_pk(as);
|
|
210
|
+
doc._id = generated;
|
|
211
|
+
// convert to user-facing value
|
|
212
|
+
pk_value = is_object_id(generated) ? generated.toString() : generated;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
throw E.pk_required(pk);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const [field, value] of Object.entries(record)) {
|
|
219
|
+
if (field === pk)
|
|
220
|
+
continue;
|
|
221
|
+
doc[field] = await bind_value(as.types[field], value);
|
|
222
|
+
}
|
|
223
|
+
await collection.insertOne(doc);
|
|
224
|
+
const result = { ...record };
|
|
225
|
+
if (pk !== null && !(pk in record) && pk_value !== null) {
|
|
226
|
+
result[pk] = pk_value;
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
async read(as, args) {
|
|
231
|
+
assert.dict(args.where);
|
|
232
|
+
if (args.count === true)
|
|
233
|
+
return this.#count(as, args.where);
|
|
234
|
+
// relations always use phased approach
|
|
235
|
+
if (common.withed(args))
|
|
236
|
+
return this.#read_phased(as, args);
|
|
237
|
+
return this.#read(as, args);
|
|
238
|
+
}
|
|
239
|
+
async #count(as, where) {
|
|
240
|
+
const filter = await this.#bind(as, where);
|
|
241
|
+
const collection = await this.#collection(as.table);
|
|
242
|
+
const count = await collection.countDocuments(filter);
|
|
243
|
+
return count;
|
|
244
|
+
}
|
|
245
|
+
async #read(as, args) {
|
|
246
|
+
const filter = await this.#bind(as, args.where);
|
|
247
|
+
const collection = await this.#collection(as.table);
|
|
248
|
+
const projection = get_projection(as.pk, args.fields);
|
|
249
|
+
const sort = get_sort(args.sort);
|
|
250
|
+
const limit = get_limit(args.limit);
|
|
251
|
+
const options = { useBigInt64: true };
|
|
252
|
+
if (projection)
|
|
253
|
+
options.projection = projection;
|
|
254
|
+
if (sort)
|
|
255
|
+
options.sort = sort;
|
|
256
|
+
const docs = await collection
|
|
257
|
+
.find(filter, options)
|
|
258
|
+
.limit(limit)
|
|
259
|
+
.toArray();
|
|
260
|
+
return docs.map(doc => this.#unbind(as, doc));
|
|
261
|
+
}
|
|
262
|
+
async #read_phased(as, args) {
|
|
263
|
+
const fields = common.expand(as, args.fields, args.with);
|
|
264
|
+
const rows = await this.#read(as, { ...args, fields });
|
|
265
|
+
const out = rows.map(row => common.project(row, args.fields));
|
|
266
|
+
for (const [name, relation] of Object.entries(args.with)) {
|
|
267
|
+
await this.#attach_relation(as, { rows, out, name, relation });
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
async #attach_relation(as, args) {
|
|
272
|
+
const relation = args.relation;
|
|
273
|
+
const by = relation.reverse ? relation.as.pk : relation.fk;
|
|
274
|
+
if (by === null)
|
|
275
|
+
throw E.relation_requires_pk("target");
|
|
276
|
+
const parent_by = relation.reverse ? relation.fk : as.pk;
|
|
277
|
+
if (parent_by === null)
|
|
278
|
+
throw E.relation_requires_pk("parent");
|
|
279
|
+
const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
|
|
280
|
+
const is_many = relation.kind === "many";
|
|
281
|
+
const empty = is_many ? [] : null;
|
|
282
|
+
if (join_values.length === 0) {
|
|
283
|
+
for (const row of args.out)
|
|
284
|
+
row[args.name] = empty;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const related = await this.#load_related({ by, join_values, ...relation });
|
|
288
|
+
const grouped = new Map();
|
|
289
|
+
for (const row of related) {
|
|
290
|
+
const key = row[by];
|
|
291
|
+
grouped.set(key, grouped.get(key)?.concat(row) ?? [row]);
|
|
292
|
+
}
|
|
293
|
+
for (let i = 0; i < args.out.length; i++) {
|
|
294
|
+
const join_value = args.rows[i][parent_by];
|
|
295
|
+
if (join_value == null) {
|
|
296
|
+
args.out[i][args.name] = empty;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const rows = grouped.get(join_value) ?? [];
|
|
300
|
+
args.out[i][args.name] = is_many
|
|
301
|
+
? rows.map(r => common.project(r, relation.fields))
|
|
302
|
+
: rows[0] ? common.project(rows[0], relation.fields) : null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async #load_related(args) {
|
|
306
|
+
// build filter with $in for join values
|
|
307
|
+
const filter = await this.#bind(args.as, args.where);
|
|
308
|
+
const by_field = this.#to_mongo_pk(args.by, args.as.pk);
|
|
309
|
+
// convert join values to ObjectId if needed
|
|
310
|
+
const pk_type = args.as.types[args.as.pk];
|
|
311
|
+
const in_values = args.by === args.as.pk && pk_type === "string"
|
|
312
|
+
? args.join_values.map(v => ObjectId.isValid(v) ? new ObjectId(v) : v)
|
|
313
|
+
: args.join_values;
|
|
314
|
+
filter[by_field] = { $in: in_values };
|
|
315
|
+
const collection = await this.#collection(args.as.table);
|
|
316
|
+
const fields_with_by = common.fields(args.fields, args.by);
|
|
317
|
+
const projection = get_projection(args.as.pk, fields_with_by);
|
|
318
|
+
const sort = get_sort(args.sort);
|
|
319
|
+
const options = { useBigInt64: true };
|
|
320
|
+
if (projection)
|
|
321
|
+
options.projection = projection;
|
|
322
|
+
if (sort)
|
|
323
|
+
options.sort = sort;
|
|
324
|
+
// for per-parent limits, fetch all and slice in memory
|
|
325
|
+
// MongoDB doesn't have native per-group limit like SQL's ROW_NUMBER
|
|
326
|
+
const docs = await collection.find(filter, options).toArray();
|
|
327
|
+
const rows = docs.map(doc => this.#unbind(args.as, doc));
|
|
328
|
+
// apply per-parent limit if needed
|
|
329
|
+
const per_parent = args.kind === "one" ? 1 : args.limit;
|
|
330
|
+
if (per_parent !== undefined) {
|
|
331
|
+
const grouped = new Map();
|
|
332
|
+
for (const row of rows) {
|
|
333
|
+
const key = row[args.by];
|
|
334
|
+
const group = grouped.get(key) ?? [];
|
|
335
|
+
if (group.length < per_parent) {
|
|
336
|
+
group.push(row);
|
|
337
|
+
grouped.set(key, group);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return [...grouped.values()].flat();
|
|
341
|
+
}
|
|
342
|
+
return rows;
|
|
343
|
+
}
|
|
344
|
+
async update(as, args) {
|
|
345
|
+
assert.nonempty(args.set);
|
|
346
|
+
assert.dict(args.where);
|
|
347
|
+
const filter = await this.#bind(as, args.where);
|
|
348
|
+
const collection = await this.#collection(as.table);
|
|
349
|
+
const $set = {};
|
|
350
|
+
const $unset = {};
|
|
351
|
+
for (const [field, value] of Object.entries(args.set)) {
|
|
352
|
+
const mongo_field = this.#to_mongo_pk(field, as.pk);
|
|
353
|
+
if (value === null) {
|
|
354
|
+
$unset[mongo_field] = "";
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
$set[mongo_field] = await bind_value(as.types[field], value);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const update = {};
|
|
361
|
+
if (Object.keys($set).length > 0)
|
|
362
|
+
update.$set = $set;
|
|
363
|
+
if (Object.keys($unset).length > 0)
|
|
364
|
+
update.$unset = $unset;
|
|
365
|
+
const result = await collection.updateMany(filter, update);
|
|
366
|
+
return result.modifiedCount;
|
|
367
|
+
}
|
|
368
|
+
async delete(as, args) {
|
|
369
|
+
assert.nonempty(args.where);
|
|
370
|
+
const filter = await this.#bind(as, args.where);
|
|
371
|
+
const collection = await this.#collection(as.table);
|
|
372
|
+
const result = await collection.deleteMany(filter);
|
|
373
|
+
return result.deletedCount;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
//# sourceMappingURL=MongoDB.js.map
|
package/lib/private/typemap.d.ts
CHANGED
package/lib/private/typemap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Binary, Decimal128
|
|
1
|
+
import { Binary, Decimal128 } from "mongodb";
|
|
2
2
|
function identity(column) {
|
|
3
3
|
return {
|
|
4
4
|
bind: value => value,
|
|
@@ -45,18 +45,6 @@ const typemap = {
|
|
|
45
45
|
i32: identity("INT"),
|
|
46
46
|
i64: identity("LONG"),
|
|
47
47
|
i8: identity("INT"),
|
|
48
|
-
primary: {
|
|
49
|
-
bind(value) {
|
|
50
|
-
if (typeof value === "string") {
|
|
51
|
-
return new ObjectId(value);
|
|
52
|
-
}
|
|
53
|
-
throw new Error(`\`${value}\` is not a valid primary key value`);
|
|
54
|
-
},
|
|
55
|
-
column: "PRIMARY",
|
|
56
|
-
unbind(value) {
|
|
57
|
-
return String(value);
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
48
|
string: identity("STRING"),
|
|
61
49
|
time: identity("TIME"),
|
|
62
50
|
u128: {
|
package/lib/public/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
declare const _default: (config: typeof
|
|
1
|
+
import MongoDB from "#MongoDB";
|
|
2
|
+
declare const _default: (config: typeof MongoDB.config) => MongoDB;
|
|
3
3
|
export default _default;
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/public/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
export default (config) => new
|
|
1
|
+
import MongoDB from "#MongoDB";
|
|
2
|
+
export default (config) => new MongoDB(config);
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primate/mongodb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "MongoDB databases for Primate",
|
|
5
5
|
"homepage": "https://primate.run/docs/database/mongodb",
|
|
6
6
|
"bugs": "https://github.com/primate-run/primate/issues",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"license": "MIT",
|
|
8
|
-
"files": [
|
|
9
|
-
"/lib/**/*.js",
|
|
10
|
-
"/lib/**/*.d.ts",
|
|
11
|
-
"!/**/*.spec.*"
|
|
12
|
-
],
|
|
13
9
|
"repository": {
|
|
14
10
|
"type": "git",
|
|
15
11
|
"url": "https://github.com/primate-run/primate",
|
|
16
12
|
"directory": "packages/mongodb"
|
|
17
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"/lib/**/*.js",
|
|
16
|
+
"/lib/**/*.d.ts",
|
|
17
|
+
"!/**/*.spec.*"
|
|
18
|
+
],
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@rcompat/assert": "^0.
|
|
20
|
-
"@rcompat/
|
|
20
|
+
"@rcompat/assert": "^0.6.0",
|
|
21
|
+
"@rcompat/dict": "^0.3.1",
|
|
22
|
+
"@rcompat/is": "^0.4.3",
|
|
21
23
|
"mongodb": "^6.17.0",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
+
"@primate/core": "^0.5.0",
|
|
25
|
+
"pema": "0.5.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@rcompat/type": "^0.9.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"primate": "^0.36.0"
|
|
24
32
|
},
|
|
25
|
-
"type": "module",
|
|
26
33
|
"imports": {
|
|
27
34
|
"#*": {
|
|
28
35
|
"apekit": "./src/private/*.ts",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import Database from "@primate/core/Database";
|
|
2
|
-
import type As from "@primate/core/database/As";
|
|
3
|
-
import type DataDict from "@primate/core/database/DataDict";
|
|
4
|
-
import type TypeMap from "@primate/core/database/TypeMap";
|
|
5
|
-
import type Dict from "@rcompat/type/Dict";
|
|
6
|
-
declare const schema: import("pema").ObjectType<{
|
|
7
|
-
database: import("pema/string").StringType;
|
|
8
|
-
host: import("pema").DefaultType<import("pema/string").StringType, "localhost">;
|
|
9
|
-
password: import("pema").OptionalType<import("pema/string").StringType>;
|
|
10
|
-
port: import("pema").DefaultType<import("pema/uint").UintType<"u32">, 27017>;
|
|
11
|
-
username: import("pema").OptionalType<import("pema/string").StringType>;
|
|
12
|
-
}>;
|
|
13
|
-
export default class MongoDBDatabase extends Database {
|
|
14
|
-
#private;
|
|
15
|
-
static config: typeof schema.input;
|
|
16
|
-
constructor(config?: typeof schema.input);
|
|
17
|
-
get typemap(): TypeMap<Dict>;
|
|
18
|
-
close(): Promise<void>;
|
|
19
|
-
get schema(): {
|
|
20
|
-
create: () => undefined;
|
|
21
|
-
delete: (name: string) => Promise<void>;
|
|
22
|
-
};
|
|
23
|
-
create<O extends Dict>(as: As, args: {
|
|
24
|
-
record: DataDict;
|
|
25
|
-
}): Promise<O>;
|
|
26
|
-
read(as: As, args: {
|
|
27
|
-
count: true;
|
|
28
|
-
criteria: DataDict;
|
|
29
|
-
}): Promise<number>;
|
|
30
|
-
read(as: As, args: {
|
|
31
|
-
criteria: DataDict;
|
|
32
|
-
fields?: string[];
|
|
33
|
-
limit?: number;
|
|
34
|
-
sort?: Dict<"asc" | "desc">;
|
|
35
|
-
}): Promise<Dict[]>;
|
|
36
|
-
update(as: As, args: {
|
|
37
|
-
changes: DataDict;
|
|
38
|
-
criteria: DataDict;
|
|
39
|
-
}): Promise<number>;
|
|
40
|
-
delete(as: As, args: {
|
|
41
|
-
criteria: DataDict;
|
|
42
|
-
}): Promise<number>;
|
|
43
|
-
}
|
|
44
|
-
export {};
|
|
45
|
-
//# sourceMappingURL=Database.d.ts.map
|
package/lib/private/Database.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import typemap from "#typemap";
|
|
2
|
-
import Database from "@primate/core/Database";
|
|
3
|
-
import assert from "@rcompat/assert";
|
|
4
|
-
import maybe from "@rcompat/assert/maybe";
|
|
5
|
-
import empty from "@rcompat/record/empty";
|
|
6
|
-
import entries from "@rcompat/record/entries";
|
|
7
|
-
import toQueryString from "@rcompat/record/toQueryString";
|
|
8
|
-
import { MongoClient } from "mongodb";
|
|
9
|
-
import pema from "pema";
|
|
10
|
-
import string from "pema/string";
|
|
11
|
-
import uint from "pema/uint";
|
|
12
|
-
const schema = pema({
|
|
13
|
-
database: string,
|
|
14
|
-
host: string.default("localhost"),
|
|
15
|
-
password: string.optional(),
|
|
16
|
-
port: uint.port().default(27017),
|
|
17
|
-
username: string.optional(),
|
|
18
|
-
});
|
|
19
|
-
function make_limit(limit) {
|
|
20
|
-
maybe(limit).usize();
|
|
21
|
-
if (limit === undefined) {
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
return limit;
|
|
25
|
-
}
|
|
26
|
-
;
|
|
27
|
-
const null_to_set_unset = (changes) => {
|
|
28
|
-
const entry_changes = entries(changes);
|
|
29
|
-
const $set = entry_changes.filter(([, value]) => value !== null).get();
|
|
30
|
-
const $unset = entry_changes.filter(([, value]) => value === null).get();
|
|
31
|
-
return { $set, $unset };
|
|
32
|
-
};
|
|
33
|
-
const url_params = { directConnection: "true", replicaSet: "rs0" };
|
|
34
|
-
export default class MongoDBDatabase extends Database {
|
|
35
|
-
static config;
|
|
36
|
-
#factory;
|
|
37
|
-
#name;
|
|
38
|
-
#client;
|
|
39
|
-
constructor(config) {
|
|
40
|
-
super();
|
|
41
|
-
const { database, host, port } = schema.parse(config);
|
|
42
|
-
const url = `mongodb://${host}:${port}?${toQueryString(url_params)}`;
|
|
43
|
-
const client = new MongoClient(url);
|
|
44
|
-
this.#name = database;
|
|
45
|
-
this.#factory = async () => {
|
|
46
|
-
await client.connect();
|
|
47
|
-
return client;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async #get(collection) {
|
|
51
|
-
if (this.#client === undefined) {
|
|
52
|
-
this.#client = await this.#factory();
|
|
53
|
-
}
|
|
54
|
-
return this.#client.db(this.#name).collection(collection);
|
|
55
|
-
}
|
|
56
|
-
get typemap() {
|
|
57
|
-
return typemap;
|
|
58
|
-
}
|
|
59
|
-
async close() {
|
|
60
|
-
await this.#client.close();
|
|
61
|
-
}
|
|
62
|
-
async #drop(name) {
|
|
63
|
-
await (await this.#get(name)).drop();
|
|
64
|
-
}
|
|
65
|
-
get schema() {
|
|
66
|
-
return {
|
|
67
|
-
// noop
|
|
68
|
-
create: () => undefined,
|
|
69
|
-
delete: this.#drop.bind(this),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
async #bind(object, types) {
|
|
73
|
-
const prepared = Object.fromEntries(Object.entries(object)
|
|
74
|
-
.map(([key, value]) => {
|
|
75
|
-
if (value === null)
|
|
76
|
-
return [key, null];
|
|
77
|
-
if (typeof value === "object" && "$like" in value) {
|
|
78
|
-
const pattern = String(value.$like);
|
|
79
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
80
|
-
const $regex = `^${escaped.replace(/%/g, ".*").replace(/_/g, ".")}$`;
|
|
81
|
-
return [key, { $regex }];
|
|
82
|
-
}
|
|
83
|
-
return [key, value];
|
|
84
|
-
}));
|
|
85
|
-
const { id, ...rest } = await this.bind(types, prepared);
|
|
86
|
-
return id === undefined
|
|
87
|
-
? rest
|
|
88
|
-
: { _id: id, ...rest };
|
|
89
|
-
}
|
|
90
|
-
#unbind(object, types) {
|
|
91
|
-
const { _id: id, ...rest } = object;
|
|
92
|
-
return this.unbind(types, id === undefined ? rest : { id, ...rest });
|
|
93
|
-
}
|
|
94
|
-
async create(as, args) {
|
|
95
|
-
const binds = await this.#bind(args.record, as.types);
|
|
96
|
-
const { insertedId } = await (await this.#get(as.name)).insertOne(binds);
|
|
97
|
-
return this.#unbind({ ...args.record, _id: insertedId }, as.types);
|
|
98
|
-
}
|
|
99
|
-
async read(as, args) {
|
|
100
|
-
this.toSelect(as.types, args.fields);
|
|
101
|
-
this.toSort(as.types, args.sort);
|
|
102
|
-
const binds = await this.#bind(args.criteria, as.types);
|
|
103
|
-
if (args.count === true) {
|
|
104
|
-
return (await this.#get(as.name)).countDocuments(binds);
|
|
105
|
-
}
|
|
106
|
-
const fields = args.fields ?? [];
|
|
107
|
-
const mapped = fields.map(f => f === "id" ? "_id" : f);
|
|
108
|
-
const sort = args.sort === undefined || empty(args.sort)
|
|
109
|
-
? {}
|
|
110
|
-
: { sort: args.sort };
|
|
111
|
-
const select = mapped.length === 0
|
|
112
|
-
? {}
|
|
113
|
-
: {
|
|
114
|
-
projection: (() => {
|
|
115
|
-
const out = {};
|
|
116
|
-
if (!mapped.includes("_id")) {
|
|
117
|
-
out._id = 0;
|
|
118
|
-
}
|
|
119
|
-
for (const field of mapped) {
|
|
120
|
-
out[field] = 1;
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
})(),
|
|
124
|
-
};
|
|
125
|
-
const options = { ...select, ...sort, useBigInt64: true };
|
|
126
|
-
const records = await (await this.#get(as.name))
|
|
127
|
-
.find(binds, options)
|
|
128
|
-
.limit(make_limit(args.limit))
|
|
129
|
-
.toArray();
|
|
130
|
-
return records.map(record => this.#unbind(record, as.types));
|
|
131
|
-
}
|
|
132
|
-
async update(as, args) {
|
|
133
|
-
assert(Object.keys(args.criteria).length > 0, "update: no criteria");
|
|
134
|
-
const criteria_binds = await this.#bind(args.criteria, as.types);
|
|
135
|
-
const changes_binds = await this.#bind(args.changes, as.types);
|
|
136
|
-
const collection = await this.#get(as.name);
|
|
137
|
-
return (await collection
|
|
138
|
-
.updateMany(criteria_binds, null_to_set_unset(changes_binds)))
|
|
139
|
-
.modifiedCount;
|
|
140
|
-
}
|
|
141
|
-
async delete(as, args) {
|
|
142
|
-
assert(Object.keys(args.criteria).length > 0, "delete: no criteria");
|
|
143
|
-
const binds = await this.#bind(args.criteria, as.types);
|
|
144
|
-
return (await ((await this.#get(as.name)).deleteMany(binds))).deletedCount;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
//# sourceMappingURL=Database.js.map
|