@primate/mongodb 0.4.0 → 0.6.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 +5 -5
- package/lib/private/MongoDB.d.ts +44 -0
- package/lib/private/MongoDB.js +443 -0
- package/lib/private/typemap.d.ts +1 -1
- package/lib/private/typemap.js +17 -13
- package/lib/public/index.d.ts +2 -2
- package/lib/public/index.js +2 -2
- package/package.json +22 -15
- package/lib/private/Database.d.ts +0 -45
- package/lib/private/Database.js +0 -147
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type TypedArray from "@rcompat/type
|
|
2
|
-
import type { Binary, Decimal128,
|
|
3
|
-
type Param = bigint | Binary | boolean | Date | Decimal128 | number |
|
|
1
|
+
import type { JSONValue, TypedArray } from "@rcompat/type";
|
|
2
|
+
import type { Binary, Decimal128, UUID } from "mongodb";
|
|
3
|
+
type Param = bigint | Binary | boolean | Date | Decimal128 | number | string | TypedArray | JSONValue;
|
|
4
4
|
type Validate<T extends {
|
|
5
5
|
[K in keyof T]: Param;
|
|
6
6
|
}> = T;
|
|
@@ -12,10 +12,10 @@ type ColumnTypes = Validate<{
|
|
|
12
12
|
DOUBLE: number;
|
|
13
13
|
INT: number;
|
|
14
14
|
LONG: bigint;
|
|
15
|
-
PRIMARY: ObjectId;
|
|
16
15
|
STRING: string;
|
|
17
16
|
TIME: string;
|
|
18
|
-
UUID:
|
|
17
|
+
UUID: UUID;
|
|
18
|
+
JSON: JSONValue;
|
|
19
19
|
}>;
|
|
20
20
|
export type { ColumnTypes as default };
|
|
21
21
|
//# sourceMappingURL=ColumnTypes.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { As, DataDict, DB, Schema, Sort, With } from "@primate/core/db";
|
|
2
|
+
import type { Dict } from "@rcompat/type";
|
|
3
|
+
declare const schema: import("pema").ObjectType<import("pema").NormalizeSchemaObject<{
|
|
4
|
+
readonly host: import("pema").DefaultType<import("pema").StringType, "localhost">;
|
|
5
|
+
readonly port: import("pema").DefaultType<import("pema").UintType<"u32">, 27017>;
|
|
6
|
+
readonly database: import("pema").StringType;
|
|
7
|
+
readonly username: import("pema").OptionalType<import("pema").StringType>;
|
|
8
|
+
readonly password: import("pema").OptionalType<import("pema").StringType>;
|
|
9
|
+
}>, {
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
database: string;
|
|
13
|
+
username: string | undefined;
|
|
14
|
+
password: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export default class MongoDB implements DB {
|
|
17
|
+
#private;
|
|
18
|
+
static config: typeof schema.input;
|
|
19
|
+
constructor(config?: typeof schema.input);
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
get schema(): Schema;
|
|
22
|
+
create<O extends Dict>(as: As, record: Dict): Promise<O>;
|
|
23
|
+
read(as: As, args: {
|
|
24
|
+
count: true;
|
|
25
|
+
where: DataDict;
|
|
26
|
+
with?: never;
|
|
27
|
+
}): Promise<number>;
|
|
28
|
+
read(as: As, args: {
|
|
29
|
+
where: DataDict;
|
|
30
|
+
fields?: string[];
|
|
31
|
+
limit?: number;
|
|
32
|
+
sort?: Sort;
|
|
33
|
+
with?: With;
|
|
34
|
+
}): Promise<Dict[]>;
|
|
35
|
+
update(as: As, args: {
|
|
36
|
+
set: DataDict;
|
|
37
|
+
where: DataDict;
|
|
38
|
+
}): Promise<number>;
|
|
39
|
+
delete(as: As, args: {
|
|
40
|
+
where: DataDict;
|
|
41
|
+
}): Promise<number>;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=MongoDB.d.ts.map
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import typemap from "#typemap";
|
|
2
|
+
import base from "@primate/core/db";
|
|
3
|
+
import E from "@primate/core/db/errors";
|
|
4
|
+
import assert from "@rcompat/assert";
|
|
5
|
+
import is from "@rcompat/is";
|
|
6
|
+
import { MongoClient } from "mongodb";
|
|
7
|
+
import p from "pema";
|
|
8
|
+
const schema = p({
|
|
9
|
+
host: p.string.default("localhost"),
|
|
10
|
+
port: p.uint.port().default(27017),
|
|
11
|
+
database: p.string,
|
|
12
|
+
username: p.string.optional(),
|
|
13
|
+
password: p.string.optional(),
|
|
14
|
+
});
|
|
15
|
+
function get_limit(limit) {
|
|
16
|
+
return limit ?? 0; // 0 = no limit
|
|
17
|
+
}
|
|
18
|
+
function get_sort(sort) {
|
|
19
|
+
if (sort === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
const entries = Object.entries(sort);
|
|
22
|
+
if (entries.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [k, dir] of entries) {
|
|
26
|
+
out[k] = dir.toLowerCase() === "desc" ? -1 : 1;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function get_projection(pk, fields) {
|
|
31
|
+
if (fields === undefined || fields.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
const out = {};
|
|
34
|
+
const has_pk = pk !== null && fields.includes(pk);
|
|
35
|
+
// MongoDB always includes _id unless explicitly excluded
|
|
36
|
+
if (!has_pk)
|
|
37
|
+
out._id = 0;
|
|
38
|
+
for (const field of fields) {
|
|
39
|
+
const key = field === pk ? "_id" : field;
|
|
40
|
+
out[key] = 1;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
async function bind_value(key, value) {
|
|
45
|
+
if (value === null)
|
|
46
|
+
return null;
|
|
47
|
+
return await typemap[key].bind(value);
|
|
48
|
+
}
|
|
49
|
+
function unbind_value(key, value) {
|
|
50
|
+
return typemap[key].unbind(value);
|
|
51
|
+
}
|
|
52
|
+
function to_mongo_pk(field, pk) {
|
|
53
|
+
return field === pk ? "_id" : field;
|
|
54
|
+
}
|
|
55
|
+
async function bind(as, object) {
|
|
56
|
+
const pk = as.pk;
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [field, value] of Object.entries(object)) {
|
|
59
|
+
const mongo_field = to_mongo_pk(field, pk);
|
|
60
|
+
const datatype = as.types[field];
|
|
61
|
+
if (value === null) {
|
|
62
|
+
out[mongo_field] = null;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (is.dict(value) && datatype !== "json") {
|
|
66
|
+
const ops = Object.entries(value);
|
|
67
|
+
if (ops.length === 0)
|
|
68
|
+
throw E.operator_empty(field);
|
|
69
|
+
for (const [op, op_value] of ops) {
|
|
70
|
+
const existing = (out[mongo_field] ?? {});
|
|
71
|
+
let next;
|
|
72
|
+
switch (op) {
|
|
73
|
+
case "$like":
|
|
74
|
+
next = { $regex: like_to_regex(String(op_value)) };
|
|
75
|
+
break;
|
|
76
|
+
case "$ilike":
|
|
77
|
+
next = { $regex: like_to_regex(String(op_value)), $options: "i" };
|
|
78
|
+
break;
|
|
79
|
+
case "$ne":
|
|
80
|
+
case "$gt":
|
|
81
|
+
case "$gte":
|
|
82
|
+
case "$lt":
|
|
83
|
+
case "$lte":
|
|
84
|
+
next = { [op]: await bind_value(datatype, op_value) };
|
|
85
|
+
break;
|
|
86
|
+
case "$after":
|
|
87
|
+
next = { $gt: await bind_value(datatype, op_value) };
|
|
88
|
+
break;
|
|
89
|
+
case "$before":
|
|
90
|
+
next = { $lt: await bind_value(datatype, op_value) };
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
throw E.operator_unknown(field, op);
|
|
94
|
+
}
|
|
95
|
+
out[mongo_field] = { ...existing, ...next };
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (field === pk) {
|
|
100
|
+
const type = as.types[pk];
|
|
101
|
+
if (base.is_uuid_type(type)) {
|
|
102
|
+
out._id = await bind_value(type, value);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
out._id = value;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
out[mongo_field] = await bind_value(datatype, value);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function like_to_regex(pattern) {
|
|
114
|
+
return "^" + pattern
|
|
115
|
+
.replace(/\\%/g, "<<PERCENT>>")
|
|
116
|
+
.replace(/\\_/g, "<<UNDERSCORE>>")
|
|
117
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
118
|
+
.replace(/%/g, ".*")
|
|
119
|
+
.replace(/_/g, ".")
|
|
120
|
+
.replace(/<<PERCENT>>/g, "%")
|
|
121
|
+
.replace(/<<UNDERSCORE>>/g, "_") + "$";
|
|
122
|
+
}
|
|
123
|
+
function columns_to_types(row) {
|
|
124
|
+
switch (row.type) {
|
|
125
|
+
case "string": return ["string", "url", "time", "i128", "u128"];
|
|
126
|
+
case "int": return ["i8", "u8", "i16", "u16", "i32", "u32"];
|
|
127
|
+
case "long": return ["i64"];
|
|
128
|
+
case "double": return ["f32", "f64"];
|
|
129
|
+
case "bool": return ["boolean"];
|
|
130
|
+
case "date": return ["datetime"];
|
|
131
|
+
case "binData": return row.name === "_id" ? base.UUID_TYPES : ["blob"];
|
|
132
|
+
case "decimal": return ["u64"];
|
|
133
|
+
case "objectId": return ["string"];
|
|
134
|
+
case "object": return ["json"];
|
|
135
|
+
case "array": return ["json"];
|
|
136
|
+
default: return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export default class MongoDB {
|
|
140
|
+
static config;
|
|
141
|
+
#factory;
|
|
142
|
+
#database;
|
|
143
|
+
#client;
|
|
144
|
+
constructor(config) {
|
|
145
|
+
const { host, port, database } = schema.parse(config);
|
|
146
|
+
const params = "directConnection=true&replicaSet=rs0";
|
|
147
|
+
const client = new MongoClient(`mongodb://${host}:${port}?${params}`);
|
|
148
|
+
this.#database = database;
|
|
149
|
+
this.#factory = async () => {
|
|
150
|
+
await client.connect();
|
|
151
|
+
return client;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async #collection(name) {
|
|
155
|
+
this.#client ??= await this.#factory();
|
|
156
|
+
return this.#client.db(this.#database).collection(name);
|
|
157
|
+
}
|
|
158
|
+
async close() {
|
|
159
|
+
await this.#client?.close();
|
|
160
|
+
}
|
|
161
|
+
get schema() {
|
|
162
|
+
return {
|
|
163
|
+
create: async (collection) => {
|
|
164
|
+
const client = this.#client ??= await this.#factory();
|
|
165
|
+
const db = client.db(this.#database);
|
|
166
|
+
// ignore if already exists
|
|
167
|
+
await db.createCollection(collection).catch(() => { });
|
|
168
|
+
},
|
|
169
|
+
delete: async (collection) => {
|
|
170
|
+
const client = this.#client ??= await this.#factory();
|
|
171
|
+
const db = client.db(this.#database);
|
|
172
|
+
// ignore if doesn't exist
|
|
173
|
+
await db.dropCollection(collection).catch(() => { });
|
|
174
|
+
},
|
|
175
|
+
introspect: async (name, pk) => {
|
|
176
|
+
const client = (this.#client ??= await this.#factory());
|
|
177
|
+
const db = client.db(this.#database);
|
|
178
|
+
const collections = await db.listCollections({ name }).toArray();
|
|
179
|
+
if (collections.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
const collection = await this.#collection(name);
|
|
182
|
+
const rows = await collection.aggregate([
|
|
183
|
+
{ $limit: 1 },
|
|
184
|
+
{ $project: { fields: { $objectToArray: "$$ROOT" } } },
|
|
185
|
+
{ $unwind: "$fields" },
|
|
186
|
+
{ $project: { _id: 0, name: "$fields.k", type: { $type: "$fields.v" } } },
|
|
187
|
+
]).toArray();
|
|
188
|
+
if (rows.length === 0)
|
|
189
|
+
return {};
|
|
190
|
+
const result = {};
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
if (pk !== undefined && row.name === "_id") {
|
|
193
|
+
if (pk !== null)
|
|
194
|
+
result[pk] = columns_to_types(row);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
result[row.name] = columns_to_types(row);
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
},
|
|
201
|
+
alter: async (name, diff) => {
|
|
202
|
+
const client = this.#client ??= await this.#factory();
|
|
203
|
+
const db = client.db(this.#database);
|
|
204
|
+
const collections = await db.listCollections({ name }).toArray();
|
|
205
|
+
if (collections.length === 0)
|
|
206
|
+
throw E.table_not_found(name);
|
|
207
|
+
const collection = await this.#collection(name);
|
|
208
|
+
if (diff.rename.length > 0) {
|
|
209
|
+
const $rename = Object.fromEntries(diff.rename);
|
|
210
|
+
await collection.updateMany({}, { $rename });
|
|
211
|
+
}
|
|
212
|
+
if (diff.drop.length > 0) {
|
|
213
|
+
const $unset = Object.fromEntries(diff.drop.map(f => [f, ""]));
|
|
214
|
+
await collection.updateMany({}, { $unset });
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
#to_mongo_pk(field, pk) {
|
|
220
|
+
return field === pk ? "_id" : field;
|
|
221
|
+
}
|
|
222
|
+
#from_mongo_pk(field, pk) {
|
|
223
|
+
return field === "_id" && pk !== null ? pk : field;
|
|
224
|
+
}
|
|
225
|
+
#unbind(as, doc) {
|
|
226
|
+
const pk = as.pk;
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const [field, value] of Object.entries(doc)) {
|
|
229
|
+
const user_field = this.#from_mongo_pk(field, pk);
|
|
230
|
+
const datatype = as.types[user_field];
|
|
231
|
+
if (value === null || value === undefined) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (field === "_id") {
|
|
235
|
+
const type = as.types[user_field];
|
|
236
|
+
out[user_field] = base.is_uuid_type(type)
|
|
237
|
+
? unbind_value(type, value)
|
|
238
|
+
: value;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
out[user_field] = unbind_value(datatype, value);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
async #generate_pk(as) {
|
|
246
|
+
const pk = as.pk;
|
|
247
|
+
const type = as.types[pk];
|
|
248
|
+
const collection = await this.#collection(as.table);
|
|
249
|
+
if (base.is_uuid_type(type))
|
|
250
|
+
return base.generate_uuid(type);
|
|
251
|
+
// for numeric types, find max and increment
|
|
252
|
+
const pipeline = [{ $group: { _id: null, max: { $max: "$_id" } } }];
|
|
253
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
254
|
+
const max = results.length === 0 ? 0 : results[0].max ?? 0;
|
|
255
|
+
if (base.BIGINT_STRING_TYPES.includes(type))
|
|
256
|
+
return BigInt(max) + 1n;
|
|
257
|
+
return Number(max) + 1;
|
|
258
|
+
}
|
|
259
|
+
async create(as, record) {
|
|
260
|
+
assert.dict(record);
|
|
261
|
+
const pk = as.pk;
|
|
262
|
+
const collection = await this.#collection(as.table);
|
|
263
|
+
const doc = {};
|
|
264
|
+
let pk_value = null;
|
|
265
|
+
if (pk !== null) {
|
|
266
|
+
const type = as.types[pk];
|
|
267
|
+
if (pk in record) {
|
|
268
|
+
pk_value = record[pk];
|
|
269
|
+
doc._id = base.is_uuid_type(type)
|
|
270
|
+
? await bind_value(type, pk_value)
|
|
271
|
+
: pk_value;
|
|
272
|
+
}
|
|
273
|
+
else if (as.generate_pk !== false) {
|
|
274
|
+
const generated = await this.#generate_pk(as);
|
|
275
|
+
// convert to user-facing value
|
|
276
|
+
pk_value = generated;
|
|
277
|
+
doc._id = base.is_uuid_type(type)
|
|
278
|
+
? await bind_value(type, generated)
|
|
279
|
+
: generated;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
throw E.pk_required(pk);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const [field, value] of Object.entries(record)) {
|
|
286
|
+
if (field === pk)
|
|
287
|
+
continue;
|
|
288
|
+
doc[field] = await bind_value(as.types[field], value);
|
|
289
|
+
}
|
|
290
|
+
await collection.insertOne(doc);
|
|
291
|
+
const result = { ...record };
|
|
292
|
+
if (pk !== null && !(pk in record) && pk_value !== null) {
|
|
293
|
+
result[pk] = pk_value;
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
async read(as, args) {
|
|
298
|
+
assert.dict(args.where);
|
|
299
|
+
if (args.count === true)
|
|
300
|
+
return this.#count(as, args.where);
|
|
301
|
+
// relations always use phased approach
|
|
302
|
+
if (base.withed(args))
|
|
303
|
+
return this.#read_phased(as, args);
|
|
304
|
+
return this.#read(as, args);
|
|
305
|
+
}
|
|
306
|
+
async #count(as, where) {
|
|
307
|
+
const filter = await bind(as, where);
|
|
308
|
+
const collection = await this.#collection(as.table);
|
|
309
|
+
const count = await collection.countDocuments(filter);
|
|
310
|
+
return count;
|
|
311
|
+
}
|
|
312
|
+
async #read(as, args) {
|
|
313
|
+
const filter = await bind(as, args.where);
|
|
314
|
+
const collection = await this.#collection(as.table);
|
|
315
|
+
const projection = get_projection(as.pk, args.fields);
|
|
316
|
+
const sort = get_sort(args.sort);
|
|
317
|
+
const limit = get_limit(args.limit);
|
|
318
|
+
const options = { useBigInt64: true };
|
|
319
|
+
if (projection)
|
|
320
|
+
options.projection = projection;
|
|
321
|
+
if (sort)
|
|
322
|
+
options.sort = sort;
|
|
323
|
+
const docs = await collection
|
|
324
|
+
.find(filter, options)
|
|
325
|
+
.limit(limit)
|
|
326
|
+
.toArray();
|
|
327
|
+
return docs.map(doc => this.#unbind(as, doc));
|
|
328
|
+
}
|
|
329
|
+
async #read_phased(as, args) {
|
|
330
|
+
const fields = base.expand(as, args.fields, args.with);
|
|
331
|
+
const rows = await this.#read(as, { ...args, fields });
|
|
332
|
+
const out = rows.map(row => base.project(row, args.fields));
|
|
333
|
+
for (const [name, relation] of Object.entries(args.with)) {
|
|
334
|
+
await this.#attach_relation(as, { rows, out, name, relation });
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
async #attach_relation(as, args) {
|
|
339
|
+
const relation = args.relation;
|
|
340
|
+
const by = relation.reverse ? relation.as.pk : relation.fk;
|
|
341
|
+
if (by === null)
|
|
342
|
+
throw E.relation_requires_pk("target");
|
|
343
|
+
const parent_by = relation.reverse ? relation.fk : as.pk;
|
|
344
|
+
if (parent_by === null)
|
|
345
|
+
throw E.relation_requires_pk("parent");
|
|
346
|
+
const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
|
|
347
|
+
const is_many = relation.kind === "many";
|
|
348
|
+
const empty = is_many ? [] : null;
|
|
349
|
+
if (join_values.length === 0) {
|
|
350
|
+
for (const row of args.out)
|
|
351
|
+
row[args.name] = empty;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const related = await this.#load_related({ by, join_values, ...relation });
|
|
355
|
+
const grouped = new Map();
|
|
356
|
+
for (const row of related) {
|
|
357
|
+
const key = row[by];
|
|
358
|
+
grouped.set(key, grouped.get(key)?.concat(row) ?? [row]);
|
|
359
|
+
}
|
|
360
|
+
for (let i = 0; i < args.out.length; i++) {
|
|
361
|
+
const join_value = args.rows[i][parent_by];
|
|
362
|
+
if (join_value == null) {
|
|
363
|
+
args.out[i][args.name] = empty;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const rows = grouped.get(join_value) ?? [];
|
|
367
|
+
args.out[i][args.name] = is_many
|
|
368
|
+
? rows.map(r => base.project(r, relation.fields))
|
|
369
|
+
: rows[0] ? base.project(rows[0], relation.fields) : null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async #load_related(args) {
|
|
373
|
+
// build filter with $in for join values
|
|
374
|
+
const filter = await bind(args.as, args.where);
|
|
375
|
+
const by_field = this.#to_mongo_pk(args.by, args.as.pk);
|
|
376
|
+
// convert join values to ObjectId if needed
|
|
377
|
+
const fk_type = args.as.types[args.by];
|
|
378
|
+
const in_values = base.is_uuid_type(fk_type)
|
|
379
|
+
? await Promise.all(args.join_values.map(v => bind_value(fk_type, v)))
|
|
380
|
+
: args.join_values;
|
|
381
|
+
filter[by_field] = { $in: in_values };
|
|
382
|
+
const collection = await this.#collection(args.as.table);
|
|
383
|
+
const fields_with_by = base.fields(args.fields, args.by);
|
|
384
|
+
const projection = get_projection(args.as.pk, fields_with_by);
|
|
385
|
+
const sort = get_sort(args.sort);
|
|
386
|
+
const options = { useBigInt64: true };
|
|
387
|
+
if (projection)
|
|
388
|
+
options.projection = projection;
|
|
389
|
+
if (sort)
|
|
390
|
+
options.sort = sort;
|
|
391
|
+
// for per-parent limits, fetch all and slice in memory
|
|
392
|
+
// MongoDB doesn't have native per-group limit like SQL's ROW_NUMBER
|
|
393
|
+
const docs = await collection.find(filter, options).toArray();
|
|
394
|
+
const rows = docs.map(doc => this.#unbind(args.as, doc));
|
|
395
|
+
// apply per-parent limit if needed
|
|
396
|
+
const per_parent = args.kind === "one" ? 1 : args.limit;
|
|
397
|
+
if (per_parent !== undefined) {
|
|
398
|
+
const grouped = new Map();
|
|
399
|
+
for (const row of rows) {
|
|
400
|
+
const key = row[args.by];
|
|
401
|
+
const group = grouped.get(key) ?? [];
|
|
402
|
+
if (group.length < per_parent) {
|
|
403
|
+
group.push(row);
|
|
404
|
+
grouped.set(key, group);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return [...grouped.values()].flat();
|
|
408
|
+
}
|
|
409
|
+
return rows;
|
|
410
|
+
}
|
|
411
|
+
async update(as, args) {
|
|
412
|
+
assert.nonempty(args.set);
|
|
413
|
+
assert.dict(args.where);
|
|
414
|
+
const filter = await bind(as, args.where);
|
|
415
|
+
const collection = await this.#collection(as.table);
|
|
416
|
+
const $set = {};
|
|
417
|
+
const $unset = {};
|
|
418
|
+
for (const [field, value] of Object.entries(args.set)) {
|
|
419
|
+
const mongo_field = this.#to_mongo_pk(field, as.pk);
|
|
420
|
+
if (value === null) {
|
|
421
|
+
$unset[mongo_field] = "";
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
$set[mongo_field] = await bind_value(as.types[field], value);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const update = {};
|
|
428
|
+
if (Object.keys($set).length > 0)
|
|
429
|
+
update.$set = $set;
|
|
430
|
+
if (Object.keys($unset).length > 0)
|
|
431
|
+
update.$unset = $unset;
|
|
432
|
+
const result = await collection.updateMany(filter, update);
|
|
433
|
+
return result.modifiedCount;
|
|
434
|
+
}
|
|
435
|
+
async delete(as, args) {
|
|
436
|
+
assert.nonempty(args.where);
|
|
437
|
+
const filter = await bind(as, args.where);
|
|
438
|
+
const collection = await this.#collection(as.table);
|
|
439
|
+
const result = await collection.deleteMany(filter);
|
|
440
|
+
return result.deletedCount;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
//# 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, UUID } from "mongodb";
|
|
2
2
|
function identity(column) {
|
|
3
3
|
return {
|
|
4
4
|
bind: value => value,
|
|
@@ -45,18 +45,7 @@ const typemap = {
|
|
|
45
45
|
i32: identity("INT"),
|
|
46
46
|
i64: identity("LONG"),
|
|
47
47
|
i8: identity("INT"),
|
|
48
|
-
|
|
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
|
-
},
|
|
48
|
+
json: identity("JSON"),
|
|
60
49
|
string: identity("STRING"),
|
|
61
50
|
time: identity("TIME"),
|
|
62
51
|
u128: {
|
|
@@ -81,6 +70,21 @@ const typemap = {
|
|
|
81
70
|
return new URL(value);
|
|
82
71
|
},
|
|
83
72
|
},
|
|
73
|
+
uuid: {
|
|
74
|
+
bind: (value) => new UUID(value),
|
|
75
|
+
column: "UUID",
|
|
76
|
+
unbind: (value) => value.toString(),
|
|
77
|
+
},
|
|
78
|
+
uuid_v4: {
|
|
79
|
+
bind: (value) => new UUID(value),
|
|
80
|
+
column: "UUID",
|
|
81
|
+
unbind: (value) => value.toString(),
|
|
82
|
+
},
|
|
83
|
+
uuid_v7: {
|
|
84
|
+
bind: (value) => new UUID(value),
|
|
85
|
+
column: "UUID",
|
|
86
|
+
unbind: (value) => value.toString(),
|
|
87
|
+
},
|
|
84
88
|
};
|
|
85
89
|
export default typemap;
|
|
86
90
|
//# sourceMappingURL=typemap.js.map
|
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,37 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primate/mongodb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.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.8.1",
|
|
21
|
+
"@rcompat/dict": "^0.5.1",
|
|
22
|
+
"@rcompat/is": "^0.6.1",
|
|
21
23
|
"mongodb": "^6.17.0",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
+
"@primate/core": "^0.6.0",
|
|
25
|
+
"pema": "0.6.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@rcompat/type": "^0.11.1"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"primate": "^0.37.0"
|
|
24
32
|
},
|
|
25
|
-
"type": "module",
|
|
26
33
|
"imports": {
|
|
27
34
|
"#*": {
|
|
28
|
-
"
|
|
35
|
+
"@primate/source": "./src/private/*.ts",
|
|
29
36
|
"default": "./lib/private/*.js"
|
|
30
37
|
}
|
|
31
38
|
},
|
|
32
39
|
"exports": {
|
|
33
40
|
".": {
|
|
34
|
-
"
|
|
41
|
+
"@primate/source": "./src/public/index.ts",
|
|
35
42
|
"default": "./lib/public/index.js"
|
|
36
43
|
}
|
|
37
44
|
},
|
|
@@ -39,7 +46,7 @@
|
|
|
39
46
|
"build": "npm run clean && tsc",
|
|
40
47
|
"clean": "rm -rf ./lib",
|
|
41
48
|
"lint": "eslint .",
|
|
42
|
-
"test": "
|
|
49
|
+
"test": "npx proby",
|
|
43
50
|
"start:db": "sudo mongod --dbpath /var/lib/mongodb"
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -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
|