@zodmon/core 0.10.0 → 0.11.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/dist/index.js CHANGED
@@ -11,30 +11,24 @@ var $avg = (field) => ({
11
11
  __accum: true,
12
12
  expr: { $avg: field }
13
13
  });
14
- var $min = (field) => ({
15
- __accum: true,
16
- expr: { $min: field }
17
- });
18
- var $max = (field) => ({
19
- __accum: true,
20
- expr: { $max: field }
21
- });
22
- var $first = (field) => ({
23
- __accum: true,
24
- expr: { $first: field }
25
- });
26
- var $last = (field) => ({
27
- __accum: true,
28
- expr: { $last: field }
29
- });
30
- var $push = (field) => ({
31
- __accum: true,
32
- expr: { $push: field }
33
- });
34
- var $addToSet = (field) => ({
35
- __accum: true,
36
- expr: { $addToSet: field }
37
- });
14
+ function $min(field) {
15
+ return { __accum: true, expr: { $min: field } };
16
+ }
17
+ function $max(field) {
18
+ return { __accum: true, expr: { $max: field } };
19
+ }
20
+ function $first(field) {
21
+ return { __accum: true, expr: { $first: field } };
22
+ }
23
+ function $last(field) {
24
+ return { __accum: true, expr: { $last: field } };
25
+ }
26
+ function $push(field) {
27
+ return { __accum: true, expr: { $push: field } };
28
+ }
29
+ function $addToSet(field) {
30
+ return { __accum: true, expr: { $addToSet: field } };
31
+ }
38
32
  function createAccumulatorBuilder() {
39
33
  return {
40
34
  count: () => ({ __accum: true, expr: { $sum: 1 } }),
@@ -110,9 +104,9 @@ var ZodmonError = class extends Error {
110
104
  /** The underlying error that caused this error, if any. */
111
105
  cause;
112
106
  constructor(message, collection2, options) {
113
- super(message);
107
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
114
108
  this.collection = collection2;
115
- if (options?.cause) {
109
+ if (options?.cause !== void 0) {
116
110
  this.cause = options.cause;
117
111
  }
118
112
  }
@@ -1240,6 +1234,54 @@ function resolveSortKeys(sortSpec) {
1240
1234
  return entries;
1241
1235
  }
1242
1236
 
1237
+ // src/query/projection.ts
1238
+ function isIncludeValue(value) {
1239
+ return value === 1 || value === true;
1240
+ }
1241
+ function isExcludeValue(value) {
1242
+ return value === 0 || value === false;
1243
+ }
1244
+ function isInclusionProjection(projection) {
1245
+ for (const key of Object.keys(projection)) {
1246
+ if (key === "_id") continue;
1247
+ const value = projection[key];
1248
+ if (value !== void 0 && isIncludeValue(value)) return true;
1249
+ }
1250
+ return false;
1251
+ }
1252
+ function buildPickMask(projection, schemaKeys) {
1253
+ const mask = {};
1254
+ const idValue = projection._id;
1255
+ if (!(idValue !== void 0 && isExcludeValue(idValue)) && schemaKeys.has("_id")) {
1256
+ mask._id = true;
1257
+ }
1258
+ for (const key of Object.keys(projection)) {
1259
+ if (key === "_id") continue;
1260
+ const value = projection[key];
1261
+ if (value !== void 0 && isIncludeValue(value) && schemaKeys.has(key)) {
1262
+ mask[key] = true;
1263
+ }
1264
+ }
1265
+ return mask;
1266
+ }
1267
+ function buildOmitMask(projection, schemaKeys) {
1268
+ const mask = {};
1269
+ for (const key of Object.keys(projection)) {
1270
+ const value = projection[key];
1271
+ if (value !== void 0 && isExcludeValue(value) && schemaKeys.has(key)) {
1272
+ mask[key] = true;
1273
+ }
1274
+ }
1275
+ return mask;
1276
+ }
1277
+ function deriveProjectedSchema(schema, projection) {
1278
+ const schemaKeys = new Set(Object.keys(schema.shape));
1279
+ if (isInclusionProjection(projection)) {
1280
+ return schema.pick(buildPickMask(projection, schemaKeys));
1281
+ }
1282
+ return schema.omit(buildOmitMask(projection, schemaKeys));
1283
+ }
1284
+
1243
1285
  // src/query/cursor.ts
1244
1286
  var TypedFindCursor = class {
1245
1287
  /** @internal */
@@ -1258,6 +1300,8 @@ var TypedFindCursor = class {
1258
1300
  /** @internal */
1259
1301
  sortSpec;
1260
1302
  /** @internal */
1303
+ projectedSchema;
1304
+ /** @internal */
1261
1305
  constructor(cursor, definition, mode, nativeCollection, filter) {
1262
1306
  this.cursor = cursor;
1263
1307
  this.schema = definition.schema;
@@ -1266,6 +1310,7 @@ var TypedFindCursor = class {
1266
1310
  this.nativeCollection = nativeCollection;
1267
1311
  this.filter = filter;
1268
1312
  this.sortSpec = null;
1313
+ this.projectedSchema = null;
1269
1314
  }
1270
1315
  /**
1271
1316
  * Set the sort order for the query.
@@ -1339,6 +1384,40 @@ var TypedFindCursor = class {
1339
1384
  this.cursor.hint(indexName);
1340
1385
  return this;
1341
1386
  }
1387
+ /**
1388
+ * Apply a projection to narrow the returned fields.
1389
+ *
1390
+ * Inclusion projections (`{ name: 1 }`) return only the specified fields
1391
+ * plus `_id` (unless `_id: 0`). Exclusion projections (`{ email: 0 }`)
1392
+ * return all fields except those excluded.
1393
+ *
1394
+ * The cursor's output type is narrowed at compile time. A derived Zod
1395
+ * schema is built for runtime validation of the projected fields.
1396
+ *
1397
+ * Projects from the original document type, not from a previous projection.
1398
+ * Calling `.project()` twice overrides the previous projection.
1399
+ *
1400
+ * @param spec - Type-safe projection document.
1401
+ * @returns A new cursor with the narrowed output type.
1402
+ *
1403
+ * @example
1404
+ * ```ts
1405
+ * const names = await find(users, {})
1406
+ * .project({ name: 1 })
1407
+ * .sort({ name: 1 })
1408
+ * .toArray()
1409
+ * // names[0].name ✓
1410
+ * // names[0].email TS error
1411
+ * ```
1412
+ */
1413
+ project(spec) {
1414
+ this.cursor.project(spec);
1415
+ this.projectedSchema = deriveProjectedSchema(
1416
+ this.schema,
1417
+ spec
1418
+ );
1419
+ return this;
1420
+ }
1342
1421
  async paginate(opts) {
1343
1422
  const sortRecord = this.sortSpec ? this.sortSpec : null;
1344
1423
  const sortKeys2 = resolveSortKeys(sortRecord);
@@ -1455,8 +1534,9 @@ var TypedFindCursor = class {
1455
1534
  if (this.mode === false || this.mode === "passthrough") {
1456
1535
  return raw2;
1457
1536
  }
1537
+ const schema = this.projectedSchema ?? this.schema;
1458
1538
  try {
1459
- return this.schema.parse(raw2);
1539
+ return schema.parse(raw2);
1460
1540
  } catch (err) {
1461
1541
  if (err instanceof z3.ZodError) {
1462
1542
  throw new ZodmonValidationError(this.collectionName, err, raw2);
@@ -1469,7 +1549,8 @@ var TypedFindCursor = class {
1469
1549
  // src/crud/find.ts
1470
1550
  async function findOne(handle, filter, options) {
1471
1551
  checkUnindexedFields(handle.definition, filter);
1472
- const findOptions = options?.project ? { projection: options.project } : void 0;
1552
+ const project = options && "project" in options ? options.project : void 0;
1553
+ const findOptions = project ? { projection: project } : void 0;
1473
1554
  let raw2;
1474
1555
  try {
1475
1556
  raw2 = await handle.native.findOne(filter, findOptions);
@@ -1481,8 +1562,12 @@ async function findOne(handle, filter, options) {
1481
1562
  if (mode === false || mode === "passthrough") {
1482
1563
  return raw2;
1483
1564
  }
1565
+ const schema = project ? deriveProjectedSchema(
1566
+ handle.definition.schema,
1567
+ project
1568
+ ) : handle.definition.schema;
1484
1569
  try {
1485
- return handle.definition.schema.parse(raw2);
1570
+ return schema.parse(raw2);
1486
1571
  } catch (err) {
1487
1572
  if (err instanceof z4.ZodError) {
1488
1573
  throw new ZodmonValidationError(handle.definition.name, err, raw2);
@@ -1502,7 +1587,12 @@ function find(handle, filter, options) {
1502
1587
  const raw2 = handle.native.find(filter);
1503
1588
  const cursor = raw2;
1504
1589
  const mode = options?.validate !== void 0 ? options.validate : handle.definition.options.validation;
1505
- return new TypedFindCursor(cursor, handle.definition, mode, handle.native, filter);
1590
+ const typedCursor = new TypedFindCursor(cursor, handle.definition, mode, handle.native, filter);
1591
+ const project = options && "project" in options ? options.project : void 0;
1592
+ if (project) {
1593
+ return typedCursor.project(project);
1594
+ }
1595
+ return typedCursor;
1506
1596
  }
1507
1597
 
1508
1598
  // src/crud/insert.ts
@@ -1651,70 +1741,12 @@ var CollectionHandle = class {
1651
1741
  async insertMany(docs) {
1652
1742
  return await insertMany(this, docs);
1653
1743
  }
1654
- /**
1655
- * Find a single document matching the filter.
1656
- *
1657
- * Queries MongoDB, then validates the fetched document against the collection's
1658
- * Zod schema. Validation mode is resolved from the per-query option, falling
1659
- * back to the collection-level default (which defaults to `'strict'`).
1660
- *
1661
- * @param filter - Type-safe filter to match documents.
1662
- * @param options - Optional projection and validation overrides.
1663
- * @returns The matched document, or `null` if no document matches.
1664
- * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
1665
- *
1666
- * @example
1667
- * ```ts
1668
- * const users = db.use(Users)
1669
- * const user = await users.findOne({ name: 'Ada' })
1670
- * if (user) console.log(user.role)
1671
- * ```
1672
- */
1673
1744
  async findOne(filter, options) {
1674
1745
  return await findOne(this, filter, options);
1675
1746
  }
1676
- /**
1677
- * Find a single document matching the filter, or throw if none exists.
1678
- *
1679
- * Behaves identically to {@link findOne} but throws {@link ZodmonNotFoundError}
1680
- * instead of returning `null` when no document matches the filter.
1681
- *
1682
- * @param filter - Type-safe filter to match documents.
1683
- * @param options - Optional projection and validation overrides.
1684
- * @returns The matched document (never null).
1685
- * @throws {ZodmonNotFoundError} When no document matches the filter.
1686
- * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
1687
- *
1688
- * @example
1689
- * ```ts
1690
- * const users = db.use(Users)
1691
- * const user = await users.findOneOrThrow({ name: 'Ada' })
1692
- * console.log(user.role) // guaranteed non-null
1693
- * ```
1694
- */
1695
1747
  async findOneOrThrow(filter, options) {
1696
1748
  return await findOneOrThrow(this, filter, options);
1697
1749
  }
1698
- /**
1699
- * Find all documents matching the filter, returning a chainable typed cursor.
1700
- *
1701
- * The cursor is lazy — no query is executed until a terminal method
1702
- * (`toArray`, `for await`) is called. Use `sort`, `skip`, and `limit`
1703
- * to shape the query before executing.
1704
- *
1705
- * @param filter - Type-safe filter to match documents.
1706
- * @param options - Optional validation overrides.
1707
- * @returns A typed cursor for chaining query modifiers.
1708
- *
1709
- * @example
1710
- * ```ts
1711
- * const users = db.use(Users)
1712
- * const admins = await users.find({ role: 'admin' })
1713
- * .sort({ name: 1 })
1714
- * .limit(10)
1715
- * .toArray()
1716
- * ```
1717
- */
1718
1750
  find(filter, options) {
1719
1751
  return find(this, filter, options);
1720
1752
  }
@@ -2302,6 +2334,7 @@ export {
2302
2334
  createExpressionBuilder,
2303
2335
  deleteMany,
2304
2336
  deleteOne,
2337
+ deriveProjectedSchema,
2305
2338
  extractComparableOptions,
2306
2339
  extractDbName,
2307
2340
  extractFieldIndexes,
@@ -2316,6 +2349,7 @@ export {
2316
2349
  index,
2317
2350
  insertMany,
2318
2351
  insertOne,
2352
+ isInclusionProjection,
2319
2353
  isOid,
2320
2354
  objectId,
2321
2355
  oid,