@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/README.md +235 -15
- package/dist/index.cjs +678 -20
- package/dist/index.d.cts +463 -2
- package/dist/index.d.ts +463 -2
- package/dist/index.js +675 -18
- package/package.json +16 -4
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -36,7 +35,9 @@ __export(index_exports, {
|
|
|
36
35
|
MongoGridFS: () => MongoGridFS,
|
|
37
36
|
MongoManager: () => MongoManager,
|
|
38
37
|
MongoPoolMonitor: () => MongoPoolMonitor,
|
|
39
|
-
MongoQueryBuilder: () => MongoQueryBuilder
|
|
38
|
+
MongoQueryBuilder: () => MongoQueryBuilder,
|
|
39
|
+
MongoSchemaBuilder: () => MongoSchemaBuilder,
|
|
40
|
+
schema: () => schema
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(index_exports);
|
|
42
43
|
|
|
@@ -54,6 +55,7 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
|
|
|
54
55
|
sortSpec = {};
|
|
55
56
|
limitCount;
|
|
56
57
|
skipCount;
|
|
58
|
+
softDeleteMode = "exclude";
|
|
57
59
|
// ============================================================================
|
|
58
60
|
// WHERE Clauses
|
|
59
61
|
// ============================================================================
|
|
@@ -649,6 +651,163 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
|
|
|
649
651
|
acknowledged: result.acknowledged
|
|
650
652
|
};
|
|
651
653
|
}
|
|
654
|
+
// ============================================================================
|
|
655
|
+
// Soft Delete Methods
|
|
656
|
+
// ============================================================================
|
|
657
|
+
/**
|
|
658
|
+
* 包含已軟刪除的記錄
|
|
659
|
+
*
|
|
660
|
+
* 查詢時包含所有記錄,不過濾已刪除的文檔
|
|
661
|
+
*
|
|
662
|
+
* @returns 當前查詢建構器實例,支援鏈式調用
|
|
663
|
+
*
|
|
664
|
+
* @example
|
|
665
|
+
* ```typescript
|
|
666
|
+
* const allUsers = await query.withTrashed().get();
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
withTrashed() {
|
|
670
|
+
this.softDeleteMode = "include";
|
|
671
|
+
return this;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* 只查詢已軟刪除的記錄
|
|
675
|
+
*
|
|
676
|
+
* 只返回 deletedAt 不為 null 的文檔
|
|
677
|
+
*
|
|
678
|
+
* @returns 當前查詢建構器實例,支援鏈式調用
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* ```typescript
|
|
682
|
+
* const trashedUsers = await query.onlyTrashed().get();
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
onlyTrashed() {
|
|
686
|
+
this.softDeleteMode = "only";
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* 軟刪除單一記錄
|
|
691
|
+
*
|
|
692
|
+
* 設置 deletedAt 為當前時間,而非真正刪除記錄
|
|
693
|
+
*
|
|
694
|
+
* @returns Promise 解析為更新結果
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```typescript
|
|
698
|
+
* await query.where('_id', userId).softDelete();
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
async softDelete() {
|
|
702
|
+
const updateDoc = { $set: { deletedAt: /* @__PURE__ */ new Date() } };
|
|
703
|
+
const result = await this.nativeCollection.updateOne(this.toFilter(), updateDoc, {
|
|
704
|
+
session: this.session
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
matchedCount: result.matchedCount,
|
|
708
|
+
modifiedCount: result.modifiedCount,
|
|
709
|
+
acknowledged: result.acknowledged
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* 批次軟刪除
|
|
714
|
+
*
|
|
715
|
+
* 設置所有符合條件的文檔的 deletedAt 為當前時間
|
|
716
|
+
*
|
|
717
|
+
* @returns Promise 解析為更新結果
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```typescript
|
|
721
|
+
* await query.where('status', 'inactive').softDeleteMany();
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
async softDeleteMany() {
|
|
725
|
+
const updateDoc = { $set: { deletedAt: /* @__PURE__ */ new Date() } };
|
|
726
|
+
const result = await this.nativeCollection.updateMany(this.toFilter(), updateDoc, {
|
|
727
|
+
session: this.session
|
|
728
|
+
});
|
|
729
|
+
return {
|
|
730
|
+
matchedCount: result.matchedCount,
|
|
731
|
+
modifiedCount: result.modifiedCount,
|
|
732
|
+
acknowledged: result.acknowledged
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* 恢復軟刪除的記錄
|
|
737
|
+
*
|
|
738
|
+
* 將 deletedAt 設置為 null,恢復軟刪除的文檔
|
|
739
|
+
*
|
|
740
|
+
* @returns Promise 解析為更新結果
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```typescript
|
|
744
|
+
* await query.where('_id', userId).restore();
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
async restore() {
|
|
748
|
+
const updateDoc = { $set: { deletedAt: null } };
|
|
749
|
+
const result = await this.nativeCollection.updateOne(this.toFilter(), updateDoc, {
|
|
750
|
+
session: this.session
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
matchedCount: result.matchedCount,
|
|
754
|
+
modifiedCount: result.modifiedCount,
|
|
755
|
+
acknowledged: result.acknowledged
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* 批次恢復軟刪除的記錄
|
|
760
|
+
*
|
|
761
|
+
* 將所有符合條件的文檔的 deletedAt 設置為 null
|
|
762
|
+
*
|
|
763
|
+
* @returns Promise 解析為更新結果
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```typescript
|
|
767
|
+
* await query.onlyTrashed().restoreMany();
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
async restoreMany() {
|
|
771
|
+
const updateDoc = { $set: { deletedAt: null } };
|
|
772
|
+
const result = await this.nativeCollection.updateMany(this.toFilter(), updateDoc, {
|
|
773
|
+
session: this.session
|
|
774
|
+
});
|
|
775
|
+
return {
|
|
776
|
+
matchedCount: result.matchedCount,
|
|
777
|
+
modifiedCount: result.modifiedCount,
|
|
778
|
+
acknowledged: result.acknowledged
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* 強制刪除(真正刪除記錄)
|
|
783
|
+
*
|
|
784
|
+
* 永久刪除單一文檔,無法恢復
|
|
785
|
+
*
|
|
786
|
+
* @returns Promise 解析為刪除結果
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* ```typescript
|
|
790
|
+
* await query.where('_id', userId).forceDelete();
|
|
791
|
+
* ```
|
|
792
|
+
*/
|
|
793
|
+
async forceDelete() {
|
|
794
|
+
return await this.delete();
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* 批次強制刪除
|
|
798
|
+
*
|
|
799
|
+
* 永久刪除所有符合條件的文檔,無法恢復
|
|
800
|
+
*
|
|
801
|
+
* @returns Promise 解析為刪除結果
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```typescript
|
|
805
|
+
* await query.where('createdAt', '<', oneYearAgo).forceDeleteMany();
|
|
806
|
+
* ```
|
|
807
|
+
*/
|
|
808
|
+
async forceDeleteMany() {
|
|
809
|
+
return await this.deleteMany();
|
|
810
|
+
}
|
|
652
811
|
/**
|
|
653
812
|
* Executes a bulk write operation.
|
|
654
813
|
*
|
|
@@ -757,17 +916,15 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
|
|
|
757
916
|
toFilter() {
|
|
758
917
|
const hasMainFilters = Object.keys(this.filters).length > 0;
|
|
759
918
|
const hasOrFilters = this.orFilters.length > 0;
|
|
919
|
+
let baseFilter;
|
|
760
920
|
if (!hasOrFilters) {
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
};
|
|
921
|
+
baseFilter = { ...this.filters };
|
|
922
|
+
} else if (!hasMainFilters) {
|
|
923
|
+
baseFilter = { $or: this.orFilters };
|
|
924
|
+
} else {
|
|
925
|
+
baseFilter = { $or: [this.filters, ...this.orFilters] };
|
|
767
926
|
}
|
|
768
|
-
return
|
|
769
|
-
$or: [this.filters, ...this.orFilters]
|
|
770
|
-
};
|
|
927
|
+
return this.applySoftDeleteFilter(baseFilter);
|
|
771
928
|
}
|
|
772
929
|
/**
|
|
773
930
|
* Creates a deep copy of the query builder.
|
|
@@ -795,6 +952,7 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
|
|
|
795
952
|
cloned.sortSpec = { ...this.sortSpec };
|
|
796
953
|
cloned.limitCount = this.limitCount;
|
|
797
954
|
cloned.skipCount = this.skipCount;
|
|
955
|
+
cloned.softDeleteMode = this.softDeleteMode;
|
|
798
956
|
return cloned;
|
|
799
957
|
}
|
|
800
958
|
// ============================================================================
|
|
@@ -833,6 +991,34 @@ var MongoQueryBuilder = class _MongoQueryBuilder {
|
|
|
833
991
|
}
|
|
834
992
|
return { $set: update };
|
|
835
993
|
}
|
|
994
|
+
/**
|
|
995
|
+
* 應用軟刪除過濾
|
|
996
|
+
*
|
|
997
|
+
* 根據 softDeleteMode 自動過濾已刪除的記錄
|
|
998
|
+
*
|
|
999
|
+
* @param filter - 基礎過濾條件
|
|
1000
|
+
* @returns 包含軟刪除過濾的完整過濾條件
|
|
1001
|
+
*/
|
|
1002
|
+
applySoftDeleteFilter(filter) {
|
|
1003
|
+
if (this.softDeleteMode === "include") {
|
|
1004
|
+
return filter;
|
|
1005
|
+
}
|
|
1006
|
+
if (this.softDeleteMode === "only") {
|
|
1007
|
+
return {
|
|
1008
|
+
...filter,
|
|
1009
|
+
deletedAt: { $ne: null }
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const softDeleteFilter = {
|
|
1013
|
+
$or: [{ deletedAt: null }, { deletedAt: { $exists: false } }]
|
|
1014
|
+
};
|
|
1015
|
+
if (Object.keys(filter).length === 0) {
|
|
1016
|
+
return softDeleteFilter;
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
$and: [filter, softDeleteFilter]
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
836
1022
|
async getObjectId() {
|
|
837
1023
|
if (_MongoQueryBuilder.ObjectIdCtor) {
|
|
838
1024
|
return _MongoQueryBuilder.ObjectIdCtor;
|
|
@@ -959,17 +1145,27 @@ var MongoClient = class {
|
|
|
959
1145
|
if (this.config.socketTimeoutMS) {
|
|
960
1146
|
options.socketTimeoutMS = this.config.socketTimeoutMS;
|
|
961
1147
|
}
|
|
962
|
-
this.client = new this.mongodb.MongoClient(uri, options);
|
|
963
1148
|
let lastError = null;
|
|
964
1149
|
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
1150
|
+
const client = new this.mongodb.MongoClient(uri, options);
|
|
1151
|
+
this.client = client;
|
|
965
1152
|
try {
|
|
966
|
-
await
|
|
1153
|
+
await client.connect();
|
|
967
1154
|
const dbName = this.config.database ?? "test";
|
|
968
|
-
this.db =
|
|
1155
|
+
this.db = client.db(dbName);
|
|
969
1156
|
this.connected = true;
|
|
970
1157
|
return;
|
|
971
1158
|
} catch (error) {
|
|
972
1159
|
lastError = error;
|
|
1160
|
+
this.connected = false;
|
|
1161
|
+
this.db = null;
|
|
1162
|
+
try {
|
|
1163
|
+
await client.close();
|
|
1164
|
+
} catch {
|
|
1165
|
+
}
|
|
1166
|
+
if (this.client === client) {
|
|
1167
|
+
this.client = null;
|
|
1168
|
+
}
|
|
973
1169
|
if (attempt < config.maxRetries) {
|
|
974
1170
|
const delay = config.retryDelayMs * config.backoffMultiplier ** attempt;
|
|
975
1171
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
@@ -1210,12 +1406,38 @@ var MongoDatabaseWrapper = class {
|
|
|
1210
1406
|
}
|
|
1211
1407
|
await this.db.createCollection(name, createOptions);
|
|
1212
1408
|
}
|
|
1213
|
-
async setValidation(collectionName,
|
|
1409
|
+
async setValidation(collectionName, schema2) {
|
|
1214
1410
|
await this.db.command({
|
|
1215
1411
|
collMod: collectionName,
|
|
1216
|
-
validator:
|
|
1217
|
-
validationLevel:
|
|
1218
|
-
validationAction:
|
|
1412
|
+
validator: schema2.validator,
|
|
1413
|
+
validationLevel: schema2.validationLevel ?? "strict",
|
|
1414
|
+
validationAction: schema2.validationAction ?? "error"
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* 使用 Schema Builder 建立 Collection
|
|
1419
|
+
*
|
|
1420
|
+
* 提供友善的 API 來建立帶有 Schema 驗證的 Collection
|
|
1421
|
+
*
|
|
1422
|
+
* @param name - Collection 名稱
|
|
1423
|
+
* @param schemaBuilder - Schema Builder 實例
|
|
1424
|
+
* @param options - 驗證選項(驗證等級、動作)
|
|
1425
|
+
*
|
|
1426
|
+
* @example
|
|
1427
|
+
* ```typescript
|
|
1428
|
+
* import { schema } from '@gravito/dark-matter'
|
|
1429
|
+
*
|
|
1430
|
+
* const userSchema = schema()
|
|
1431
|
+
* .required('name', 'email')
|
|
1432
|
+
* .string('name')
|
|
1433
|
+
* .string('email')
|
|
1434
|
+
*
|
|
1435
|
+
* await Mongo.database().createCollectionWithSchema('users', userSchema)
|
|
1436
|
+
* ```
|
|
1437
|
+
*/
|
|
1438
|
+
async createCollectionWithSchema(name, schemaBuilder, options) {
|
|
1439
|
+
await this.createCollection(name, {
|
|
1440
|
+
schema: schemaBuilder.toValidationOptions(options)
|
|
1219
1441
|
});
|
|
1220
1442
|
}
|
|
1221
1443
|
};
|
|
@@ -1525,7 +1747,9 @@ var MongoGridFS = class {
|
|
|
1525
1747
|
const reader = source.getReader();
|
|
1526
1748
|
while (true) {
|
|
1527
1749
|
const { done, value } = await reader.read();
|
|
1528
|
-
if (done)
|
|
1750
|
+
if (done) {
|
|
1751
|
+
break;
|
|
1752
|
+
}
|
|
1529
1753
|
uploadStream.write(value);
|
|
1530
1754
|
}
|
|
1531
1755
|
uploadStream.end();
|
|
@@ -1594,6 +1818,190 @@ var MongoGridFS = class {
|
|
|
1594
1818
|
const cursor = this.bucket.find(filter ?? {});
|
|
1595
1819
|
return await cursor.toArray();
|
|
1596
1820
|
}
|
|
1821
|
+
/**
|
|
1822
|
+
* 串流上傳檔案
|
|
1823
|
+
*
|
|
1824
|
+
* 支援進度回調的串流上傳,適合大檔案上傳
|
|
1825
|
+
*
|
|
1826
|
+
* @param stream - ReadableStream 來源
|
|
1827
|
+
* @param options - 上傳選項
|
|
1828
|
+
* @param onProgress - 可選的進度回調函數
|
|
1829
|
+
* @returns Promise 解析為檔案 ID
|
|
1830
|
+
*
|
|
1831
|
+
* @example
|
|
1832
|
+
* ```typescript
|
|
1833
|
+
* const stream = file.stream()
|
|
1834
|
+
* const fileId = await grid.uploadStream(stream, {
|
|
1835
|
+
* filename: 'video.mp4'
|
|
1836
|
+
* }, (progress) => {
|
|
1837
|
+
* console.log(`上傳進度: ${progress.percentage}%`)
|
|
1838
|
+
* })
|
|
1839
|
+
* ```
|
|
1840
|
+
*/
|
|
1841
|
+
async uploadStream(stream, options, onProgress) {
|
|
1842
|
+
await this.ensureBucket();
|
|
1843
|
+
const uploadStream = this.bucket.openUploadStream(options.filename, {
|
|
1844
|
+
chunkSizeBytes: options.chunkSizeBytes,
|
|
1845
|
+
metadata: options.metadata,
|
|
1846
|
+
contentType: options.contentType
|
|
1847
|
+
});
|
|
1848
|
+
let bytesWritten = 0;
|
|
1849
|
+
const reader = stream.getReader();
|
|
1850
|
+
try {
|
|
1851
|
+
while (true) {
|
|
1852
|
+
const { done, value } = await reader.read();
|
|
1853
|
+
if (done) {
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
uploadStream.write(Buffer.from(value));
|
|
1857
|
+
bytesWritten += value.length;
|
|
1858
|
+
if (onProgress) {
|
|
1859
|
+
onProgress({
|
|
1860
|
+
bytesWritten,
|
|
1861
|
+
totalBytes: 0,
|
|
1862
|
+
// 串流無法預知總大小
|
|
1863
|
+
percentage: 0
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
uploadStream.end();
|
|
1868
|
+
return new Promise((resolve, reject) => {
|
|
1869
|
+
uploadStream.on("finish", () => resolve(uploadStream.id.toString()));
|
|
1870
|
+
uploadStream.on("error", reject);
|
|
1871
|
+
});
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
uploadStream.abort();
|
|
1874
|
+
throw error;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* 串流下載檔案
|
|
1879
|
+
*
|
|
1880
|
+
* 返回 ReadableStream 以支援大檔案的串流下載
|
|
1881
|
+
*
|
|
1882
|
+
* @param fileId - 檔案 ID
|
|
1883
|
+
* @returns ReadableStream 提供檔案內容
|
|
1884
|
+
*
|
|
1885
|
+
* @example
|
|
1886
|
+
* ```typescript
|
|
1887
|
+
* const stream = grid.downloadStream(fileId)
|
|
1888
|
+
* const reader = stream.getReader()
|
|
1889
|
+
*
|
|
1890
|
+
* while (true) {
|
|
1891
|
+
* const { done, value } = await reader.read()
|
|
1892
|
+
* if (done) break
|
|
1893
|
+
* // 處理 chunk
|
|
1894
|
+
* }
|
|
1895
|
+
* ```
|
|
1896
|
+
*/
|
|
1897
|
+
downloadStream(fileId) {
|
|
1898
|
+
return new ReadableStream({
|
|
1899
|
+
start: async (controller) => {
|
|
1900
|
+
try {
|
|
1901
|
+
await this.ensureBucket();
|
|
1902
|
+
const { ObjectId } = await import("mongodb");
|
|
1903
|
+
const downloadStream = this.bucket.openDownloadStream(new ObjectId(fileId));
|
|
1904
|
+
downloadStream.on("data", (chunk) => {
|
|
1905
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
1906
|
+
});
|
|
1907
|
+
downloadStream.on("end", () => {
|
|
1908
|
+
controller.close();
|
|
1909
|
+
});
|
|
1910
|
+
downloadStream.on("error", (error) => {
|
|
1911
|
+
controller.error(error);
|
|
1912
|
+
});
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
controller.error(error);
|
|
1915
|
+
}
|
|
1916
|
+
},
|
|
1917
|
+
cancel: async () => {
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* 分片上傳大檔案
|
|
1923
|
+
*
|
|
1924
|
+
* 支援進度追蹤的大檔案上傳,自動處理分片
|
|
1925
|
+
*
|
|
1926
|
+
* @param file - Blob 或 File 物件
|
|
1927
|
+
* @param options - 上傳選項
|
|
1928
|
+
* @param onProgress - 可選的進度回調函數
|
|
1929
|
+
* @returns Promise 解析為檔案 ID
|
|
1930
|
+
*
|
|
1931
|
+
* @example
|
|
1932
|
+
* ```typescript
|
|
1933
|
+
* const fileId = await grid.uploadLargeFile(file, {
|
|
1934
|
+
* filename: 'large-video.mp4',
|
|
1935
|
+
* chunkSizeBytes: 255 * 1024 // 255 KB
|
|
1936
|
+
* }, (progress) => {
|
|
1937
|
+
* console.log(`進度: ${progress.percentage}%`)
|
|
1938
|
+
* console.log(`已上傳: ${progress.bytesWritten} / ${progress.totalBytes}`)
|
|
1939
|
+
* })
|
|
1940
|
+
* ```
|
|
1941
|
+
*/
|
|
1942
|
+
async uploadLargeFile(file, options, onProgress) {
|
|
1943
|
+
await this.ensureBucket();
|
|
1944
|
+
const chunkSize = options.chunkSizeBytes ?? 255 * 1024;
|
|
1945
|
+
const totalBytes = file.size;
|
|
1946
|
+
let bytesWritten = 0;
|
|
1947
|
+
const uploadStream = this.bucket.openUploadStream(options.filename, {
|
|
1948
|
+
chunkSizeBytes: chunkSize,
|
|
1949
|
+
metadata: { ...options.metadata, totalSize: totalBytes },
|
|
1950
|
+
contentType: options.contentType
|
|
1951
|
+
});
|
|
1952
|
+
const stream = file.stream();
|
|
1953
|
+
const reader = stream.getReader();
|
|
1954
|
+
try {
|
|
1955
|
+
while (true) {
|
|
1956
|
+
const { done, value } = await reader.read();
|
|
1957
|
+
if (done) {
|
|
1958
|
+
break;
|
|
1959
|
+
}
|
|
1960
|
+
uploadStream.write(Buffer.from(value));
|
|
1961
|
+
bytesWritten += value.length;
|
|
1962
|
+
if (onProgress) {
|
|
1963
|
+
onProgress({
|
|
1964
|
+
bytesWritten,
|
|
1965
|
+
totalBytes,
|
|
1966
|
+
percentage: Math.round(bytesWritten / totalBytes * 100)
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
uploadStream.end();
|
|
1971
|
+
return new Promise((resolve, reject) => {
|
|
1972
|
+
uploadStream.on("finish", () => resolve(uploadStream.id.toString()));
|
|
1973
|
+
uploadStream.on("error", reject);
|
|
1974
|
+
});
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
uploadStream.abort();
|
|
1977
|
+
throw error;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* 取得檔案中繼資料
|
|
1982
|
+
*
|
|
1983
|
+
* 查詢檔案資訊但不下載內容
|
|
1984
|
+
*
|
|
1985
|
+
* @param fileId - 檔案 ID
|
|
1986
|
+
* @returns Promise 解析為檔案中繼資料,找不到時返回 null
|
|
1987
|
+
*
|
|
1988
|
+
* @example
|
|
1989
|
+
* ```typescript
|
|
1990
|
+
* const fileInfo = await grid.findById(fileId)
|
|
1991
|
+
* if (fileInfo) {
|
|
1992
|
+
* console.log(`檔案名稱: ${fileInfo.filename}`)
|
|
1993
|
+
* console.log(`檔案大小: ${fileInfo.length} bytes`)
|
|
1994
|
+
* console.log(`上傳日期: ${fileInfo.uploadDate}`)
|
|
1995
|
+
* }
|
|
1996
|
+
* ```
|
|
1997
|
+
*/
|
|
1998
|
+
async findById(fileId) {
|
|
1999
|
+
await this.ensureBucket();
|
|
2000
|
+
const { ObjectId } = await import("mongodb");
|
|
2001
|
+
const cursor = this.bucket.find({ _id: new ObjectId(fileId) });
|
|
2002
|
+
const files = await cursor.toArray();
|
|
2003
|
+
return files.length > 0 ? files[0] : null;
|
|
2004
|
+
}
|
|
1597
2005
|
async ensureBucket() {
|
|
1598
2006
|
if (!this.bucket) {
|
|
1599
2007
|
throw new Error("GridFS bucket not initialized. Please wait a moment after creation.");
|
|
@@ -1656,6 +2064,254 @@ var MongoPoolMonitor = class {
|
|
|
1656
2064
|
};
|
|
1657
2065
|
}
|
|
1658
2066
|
};
|
|
2067
|
+
|
|
2068
|
+
// src/MongoSchemaBuilder.ts
|
|
2069
|
+
var MongoSchemaBuilder = class _MongoSchemaBuilder {
|
|
2070
|
+
schema = {
|
|
2071
|
+
bsonType: "object",
|
|
2072
|
+
required: [],
|
|
2073
|
+
properties: {}
|
|
2074
|
+
};
|
|
2075
|
+
/**
|
|
2076
|
+
* 定義必填欄位
|
|
2077
|
+
*
|
|
2078
|
+
* @param fields - 必填欄位名稱列表
|
|
2079
|
+
* @returns 當前 Schema Builder 實例
|
|
2080
|
+
*
|
|
2081
|
+
* @example
|
|
2082
|
+
* ```typescript
|
|
2083
|
+
* schema().required('name', 'email', 'age')
|
|
2084
|
+
* ```
|
|
2085
|
+
*/
|
|
2086
|
+
required(...fields) {
|
|
2087
|
+
this.schema.required = [...this.schema.required, ...fields];
|
|
2088
|
+
return this;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* 定義字串欄位
|
|
2092
|
+
*
|
|
2093
|
+
* @param field - 欄位名稱
|
|
2094
|
+
* @param options - 字串選項(長度、模式、枚舉等)
|
|
2095
|
+
* @returns 當前 Schema Builder 實例
|
|
2096
|
+
*
|
|
2097
|
+
* @example
|
|
2098
|
+
* ```typescript
|
|
2099
|
+
* schema()
|
|
2100
|
+
* .string('username', { minLength: 3, maxLength: 50 })
|
|
2101
|
+
* .string('email', { pattern: '^.+@.+$' })
|
|
2102
|
+
* .string('status', { enum: ['active', 'inactive'] })
|
|
2103
|
+
* ```
|
|
2104
|
+
*/
|
|
2105
|
+
string(field, options) {
|
|
2106
|
+
const property = { bsonType: "string" };
|
|
2107
|
+
if (options?.minLength !== void 0) {
|
|
2108
|
+
property.minLength = options.minLength;
|
|
2109
|
+
}
|
|
2110
|
+
if (options?.maxLength !== void 0) {
|
|
2111
|
+
property.maxLength = options.maxLength;
|
|
2112
|
+
}
|
|
2113
|
+
if (options?.pattern) {
|
|
2114
|
+
property.pattern = options.pattern;
|
|
2115
|
+
}
|
|
2116
|
+
if (options?.enum) {
|
|
2117
|
+
property.enum = options.enum;
|
|
2118
|
+
}
|
|
2119
|
+
;
|
|
2120
|
+
this.schema.properties[field] = property;
|
|
2121
|
+
return this;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* 定義數字欄位
|
|
2125
|
+
*
|
|
2126
|
+
* @param field - 欄位名稱
|
|
2127
|
+
* @param options - 數字選項(最小值、最大值等)
|
|
2128
|
+
* @returns 當前 Schema Builder 實例
|
|
2129
|
+
*
|
|
2130
|
+
* @example
|
|
2131
|
+
* ```typescript
|
|
2132
|
+
* schema()
|
|
2133
|
+
* .number('price', { minimum: 0, maximum: 10000 })
|
|
2134
|
+
* .number('discount', { minimum: 0, maximum: 1, exclusiveMaximum: true })
|
|
2135
|
+
* ```
|
|
2136
|
+
*/
|
|
2137
|
+
number(field, options) {
|
|
2138
|
+
const property = { bsonType: "number" };
|
|
2139
|
+
if (options?.minimum !== void 0) {
|
|
2140
|
+
property.minimum = options.minimum;
|
|
2141
|
+
}
|
|
2142
|
+
if (options?.maximum !== void 0) {
|
|
2143
|
+
property.maximum = options.maximum;
|
|
2144
|
+
}
|
|
2145
|
+
if (options?.exclusiveMinimum) {
|
|
2146
|
+
property.exclusiveMinimum = true;
|
|
2147
|
+
}
|
|
2148
|
+
if (options?.exclusiveMaximum) {
|
|
2149
|
+
property.exclusiveMaximum = true;
|
|
2150
|
+
}
|
|
2151
|
+
;
|
|
2152
|
+
this.schema.properties[field] = property;
|
|
2153
|
+
return this;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* 定義整數欄位
|
|
2157
|
+
*
|
|
2158
|
+
* @param field - 欄位名稱
|
|
2159
|
+
* @param options - 整數選項(最小值、最大值)
|
|
2160
|
+
* @returns 當前 Schema Builder 實例
|
|
2161
|
+
*
|
|
2162
|
+
* @example
|
|
2163
|
+
* ```typescript
|
|
2164
|
+
* schema()
|
|
2165
|
+
* .integer('age', { minimum: 0, maximum: 150 })
|
|
2166
|
+
* .integer('count')
|
|
2167
|
+
* ```
|
|
2168
|
+
*/
|
|
2169
|
+
integer(field, options) {
|
|
2170
|
+
const property = { bsonType: "int" };
|
|
2171
|
+
if (options?.minimum !== void 0) {
|
|
2172
|
+
property.minimum = options.minimum;
|
|
2173
|
+
}
|
|
2174
|
+
if (options?.maximum !== void 0) {
|
|
2175
|
+
property.maximum = options.maximum;
|
|
2176
|
+
}
|
|
2177
|
+
;
|
|
2178
|
+
this.schema.properties[field] = property;
|
|
2179
|
+
return this;
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* 定義布林欄位
|
|
2183
|
+
*
|
|
2184
|
+
* @param field - 欄位名稱
|
|
2185
|
+
* @returns 當前 Schema Builder 實例
|
|
2186
|
+
*
|
|
2187
|
+
* @example
|
|
2188
|
+
* ```typescript
|
|
2189
|
+
* schema().boolean('isActive').boolean('verified')
|
|
2190
|
+
* ```
|
|
2191
|
+
*/
|
|
2192
|
+
boolean(field) {
|
|
2193
|
+
;
|
|
2194
|
+
this.schema.properties[field] = {
|
|
2195
|
+
bsonType: "bool"
|
|
2196
|
+
};
|
|
2197
|
+
return this;
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* 定義日期欄位
|
|
2201
|
+
*
|
|
2202
|
+
* @param field - 欄位名稱
|
|
2203
|
+
* @returns 當前 Schema Builder 實例
|
|
2204
|
+
*
|
|
2205
|
+
* @example
|
|
2206
|
+
* ```typescript
|
|
2207
|
+
* schema().date('createdAt').date('updatedAt')
|
|
2208
|
+
* ```
|
|
2209
|
+
*/
|
|
2210
|
+
date(field) {
|
|
2211
|
+
;
|
|
2212
|
+
this.schema.properties[field] = {
|
|
2213
|
+
bsonType: "date"
|
|
2214
|
+
};
|
|
2215
|
+
return this;
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* 定義陣列欄位
|
|
2219
|
+
*
|
|
2220
|
+
* @param field - 欄位名稱
|
|
2221
|
+
* @param itemType - 陣列元素類型
|
|
2222
|
+
* @param options - 陣列選項(長度、唯一性等)
|
|
2223
|
+
* @returns 當前 Schema Builder 實例
|
|
2224
|
+
*
|
|
2225
|
+
* @example
|
|
2226
|
+
* ```typescript
|
|
2227
|
+
* schema()
|
|
2228
|
+
* .array('tags', 'string')
|
|
2229
|
+
* .array('scores', 'number')
|
|
2230
|
+
* .array('roles', 'string', { minItems: 1, uniqueItems: true })
|
|
2231
|
+
* ```
|
|
2232
|
+
*/
|
|
2233
|
+
array(field, itemType, options) {
|
|
2234
|
+
const property = {
|
|
2235
|
+
bsonType: "array",
|
|
2236
|
+
items: { bsonType: itemType }
|
|
2237
|
+
};
|
|
2238
|
+
if (options?.minItems !== void 0) {
|
|
2239
|
+
property.minItems = options.minItems;
|
|
2240
|
+
}
|
|
2241
|
+
if (options?.maxItems !== void 0) {
|
|
2242
|
+
property.maxItems = options.maxItems;
|
|
2243
|
+
}
|
|
2244
|
+
if (options?.uniqueItems) {
|
|
2245
|
+
property.uniqueItems = true;
|
|
2246
|
+
}
|
|
2247
|
+
;
|
|
2248
|
+
this.schema.properties[field] = property;
|
|
2249
|
+
return this;
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* 定義物件欄位
|
|
2253
|
+
*
|
|
2254
|
+
* @param field - 欄位名稱
|
|
2255
|
+
* @param callback - 巢狀 Schema 建構函數
|
|
2256
|
+
* @returns 當前 Schema Builder 實例
|
|
2257
|
+
*
|
|
2258
|
+
* @example
|
|
2259
|
+
* ```typescript
|
|
2260
|
+
* schema().object('profile', (s) =>
|
|
2261
|
+
* s
|
|
2262
|
+
* .string('bio', { maxLength: 500 })
|
|
2263
|
+
* .string('avatar')
|
|
2264
|
+
* .integer('followers')
|
|
2265
|
+
* )
|
|
2266
|
+
* ```
|
|
2267
|
+
*/
|
|
2268
|
+
object(field, callback) {
|
|
2269
|
+
const nestedBuilder = new _MongoSchemaBuilder();
|
|
2270
|
+
callback(nestedBuilder);
|
|
2271
|
+
this.schema.properties[field] = nestedBuilder.build();
|
|
2272
|
+
return this;
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* 建構最終的 JSON Schema
|
|
2276
|
+
*
|
|
2277
|
+
* @returns JSON Schema 物件
|
|
2278
|
+
*
|
|
2279
|
+
* @example
|
|
2280
|
+
* ```typescript
|
|
2281
|
+
* const schema = schema()
|
|
2282
|
+
* .required('name')
|
|
2283
|
+
* .string('name')
|
|
2284
|
+
* .build()
|
|
2285
|
+
* ```
|
|
2286
|
+
*/
|
|
2287
|
+
build() {
|
|
2288
|
+
return this.schema;
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* 轉換為 MongoDB 驗證選項
|
|
2292
|
+
*
|
|
2293
|
+
* @param options - 驗證選項(驗證等級、動作)
|
|
2294
|
+
* @returns MongoDB Schema 驗證選項
|
|
2295
|
+
*
|
|
2296
|
+
* @example
|
|
2297
|
+
* ```typescript
|
|
2298
|
+
* const options = schema()
|
|
2299
|
+
* .required('name')
|
|
2300
|
+
* .string('name')
|
|
2301
|
+
* .toValidationOptions({ validationLevel: 'moderate' })
|
|
2302
|
+
* ```
|
|
2303
|
+
*/
|
|
2304
|
+
toValidationOptions(options) {
|
|
2305
|
+
return {
|
|
2306
|
+
validator: { $jsonSchema: this.schema },
|
|
2307
|
+
validationLevel: options?.validationLevel ?? "strict",
|
|
2308
|
+
validationAction: options?.validationAction ?? "error"
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
function schema() {
|
|
2313
|
+
return new MongoSchemaBuilder();
|
|
2314
|
+
}
|
|
1659
2315
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1660
2316
|
0 && (module.exports = {
|
|
1661
2317
|
Mongo,
|
|
@@ -1664,5 +2320,7 @@ var MongoPoolMonitor = class {
|
|
|
1664
2320
|
MongoGridFS,
|
|
1665
2321
|
MongoManager,
|
|
1666
2322
|
MongoPoolMonitor,
|
|
1667
|
-
MongoQueryBuilder
|
|
2323
|
+
MongoQueryBuilder,
|
|
2324
|
+
MongoSchemaBuilder,
|
|
2325
|
+
schema
|
|
1668
2326
|
});
|