@primate/mongodb 0.5.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";
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
@@ -1,22 +1,24 @@
1
- import type { As, DataDict, DB, Sort, With } from "@primate/core/db";
1
+ import type { As, DataDict, DB, Schema, Sort, With } from "@primate/core/db";
2
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>;
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;
10
15
  }>;
11
16
  export default class MongoDB implements DB {
12
17
  #private;
13
18
  static config: typeof schema.input;
14
19
  constructor(config?: typeof schema.input);
15
20
  close(): Promise<void>;
16
- get schema(): {
17
- create: (_as: As, _store: StoreSchema) => Promise<void>;
18
- delete: (name: string) => Promise<void>;
19
- };
21
+ get schema(): Schema;
20
22
  create<O extends Dict>(as: As, record: Dict): Promise<O>;
21
23
  read(as: As, args: {
22
24
  count: true;
@@ -1,20 +1,17 @@
1
1
  import typemap from "#typemap";
2
- import common from "@primate/core/db";
3
- import E from "@primate/core/db/error";
2
+ import base from "@primate/core/db";
3
+ import E from "@primate/core/db/errors";
4
4
  import assert from "@rcompat/assert";
5
5
  import is from "@rcompat/is";
6
- import { MongoClient, ObjectId } from "mongodb";
6
+ import { MongoClient } from "mongodb";
7
7
  import p from "pema";
8
8
  const schema = p({
9
- database: p.string,
10
9
  host: p.string.default("localhost"),
11
- password: p.string.optional(),
12
10
  port: p.uint.port().default(27017),
11
+ database: p.string,
13
12
  username: p.string.optional(),
13
+ password: p.string.optional(),
14
14
  });
15
- function is_object_id(x) {
16
- return x instanceof ObjectId;
17
- }
18
15
  function get_limit(limit) {
19
16
  return limit ?? 0; // 0 = no limit
20
17
  }
@@ -52,6 +49,67 @@ async function bind_value(key, value) {
52
49
  function unbind_value(key, value) {
53
50
  return typemap[key].unbind(value);
54
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
+ }
55
113
  function like_to_regex(pattern) {
56
114
  return "^" + pattern
57
115
  .replace(/\\%/g, "<<PERCENT>>")
@@ -62,6 +120,22 @@ function like_to_regex(pattern) {
62
120
  .replace(/<<PERCENT>>/g, "%")
63
121
  .replace(/<<UNDERSCORE>>/g, "_") + "$";
64
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
+ }
65
139
  export default class MongoDB {
66
140
  static config;
67
141
  #factory;
@@ -86,12 +160,59 @@ export default class MongoDB {
86
160
  }
87
161
  get schema() {
88
162
  return {
89
- create: async (_as, _store) => {
90
- // MongoDB is schemaless, noop
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(() => { });
91
174
  },
92
- delete: async (name) => {
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;
93
181
  const collection = await this.#collection(name);
94
- await collection.drop().catch(() => { }); // ignore if doesn't exist
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
+ }
95
216
  },
96
217
  };
97
218
  }
@@ -101,64 +222,6 @@ export default class MongoDB {
101
222
  #from_mongo_pk(field, pk) {
102
223
  return field === "_id" && pk !== null ? pk : field;
103
224
  }
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
225
  #unbind(as, doc) {
163
226
  const pk = as.pk;
164
227
  const out = {};
@@ -169,8 +232,10 @@ export default class MongoDB {
169
232
  continue;
170
233
  }
171
234
  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;
235
+ const type = as.types[user_field];
236
+ out[user_field] = base.is_uuid_type(type)
237
+ ? unbind_value(type, value)
238
+ : value;
174
239
  continue;
175
240
  }
176
241
  out[user_field] = unbind_value(datatype, value);
@@ -181,13 +246,13 @@ export default class MongoDB {
181
246
  const pk = as.pk;
182
247
  const type = as.types[pk];
183
248
  const collection = await this.#collection(as.table);
184
- if (type === "string")
185
- return new ObjectId();
249
+ if (base.is_uuid_type(type))
250
+ return base.generate_uuid(type);
186
251
  // for numeric types, find max and increment
187
252
  const pipeline = [{ $group: { _id: null, max: { $max: "$_id" } } }];
188
253
  const results = await collection.aggregate(pipeline).toArray();
189
254
  const max = results.length === 0 ? 0 : results[0].max ?? 0;
190
- if (common.BIGINT_STRING_TYPES.includes(type))
255
+ if (base.BIGINT_STRING_TYPES.includes(type))
191
256
  return BigInt(max) + 1n;
192
257
  return Number(max) + 1;
193
258
  }
@@ -201,15 +266,17 @@ export default class MongoDB {
201
266
  const type = as.types[pk];
202
267
  if (pk in record) {
203
268
  pk_value = record[pk];
204
- doc._id = type === "string" && ObjectId.isValid(pk_value)
205
- ? new ObjectId(pk_value)
269
+ doc._id = base.is_uuid_type(type)
270
+ ? await bind_value(type, pk_value)
206
271
  : pk_value;
207
272
  }
208
273
  else if (as.generate_pk !== false) {
209
274
  const generated = await this.#generate_pk(as);
210
- doc._id = generated;
211
275
  // convert to user-facing value
212
- pk_value = is_object_id(generated) ? generated.toString() : generated;
276
+ pk_value = generated;
277
+ doc._id = base.is_uuid_type(type)
278
+ ? await bind_value(type, generated)
279
+ : generated;
213
280
  }
214
281
  else {
215
282
  throw E.pk_required(pk);
@@ -232,18 +299,18 @@ export default class MongoDB {
232
299
  if (args.count === true)
233
300
  return this.#count(as, args.where);
234
301
  // relations always use phased approach
235
- if (common.withed(args))
302
+ if (base.withed(args))
236
303
  return this.#read_phased(as, args);
237
304
  return this.#read(as, args);
238
305
  }
239
306
  async #count(as, where) {
240
- const filter = await this.#bind(as, where);
307
+ const filter = await bind(as, where);
241
308
  const collection = await this.#collection(as.table);
242
309
  const count = await collection.countDocuments(filter);
243
310
  return count;
244
311
  }
245
312
  async #read(as, args) {
246
- const filter = await this.#bind(as, args.where);
313
+ const filter = await bind(as, args.where);
247
314
  const collection = await this.#collection(as.table);
248
315
  const projection = get_projection(as.pk, args.fields);
249
316
  const sort = get_sort(args.sort);
@@ -260,9 +327,9 @@ export default class MongoDB {
260
327
  return docs.map(doc => this.#unbind(as, doc));
261
328
  }
262
329
  async #read_phased(as, args) {
263
- const fields = common.expand(as, args.fields, args.with);
330
+ const fields = base.expand(as, args.fields, args.with);
264
331
  const rows = await this.#read(as, { ...args, fields });
265
- const out = rows.map(row => common.project(row, args.fields));
332
+ const out = rows.map(row => base.project(row, args.fields));
266
333
  for (const [name, relation] of Object.entries(args.with)) {
267
334
  await this.#attach_relation(as, { rows, out, name, relation });
268
335
  }
@@ -298,22 +365,22 @@ export default class MongoDB {
298
365
  }
299
366
  const rows = grouped.get(join_value) ?? [];
300
367
  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;
368
+ ? rows.map(r => base.project(r, relation.fields))
369
+ : rows[0] ? base.project(rows[0], relation.fields) : null;
303
370
  }
304
371
  }
305
372
  async #load_related(args) {
306
373
  // build filter with $in for join values
307
- const filter = await this.#bind(args.as, args.where);
374
+ const filter = await bind(args.as, args.where);
308
375
  const by_field = this.#to_mongo_pk(args.by, args.as.pk);
309
376
  // 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)
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)))
313
380
  : args.join_values;
314
381
  filter[by_field] = { $in: in_values };
315
382
  const collection = await this.#collection(args.as.table);
316
- const fields_with_by = common.fields(args.fields, args.by);
383
+ const fields_with_by = base.fields(args.fields, args.by);
317
384
  const projection = get_projection(args.as.pk, fields_with_by);
318
385
  const sort = get_sort(args.sort);
319
386
  const options = { useBigInt64: true };
@@ -344,7 +411,7 @@ export default class MongoDB {
344
411
  async update(as, args) {
345
412
  assert.nonempty(args.set);
346
413
  assert.dict(args.where);
347
- const filter = await this.#bind(as, args.where);
414
+ const filter = await bind(as, args.where);
348
415
  const collection = await this.#collection(as.table);
349
416
  const $set = {};
350
417
  const $unset = {};
@@ -367,7 +434,7 @@ export default class MongoDB {
367
434
  }
368
435
  async delete(as, args) {
369
436
  assert.nonempty(args.where);
370
- const filter = await this.#bind(as, args.where);
437
+ const filter = await bind(as, args.where);
371
438
  const collection = await this.#collection(as.table);
372
439
  const result = await collection.deleteMany(filter);
373
440
  return result.deletedCount;
@@ -1,4 +1,4 @@
1
- import { Binary, Decimal128 } from "mongodb";
1
+ import { Binary, Decimal128, UUID } from "mongodb";
2
2
  function identity(column) {
3
3
  return {
4
4
  bind: value => value,
@@ -45,6 +45,7 @@ const typemap = {
45
45
  i32: identity("INT"),
46
46
  i64: identity("LONG"),
47
47
  i8: identity("INT"),
48
+ json: identity("JSON"),
48
49
  string: identity("STRING"),
49
50
  time: identity("TIME"),
50
51
  u128: {
@@ -69,6 +70,21 @@ const typemap = {
69
70
  return new URL(value);
70
71
  },
71
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
+ },
72
88
  };
73
89
  export default typemap;
74
90
  //# sourceMappingURL=typemap.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primate/mongodb",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
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",
@@ -17,28 +17,28 @@
17
17
  "!/**/*.spec.*"
18
18
  ],
19
19
  "dependencies": {
20
- "@rcompat/assert": "^0.6.0",
21
- "@rcompat/dict": "^0.3.1",
22
- "@rcompat/is": "^0.4.3",
20
+ "@rcompat/assert": "^0.8.1",
21
+ "@rcompat/dict": "^0.5.1",
22
+ "@rcompat/is": "^0.6.1",
23
23
  "mongodb": "^6.17.0",
24
- "@primate/core": "^0.5.0",
25
- "pema": "0.5.0"
24
+ "@primate/core": "^0.6.0",
25
+ "pema": "0.6.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@rcompat/type": "^0.9.0"
28
+ "@rcompat/type": "^0.11.1"
29
29
  },
30
30
  "peerDependencies": {
31
- "primate": "^0.36.0"
31
+ "primate": "^0.37.0"
32
32
  },
33
33
  "imports": {
34
34
  "#*": {
35
- "apekit": "./src/private/*.ts",
35
+ "@primate/source": "./src/private/*.ts",
36
36
  "default": "./lib/private/*.js"
37
37
  }
38
38
  },
39
39
  "exports": {
40
40
  ".": {
41
- "apekit": "./src/public/index.ts",
41
+ "@primate/source": "./src/public/index.ts",
42
42
  "default": "./lib/public/index.js"
43
43
  }
44
44
  },
@@ -46,7 +46,7 @@
46
46
  "build": "npm run clean && tsc",
47
47
  "clean": "rm -rf ./lib",
48
48
  "lint": "eslint .",
49
- "test": "npm run build && npx proby",
49
+ "test": "npx proby",
50
50
  "start:db": "sudo mongod --dbpath /var/lib/mongodb"
51
51
  }
52
52
  }