@gravito/dark-matter 1.0.0 → 1.1.2

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
@@ -12,6 +12,7 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
12
12
  sortSpec = {};
13
13
  limitCount;
14
14
  skipCount;
15
+ softDeleteMode = "exclude";
15
16
  // ============================================================================
16
17
  // WHERE Clauses
17
18
  // ============================================================================
@@ -607,6 +608,163 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
607
608
  acknowledged: result.acknowledged
608
609
  };
609
610
  }
611
+ // ============================================================================
612
+ // Soft Delete Methods
613
+ // ============================================================================
614
+ /**
615
+ * 包含已軟刪除的記錄
616
+ *
617
+ * 查詢時包含所有記錄,不過濾已刪除的文檔
618
+ *
619
+ * @returns 當前查詢建構器實例,支援鏈式調用
620
+ *
621
+ * @example
622
+ * ```typescript
623
+ * const allUsers = await query.withTrashed().get();
624
+ * ```
625
+ */
626
+ withTrashed() {
627
+ this.softDeleteMode = "include";
628
+ return this;
629
+ }
630
+ /**
631
+ * 只查詢已軟刪除的記錄
632
+ *
633
+ * 只返回 deletedAt 不為 null 的文檔
634
+ *
635
+ * @returns 當前查詢建構器實例,支援鏈式調用
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * const trashedUsers = await query.onlyTrashed().get();
640
+ * ```
641
+ */
642
+ onlyTrashed() {
643
+ this.softDeleteMode = "only";
644
+ return this;
645
+ }
646
+ /**
647
+ * 軟刪除單一記錄
648
+ *
649
+ * 設置 deletedAt 為當前時間,而非真正刪除記錄
650
+ *
651
+ * @returns Promise 解析為更新結果
652
+ *
653
+ * @example
654
+ * ```typescript
655
+ * await query.where('_id', userId).softDelete();
656
+ * ```
657
+ */
658
+ async softDelete() {
659
+ const updateDoc = { $set: { deletedAt: /* @__PURE__ */ new Date() } };
660
+ const result = await this.nativeCollection.updateOne(this.toFilter(), updateDoc, {
661
+ session: this.session
662
+ });
663
+ return {
664
+ matchedCount: result.matchedCount,
665
+ modifiedCount: result.modifiedCount,
666
+ acknowledged: result.acknowledged
667
+ };
668
+ }
669
+ /**
670
+ * 批次軟刪除
671
+ *
672
+ * 設置所有符合條件的文檔的 deletedAt 為當前時間
673
+ *
674
+ * @returns Promise 解析為更新結果
675
+ *
676
+ * @example
677
+ * ```typescript
678
+ * await query.where('status', 'inactive').softDeleteMany();
679
+ * ```
680
+ */
681
+ async softDeleteMany() {
682
+ const updateDoc = { $set: { deletedAt: /* @__PURE__ */ new Date() } };
683
+ const result = await this.nativeCollection.updateMany(this.toFilter(), updateDoc, {
684
+ session: this.session
685
+ });
686
+ return {
687
+ matchedCount: result.matchedCount,
688
+ modifiedCount: result.modifiedCount,
689
+ acknowledged: result.acknowledged
690
+ };
691
+ }
692
+ /**
693
+ * 恢復軟刪除的記錄
694
+ *
695
+ * 將 deletedAt 設置為 null,恢復軟刪除的文檔
696
+ *
697
+ * @returns Promise 解析為更新結果
698
+ *
699
+ * @example
700
+ * ```typescript
701
+ * await query.where('_id', userId).restore();
702
+ * ```
703
+ */
704
+ async restore() {
705
+ const updateDoc = { $set: { deletedAt: null } };
706
+ const result = await this.nativeCollection.updateOne(this.toFilter(), updateDoc, {
707
+ session: this.session
708
+ });
709
+ return {
710
+ matchedCount: result.matchedCount,
711
+ modifiedCount: result.modifiedCount,
712
+ acknowledged: result.acknowledged
713
+ };
714
+ }
715
+ /**
716
+ * 批次恢復軟刪除的記錄
717
+ *
718
+ * 將所有符合條件的文檔的 deletedAt 設置為 null
719
+ *
720
+ * @returns Promise 解析為更新結果
721
+ *
722
+ * @example
723
+ * ```typescript
724
+ * await query.onlyTrashed().restoreMany();
725
+ * ```
726
+ */
727
+ async restoreMany() {
728
+ const updateDoc = { $set: { deletedAt: null } };
729
+ const result = await this.nativeCollection.updateMany(this.toFilter(), updateDoc, {
730
+ session: this.session
731
+ });
732
+ return {
733
+ matchedCount: result.matchedCount,
734
+ modifiedCount: result.modifiedCount,
735
+ acknowledged: result.acknowledged
736
+ };
737
+ }
738
+ /**
739
+ * 強制刪除(真正刪除記錄)
740
+ *
741
+ * 永久刪除單一文檔,無法恢復
742
+ *
743
+ * @returns Promise 解析為刪除結果
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * await query.where('_id', userId).forceDelete();
748
+ * ```
749
+ */
750
+ async forceDelete() {
751
+ return await this.delete();
752
+ }
753
+ /**
754
+ * 批次強制刪除
755
+ *
756
+ * 永久刪除所有符合條件的文檔,無法恢復
757
+ *
758
+ * @returns Promise 解析為刪除結果
759
+ *
760
+ * @example
761
+ * ```typescript
762
+ * await query.where('createdAt', '<', oneYearAgo).forceDeleteMany();
763
+ * ```
764
+ */
765
+ async forceDeleteMany() {
766
+ return await this.deleteMany();
767
+ }
610
768
  /**
611
769
  * Executes a bulk write operation.
612
770
  *
@@ -715,17 +873,15 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
715
873
  toFilter() {
716
874
  const hasMainFilters = Object.keys(this.filters).length > 0;
717
875
  const hasOrFilters = this.orFilters.length > 0;
876
+ let baseFilter;
718
877
  if (!hasOrFilters) {
719
- return { ...this.filters };
720
- }
721
- if (!hasMainFilters) {
722
- return {
723
- $or: this.orFilters
724
- };
878
+ baseFilter = { ...this.filters };
879
+ } else if (!hasMainFilters) {
880
+ baseFilter = { $or: this.orFilters };
881
+ } else {
882
+ baseFilter = { $or: [this.filters, ...this.orFilters] };
725
883
  }
726
- return {
727
- $or: [this.filters, ...this.orFilters]
728
- };
884
+ return this.applySoftDeleteFilter(baseFilter);
729
885
  }
730
886
  /**
731
887
  * Creates a deep copy of the query builder.
@@ -753,6 +909,7 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
753
909
  cloned.sortSpec = { ...this.sortSpec };
754
910
  cloned.limitCount = this.limitCount;
755
911
  cloned.skipCount = this.skipCount;
912
+ cloned.softDeleteMode = this.softDeleteMode;
756
913
  return cloned;
757
914
  }
758
915
  // ============================================================================
@@ -791,6 +948,34 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
791
948
  }
792
949
  return { $set: update };
793
950
  }
951
+ /**
952
+ * 應用軟刪除過濾
953
+ *
954
+ * 根據 softDeleteMode 自動過濾已刪除的記錄
955
+ *
956
+ * @param filter - 基礎過濾條件
957
+ * @returns 包含軟刪除過濾的完整過濾條件
958
+ */
959
+ applySoftDeleteFilter(filter) {
960
+ if (this.softDeleteMode === "include") {
961
+ return filter;
962
+ }
963
+ if (this.softDeleteMode === "only") {
964
+ return {
965
+ ...filter,
966
+ deletedAt: { $ne: null }
967
+ };
968
+ }
969
+ const softDeleteFilter = {
970
+ $or: [{ deletedAt: null }, { deletedAt: { $exists: false } }]
971
+ };
972
+ if (Object.keys(filter).length === 0) {
973
+ return softDeleteFilter;
974
+ }
975
+ return {
976
+ $and: [filter, softDeleteFilter]
977
+ };
978
+ }
794
979
  async getObjectId() {
795
980
  if (_MongoQueryBuilder.ObjectIdCtor) {
796
981
  return _MongoQueryBuilder.ObjectIdCtor;
@@ -917,17 +1102,27 @@ var MongoClient = class {
917
1102
  if (this.config.socketTimeoutMS) {
918
1103
  options.socketTimeoutMS = this.config.socketTimeoutMS;
919
1104
  }
920
- this.client = new this.mongodb.MongoClient(uri, options);
921
1105
  let lastError = null;
922
1106
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
1107
+ const client = new this.mongodb.MongoClient(uri, options);
1108
+ this.client = client;
923
1109
  try {
924
- await this.client.connect();
1110
+ await client.connect();
925
1111
  const dbName = this.config.database ?? "test";
926
- this.db = this.client.db(dbName);
1112
+ this.db = client.db(dbName);
927
1113
  this.connected = true;
928
1114
  return;
929
1115
  } catch (error) {
930
1116
  lastError = error;
1117
+ this.connected = false;
1118
+ this.db = null;
1119
+ try {
1120
+ await client.close();
1121
+ } catch {
1122
+ }
1123
+ if (this.client === client) {
1124
+ this.client = null;
1125
+ }
931
1126
  if (attempt < config.maxRetries) {
932
1127
  const delay = config.retryDelayMs * config.backoffMultiplier ** attempt;
933
1128
  await new Promise((resolve) => setTimeout(resolve, delay));
@@ -1168,12 +1363,38 @@ var MongoDatabaseWrapper = class {
1168
1363
  }
1169
1364
  await this.db.createCollection(name, createOptions);
1170
1365
  }
1171
- async setValidation(collectionName, schema) {
1366
+ async setValidation(collectionName, schema2) {
1172
1367
  await this.db.command({
1173
1368
  collMod: collectionName,
1174
- validator: schema.validator,
1175
- validationLevel: schema.validationLevel ?? "strict",
1176
- validationAction: schema.validationAction ?? "error"
1369
+ validator: schema2.validator,
1370
+ validationLevel: schema2.validationLevel ?? "strict",
1371
+ validationAction: schema2.validationAction ?? "error"
1372
+ });
1373
+ }
1374
+ /**
1375
+ * 使用 Schema Builder 建立 Collection
1376
+ *
1377
+ * 提供友善的 API 來建立帶有 Schema 驗證的 Collection
1378
+ *
1379
+ * @param name - Collection 名稱
1380
+ * @param schemaBuilder - Schema Builder 實例
1381
+ * @param options - 驗證選項(驗證等級、動作)
1382
+ *
1383
+ * @example
1384
+ * ```typescript
1385
+ * import { schema } from '@gravito/dark-matter'
1386
+ *
1387
+ * const userSchema = schema()
1388
+ * .required('name', 'email')
1389
+ * .string('name')
1390
+ * .string('email')
1391
+ *
1392
+ * await Mongo.database().createCollectionWithSchema('users', userSchema)
1393
+ * ```
1394
+ */
1395
+ async createCollectionWithSchema(name, schemaBuilder, options) {
1396
+ await this.createCollection(name, {
1397
+ schema: schemaBuilder.toValidationOptions(options)
1177
1398
  });
1178
1399
  }
1179
1400
  };
@@ -1483,7 +1704,9 @@ var MongoGridFS = class {
1483
1704
  const reader = source.getReader();
1484
1705
  while (true) {
1485
1706
  const { done, value } = await reader.read();
1486
- if (done) break;
1707
+ if (done) {
1708
+ break;
1709
+ }
1487
1710
  uploadStream.write(value);
1488
1711
  }
1489
1712
  uploadStream.end();
@@ -1552,6 +1775,190 @@ var MongoGridFS = class {
1552
1775
  const cursor = this.bucket.find(filter ?? {});
1553
1776
  return await cursor.toArray();
1554
1777
  }
1778
+ /**
1779
+ * 串流上傳檔案
1780
+ *
1781
+ * 支援進度回調的串流上傳,適合大檔案上傳
1782
+ *
1783
+ * @param stream - ReadableStream 來源
1784
+ * @param options - 上傳選項
1785
+ * @param onProgress - 可選的進度回調函數
1786
+ * @returns Promise 解析為檔案 ID
1787
+ *
1788
+ * @example
1789
+ * ```typescript
1790
+ * const stream = file.stream()
1791
+ * const fileId = await grid.uploadStream(stream, {
1792
+ * filename: 'video.mp4'
1793
+ * }, (progress) => {
1794
+ * console.log(`上傳進度: ${progress.percentage}%`)
1795
+ * })
1796
+ * ```
1797
+ */
1798
+ async uploadStream(stream, options, onProgress) {
1799
+ await this.ensureBucket();
1800
+ const uploadStream = this.bucket.openUploadStream(options.filename, {
1801
+ chunkSizeBytes: options.chunkSizeBytes,
1802
+ metadata: options.metadata,
1803
+ contentType: options.contentType
1804
+ });
1805
+ let bytesWritten = 0;
1806
+ const reader = stream.getReader();
1807
+ try {
1808
+ while (true) {
1809
+ const { done, value } = await reader.read();
1810
+ if (done) {
1811
+ break;
1812
+ }
1813
+ uploadStream.write(Buffer.from(value));
1814
+ bytesWritten += value.length;
1815
+ if (onProgress) {
1816
+ onProgress({
1817
+ bytesWritten,
1818
+ totalBytes: 0,
1819
+ // 串流無法預知總大小
1820
+ percentage: 0
1821
+ });
1822
+ }
1823
+ }
1824
+ uploadStream.end();
1825
+ return new Promise((resolve, reject) => {
1826
+ uploadStream.on("finish", () => resolve(uploadStream.id.toString()));
1827
+ uploadStream.on("error", reject);
1828
+ });
1829
+ } catch (error) {
1830
+ uploadStream.abort();
1831
+ throw error;
1832
+ }
1833
+ }
1834
+ /**
1835
+ * 串流下載檔案
1836
+ *
1837
+ * 返回 ReadableStream 以支援大檔案的串流下載
1838
+ *
1839
+ * @param fileId - 檔案 ID
1840
+ * @returns ReadableStream 提供檔案內容
1841
+ *
1842
+ * @example
1843
+ * ```typescript
1844
+ * const stream = grid.downloadStream(fileId)
1845
+ * const reader = stream.getReader()
1846
+ *
1847
+ * while (true) {
1848
+ * const { done, value } = await reader.read()
1849
+ * if (done) break
1850
+ * // 處理 chunk
1851
+ * }
1852
+ * ```
1853
+ */
1854
+ downloadStream(fileId) {
1855
+ return new ReadableStream({
1856
+ start: async (controller) => {
1857
+ try {
1858
+ await this.ensureBucket();
1859
+ const { ObjectId } = await import("mongodb");
1860
+ const downloadStream = this.bucket.openDownloadStream(new ObjectId(fileId));
1861
+ downloadStream.on("data", (chunk) => {
1862
+ controller.enqueue(new Uint8Array(chunk));
1863
+ });
1864
+ downloadStream.on("end", () => {
1865
+ controller.close();
1866
+ });
1867
+ downloadStream.on("error", (error) => {
1868
+ controller.error(error);
1869
+ });
1870
+ } catch (error) {
1871
+ controller.error(error);
1872
+ }
1873
+ },
1874
+ cancel: async () => {
1875
+ }
1876
+ });
1877
+ }
1878
+ /**
1879
+ * 分片上傳大檔案
1880
+ *
1881
+ * 支援進度追蹤的大檔案上傳,自動處理分片
1882
+ *
1883
+ * @param file - Blob 或 File 物件
1884
+ * @param options - 上傳選項
1885
+ * @param onProgress - 可選的進度回調函數
1886
+ * @returns Promise 解析為檔案 ID
1887
+ *
1888
+ * @example
1889
+ * ```typescript
1890
+ * const fileId = await grid.uploadLargeFile(file, {
1891
+ * filename: 'large-video.mp4',
1892
+ * chunkSizeBytes: 255 * 1024 // 255 KB
1893
+ * }, (progress) => {
1894
+ * console.log(`進度: ${progress.percentage}%`)
1895
+ * console.log(`已上傳: ${progress.bytesWritten} / ${progress.totalBytes}`)
1896
+ * })
1897
+ * ```
1898
+ */
1899
+ async uploadLargeFile(file, options, onProgress) {
1900
+ await this.ensureBucket();
1901
+ const chunkSize = options.chunkSizeBytes ?? 255 * 1024;
1902
+ const totalBytes = file.size;
1903
+ let bytesWritten = 0;
1904
+ const uploadStream = this.bucket.openUploadStream(options.filename, {
1905
+ chunkSizeBytes: chunkSize,
1906
+ metadata: { ...options.metadata, totalSize: totalBytes },
1907
+ contentType: options.contentType
1908
+ });
1909
+ const stream = file.stream();
1910
+ const reader = stream.getReader();
1911
+ try {
1912
+ while (true) {
1913
+ const { done, value } = await reader.read();
1914
+ if (done) {
1915
+ break;
1916
+ }
1917
+ uploadStream.write(Buffer.from(value));
1918
+ bytesWritten += value.length;
1919
+ if (onProgress) {
1920
+ onProgress({
1921
+ bytesWritten,
1922
+ totalBytes,
1923
+ percentage: Math.round(bytesWritten / totalBytes * 100)
1924
+ });
1925
+ }
1926
+ }
1927
+ uploadStream.end();
1928
+ return new Promise((resolve, reject) => {
1929
+ uploadStream.on("finish", () => resolve(uploadStream.id.toString()));
1930
+ uploadStream.on("error", reject);
1931
+ });
1932
+ } catch (error) {
1933
+ uploadStream.abort();
1934
+ throw error;
1935
+ }
1936
+ }
1937
+ /**
1938
+ * 取得檔案中繼資料
1939
+ *
1940
+ * 查詢檔案資訊但不下載內容
1941
+ *
1942
+ * @param fileId - 檔案 ID
1943
+ * @returns Promise 解析為檔案中繼資料,找不到時返回 null
1944
+ *
1945
+ * @example
1946
+ * ```typescript
1947
+ * const fileInfo = await grid.findById(fileId)
1948
+ * if (fileInfo) {
1949
+ * console.log(`檔案名稱: ${fileInfo.filename}`)
1950
+ * console.log(`檔案大小: ${fileInfo.length} bytes`)
1951
+ * console.log(`上傳日期: ${fileInfo.uploadDate}`)
1952
+ * }
1953
+ * ```
1954
+ */
1955
+ async findById(fileId) {
1956
+ await this.ensureBucket();
1957
+ const { ObjectId } = await import("mongodb");
1958
+ const cursor = this.bucket.find({ _id: new ObjectId(fileId) });
1959
+ const files = await cursor.toArray();
1960
+ return files.length > 0 ? files[0] : null;
1961
+ }
1555
1962
  async ensureBucket() {
1556
1963
  if (!this.bucket) {
1557
1964
  throw new Error("GridFS bucket not initialized. Please wait a moment after creation.");
@@ -1614,6 +2021,254 @@ var MongoPoolMonitor = class {
1614
2021
  };
1615
2022
  }
1616
2023
  };
2024
+
2025
+ // src/MongoSchemaBuilder.ts
2026
+ var MongoSchemaBuilder = class _MongoSchemaBuilder {
2027
+ schema = {
2028
+ bsonType: "object",
2029
+ required: [],
2030
+ properties: {}
2031
+ };
2032
+ /**
2033
+ * 定義必填欄位
2034
+ *
2035
+ * @param fields - 必填欄位名稱列表
2036
+ * @returns 當前 Schema Builder 實例
2037
+ *
2038
+ * @example
2039
+ * ```typescript
2040
+ * schema().required('name', 'email', 'age')
2041
+ * ```
2042
+ */
2043
+ required(...fields) {
2044
+ this.schema.required = [...this.schema.required, ...fields];
2045
+ return this;
2046
+ }
2047
+ /**
2048
+ * 定義字串欄位
2049
+ *
2050
+ * @param field - 欄位名稱
2051
+ * @param options - 字串選項(長度、模式、枚舉等)
2052
+ * @returns 當前 Schema Builder 實例
2053
+ *
2054
+ * @example
2055
+ * ```typescript
2056
+ * schema()
2057
+ * .string('username', { minLength: 3, maxLength: 50 })
2058
+ * .string('email', { pattern: '^.+@.+$' })
2059
+ * .string('status', { enum: ['active', 'inactive'] })
2060
+ * ```
2061
+ */
2062
+ string(field, options) {
2063
+ const property = { bsonType: "string" };
2064
+ if (options?.minLength !== void 0) {
2065
+ property.minLength = options.minLength;
2066
+ }
2067
+ if (options?.maxLength !== void 0) {
2068
+ property.maxLength = options.maxLength;
2069
+ }
2070
+ if (options?.pattern) {
2071
+ property.pattern = options.pattern;
2072
+ }
2073
+ if (options?.enum) {
2074
+ property.enum = options.enum;
2075
+ }
2076
+ ;
2077
+ this.schema.properties[field] = property;
2078
+ return this;
2079
+ }
2080
+ /**
2081
+ * 定義數字欄位
2082
+ *
2083
+ * @param field - 欄位名稱
2084
+ * @param options - 數字選項(最小值、最大值等)
2085
+ * @returns 當前 Schema Builder 實例
2086
+ *
2087
+ * @example
2088
+ * ```typescript
2089
+ * schema()
2090
+ * .number('price', { minimum: 0, maximum: 10000 })
2091
+ * .number('discount', { minimum: 0, maximum: 1, exclusiveMaximum: true })
2092
+ * ```
2093
+ */
2094
+ number(field, options) {
2095
+ const property = { bsonType: "number" };
2096
+ if (options?.minimum !== void 0) {
2097
+ property.minimum = options.minimum;
2098
+ }
2099
+ if (options?.maximum !== void 0) {
2100
+ property.maximum = options.maximum;
2101
+ }
2102
+ if (options?.exclusiveMinimum) {
2103
+ property.exclusiveMinimum = true;
2104
+ }
2105
+ if (options?.exclusiveMaximum) {
2106
+ property.exclusiveMaximum = true;
2107
+ }
2108
+ ;
2109
+ this.schema.properties[field] = property;
2110
+ return this;
2111
+ }
2112
+ /**
2113
+ * 定義整數欄位
2114
+ *
2115
+ * @param field - 欄位名稱
2116
+ * @param options - 整數選項(最小值、最大值)
2117
+ * @returns 當前 Schema Builder 實例
2118
+ *
2119
+ * @example
2120
+ * ```typescript
2121
+ * schema()
2122
+ * .integer('age', { minimum: 0, maximum: 150 })
2123
+ * .integer('count')
2124
+ * ```
2125
+ */
2126
+ integer(field, options) {
2127
+ const property = { bsonType: "int" };
2128
+ if (options?.minimum !== void 0) {
2129
+ property.minimum = options.minimum;
2130
+ }
2131
+ if (options?.maximum !== void 0) {
2132
+ property.maximum = options.maximum;
2133
+ }
2134
+ ;
2135
+ this.schema.properties[field] = property;
2136
+ return this;
2137
+ }
2138
+ /**
2139
+ * 定義布林欄位
2140
+ *
2141
+ * @param field - 欄位名稱
2142
+ * @returns 當前 Schema Builder 實例
2143
+ *
2144
+ * @example
2145
+ * ```typescript
2146
+ * schema().boolean('isActive').boolean('verified')
2147
+ * ```
2148
+ */
2149
+ boolean(field) {
2150
+ ;
2151
+ this.schema.properties[field] = {
2152
+ bsonType: "bool"
2153
+ };
2154
+ return this;
2155
+ }
2156
+ /**
2157
+ * 定義日期欄位
2158
+ *
2159
+ * @param field - 欄位名稱
2160
+ * @returns 當前 Schema Builder 實例
2161
+ *
2162
+ * @example
2163
+ * ```typescript
2164
+ * schema().date('createdAt').date('updatedAt')
2165
+ * ```
2166
+ */
2167
+ date(field) {
2168
+ ;
2169
+ this.schema.properties[field] = {
2170
+ bsonType: "date"
2171
+ };
2172
+ return this;
2173
+ }
2174
+ /**
2175
+ * 定義陣列欄位
2176
+ *
2177
+ * @param field - 欄位名稱
2178
+ * @param itemType - 陣列元素類型
2179
+ * @param options - 陣列選項(長度、唯一性等)
2180
+ * @returns 當前 Schema Builder 實例
2181
+ *
2182
+ * @example
2183
+ * ```typescript
2184
+ * schema()
2185
+ * .array('tags', 'string')
2186
+ * .array('scores', 'number')
2187
+ * .array('roles', 'string', { minItems: 1, uniqueItems: true })
2188
+ * ```
2189
+ */
2190
+ array(field, itemType, options) {
2191
+ const property = {
2192
+ bsonType: "array",
2193
+ items: { bsonType: itemType }
2194
+ };
2195
+ if (options?.minItems !== void 0) {
2196
+ property.minItems = options.minItems;
2197
+ }
2198
+ if (options?.maxItems !== void 0) {
2199
+ property.maxItems = options.maxItems;
2200
+ }
2201
+ if (options?.uniqueItems) {
2202
+ property.uniqueItems = true;
2203
+ }
2204
+ ;
2205
+ this.schema.properties[field] = property;
2206
+ return this;
2207
+ }
2208
+ /**
2209
+ * 定義物件欄位
2210
+ *
2211
+ * @param field - 欄位名稱
2212
+ * @param callback - 巢狀 Schema 建構函數
2213
+ * @returns 當前 Schema Builder 實例
2214
+ *
2215
+ * @example
2216
+ * ```typescript
2217
+ * schema().object('profile', (s) =>
2218
+ * s
2219
+ * .string('bio', { maxLength: 500 })
2220
+ * .string('avatar')
2221
+ * .integer('followers')
2222
+ * )
2223
+ * ```
2224
+ */
2225
+ object(field, callback) {
2226
+ const nestedBuilder = new _MongoSchemaBuilder();
2227
+ callback(nestedBuilder);
2228
+ this.schema.properties[field] = nestedBuilder.build();
2229
+ return this;
2230
+ }
2231
+ /**
2232
+ * 建構最終的 JSON Schema
2233
+ *
2234
+ * @returns JSON Schema 物件
2235
+ *
2236
+ * @example
2237
+ * ```typescript
2238
+ * const schema = schema()
2239
+ * .required('name')
2240
+ * .string('name')
2241
+ * .build()
2242
+ * ```
2243
+ */
2244
+ build() {
2245
+ return this.schema;
2246
+ }
2247
+ /**
2248
+ * 轉換為 MongoDB 驗證選項
2249
+ *
2250
+ * @param options - 驗證選項(驗證等級、動作)
2251
+ * @returns MongoDB Schema 驗證選項
2252
+ *
2253
+ * @example
2254
+ * ```typescript
2255
+ * const options = schema()
2256
+ * .required('name')
2257
+ * .string('name')
2258
+ * .toValidationOptions({ validationLevel: 'moderate' })
2259
+ * ```
2260
+ */
2261
+ toValidationOptions(options) {
2262
+ return {
2263
+ validator: { $jsonSchema: this.schema },
2264
+ validationLevel: options?.validationLevel ?? "strict",
2265
+ validationAction: options?.validationAction ?? "error"
2266
+ };
2267
+ }
2268
+ };
2269
+ function schema() {
2270
+ return new MongoSchemaBuilder();
2271
+ }
1617
2272
  export {
1618
2273
  Mongo,
1619
2274
  MongoAggregateBuilder,
@@ -1621,5 +2276,7 @@ export {
1621
2276
  MongoGridFS,
1622
2277
  MongoManager,
1623
2278
  MongoPoolMonitor,
1624
- MongoQueryBuilder
2279
+ MongoQueryBuilder,
2280
+ MongoSchemaBuilder,
2281
+ schema
1625
2282
  };