@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.
@@ -1,6 +1,6 @@
1
- import type TypedArray from "@rcompat/type/TypedArray";
2
- import type { Binary, Decimal128, ObjectId } from "mongodb";
3
- type Param = bigint | Binary | boolean | Date | Decimal128 | number | ObjectId | string | TypedArray;
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: string;
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
@@ -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, 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
- 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
- },
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
@@ -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,37 +1,44 @@
1
1
  {
2
2
  "name": "@primate/mongodb",
3
- "version": "0.4.0",
4
- "description": "Primate MongoDB database",
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.3.1",
20
- "@rcompat/record": "^0.9.1",
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
- "pema": "",
23
- "@primate/core": "^0.4.0"
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
- "apekit": "./src/private/*.ts",
35
+ "@primate/source": "./src/private/*.ts",
29
36
  "default": "./lib/private/*.js"
30
37
  }
31
38
  },
32
39
  "exports": {
33
40
  ".": {
34
- "apekit": "./src/public/index.ts",
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": "npm run build && npx proby",
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
@@ -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