@primate/mongodb 0.3.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.
@@ -1,4 +1,4 @@
1
- import type TypedArray from "@rcompat/type/TypedArray";
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
@@ -1,5 +1,5 @@
1
1
  import type ColumnTypes from "#ColumnTypes";
2
- import type TypeMap from "@primate/core/database/TypeMap";
2
+ import type { TypeMap } from "@primate/core/db";
3
3
  declare const typemap: TypeMap<ColumnTypes>;
4
4
  export default typemap;
5
5
  //# sourceMappingURL=typemap.d.ts.map
@@ -1,4 +1,4 @@
1
- import { Binary, Decimal128, ObjectId } from "mongodb";
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: {
@@ -1,4 +1,4 @@
1
- import Database from "#Database";
2
- declare const _default: (config: typeof Database.config) => Database;
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
@@ -1,3 +1,3 @@
1
- import Database from "#Database";
2
- export default (config) => new Database(config);
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.3.0",
4
- "description": "Primate MongoDB database",
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.3.1",
20
- "@rcompat/record": "^0.9.1",
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
- "pema": "",
23
- "@primate/core": "^0.3.0"
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
@@ -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