@unchainedshop/mongodb 4.5.0 → 4.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.
package/README.md CHANGED
@@ -84,6 +84,107 @@ await stopDb(db);
84
84
  |----------|-------------|
85
85
  | `UNCHAINED_DOCUMENTDB_COMPAT_MODE` | Enable AWS DocumentDB compatibility mode |
86
86
 
87
+ ## Best Practices
88
+
89
+ ### Collection Naming Conventions
90
+
91
+ Unchained uses the following collection naming patterns:
92
+
93
+ | Pattern | Example | Usage |
94
+ |---------|---------|-------|
95
+ | Plural lowercase | `products`, `orders`, `users` | Main entity collections |
96
+ | Underscore-separated | `product_texts`, `product_media` | Related sub-collections |
97
+
98
+ **Note:** Some legacy collections may use different patterns. When creating new collections, prefer the underscore-separated pattern for sub-collections.
99
+
100
+ ### Index Guidelines
101
+
102
+ #### Using `buildDbIndexes`
103
+
104
+ Always use the `buildDbIndexes` helper to create indexes:
105
+
106
+ ```typescript
107
+ import { buildDbIndexes } from '@unchainedshop/mongodb';
108
+
109
+ await buildDbIndexes<Product>(Products, [
110
+ { index: { deleted: 1 } }, // Soft delete support
111
+ { index: { status: 1 } }, // Query by status
112
+ { index: { slugs: 1 } }, // URL slug lookups
113
+ { index: { tags: 1 } }, // Tag filtering
114
+ ]);
115
+ ```
116
+
117
+ #### Soft Delete Pattern
118
+
119
+ Collections using soft delete should always include a `deleted` index:
120
+
121
+ ```typescript
122
+ { index: { deleted: 1 } }
123
+ ```
124
+
125
+ Queries should filter by `deleted: null` to exclude soft-deleted documents.
126
+
127
+ #### Sparse Indexes
128
+
129
+ Use sparse indexes when the indexed field may be null/undefined for most documents:
130
+
131
+ ```typescript
132
+ {
133
+ index: { optionalField: 1 },
134
+ options: { sparse: true }
135
+ }
136
+ ```
137
+
138
+ Sparse indexes are smaller and more efficient when the field is rarely present.
139
+
140
+ #### Text Indexes
141
+
142
+ For full-text search, create compound text indexes:
143
+
144
+ ```typescript
145
+ {
146
+ index: {
147
+ _id: 'text',
148
+ name: 'text',
149
+ description: 'text',
150
+ } as any,
151
+ options: {
152
+ weights: {
153
+ _id: 10,
154
+ name: 5,
155
+ description: 1,
156
+ },
157
+ name: 'fulltext_search',
158
+ },
159
+ }
160
+ ```
161
+
162
+ **Important:** Text indexes are not supported in DocumentDB compatibility mode. Use `isDocumentDBCompatModeEnabled()` to conditionally create them.
163
+
164
+ ### DocumentDB Compatibility
165
+
166
+ When running on AWS DocumentDB, some MongoDB features are not supported:
167
+
168
+ | Feature | Alternative |
169
+ |---------|-------------|
170
+ | Text indexes | Use application-level search or external search service |
171
+ | `$text` queries | Use regex queries (less performant) |
172
+ | Some aggregation operators | Check DocumentDB documentation |
173
+
174
+ Use the compatibility helpers:
175
+
176
+ ```typescript
177
+ import { isDocumentDBCompatModeEnabled, assertDocumentDBCompatMode } from '@unchainedshop/mongodb';
178
+
179
+ // Check before using unsupported features
180
+ if (!isDocumentDBCompatModeEnabled()) {
181
+ // Create text index
182
+ }
183
+
184
+ // Throw error if DocumentDB mode is enabled
185
+ assertDocumentDBCompatMode(); // Throws if in compat mode
186
+ ```
187
+
87
188
  ## License
88
189
 
89
190
  EUPL-1.2
package/lib/initDb.d.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import type { Db } from 'mongodb';
2
+ import { MongoClient } from 'mongodb';
2
3
  export declare const startDb: (options?: {
3
4
  forceInMemory?: boolean;
5
+ port?: number;
4
6
  }) => Promise<string>;
5
7
  export declare const stopDb: () => Promise<void>;
8
+ export interface DatabaseResource extends AsyncDisposable {
9
+ db: Db;
10
+ client: MongoClient;
11
+ }
12
+ export declare const createDatabaseResource: (options?: {
13
+ forceInMemory?: boolean;
14
+ port?: number;
15
+ }) => Promise<DatabaseResource>;
6
16
  declare const initDb: (options?: {
7
17
  forceInMemory?: boolean;
18
+ port?: number;
8
19
  }) => Promise<Db>;
9
20
  export { initDb };
package/lib/initDb.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { MongoClient } from 'mongodb';
2
2
  let zstdEnabled = false;
3
- let mongod;
4
- let mongoClient;
5
- const { PORT = '4010' } = process.env;
3
+ let mongod = null;
4
+ let mongoClient = null;
5
+ let useEphemeralStorage = false;
6
+ const CLEANUP_TIMEOUT_MS = 10000;
6
7
  try {
7
8
  await import('@mongodb-js/zstd');
8
9
  zstdEnabled = true;
@@ -13,6 +14,8 @@ export const startDb = async (options) => {
13
14
  const { mkdir } = await import('node:fs/promises');
14
15
  const { MongoMemoryServer } = await import('mongodb-memory-server');
15
16
  const useInMemory = options?.forceInMemory || process.env.NODE_ENV === 'test';
17
+ useEphemeralStorage = useInMemory;
18
+ const mongoPort = options?.port ?? parseInt(process.env.PORT || '4010', 10) + 1;
16
19
  if (!useInMemory) {
17
20
  try {
18
21
  await mkdir(`${process.cwd()}/.db`);
@@ -23,11 +26,11 @@ export const startDb = async (options) => {
23
26
  try {
24
27
  mongod = MongoMemoryServer.create({
25
28
  instance: useInMemory
26
- ? { dbName: 'test', port: parseInt(PORT, 10) + 1, storageEngine: 'ephemeralForTest' }
29
+ ? { dbName: 'test', port: mongoPort, storageEngine: 'ephemeralForTest' }
27
30
  : {
28
31
  dbPath: `${process.cwd()}/.db`,
29
32
  storageEngine: 'wiredTiger',
30
- port: parseInt(PORT, 10) + 1,
33
+ port: mongoPort,
31
34
  },
32
35
  });
33
36
  const mongoInstance = await mongod;
@@ -35,22 +38,56 @@ export const startDb = async (options) => {
35
38
  return `${mongoInstance.getUri()}${useInMemory ? 'test' : 'unchained'}`;
36
39
  }
37
40
  }
38
- catch {
41
+ catch (e) {
42
+ const error = e;
43
+ if (error.message?.includes('code "62"')) {
44
+ throw new Error(`MongoDB database files in .db are incompatible with the current MongoDB version. ` +
45
+ `This usually happens after a MongoDB upgrade. ` +
46
+ `To fix this, remove the .db directory: rm -rf ${process.cwd()}/.db`);
47
+ }
48
+ throw error;
39
49
  }
40
50
  throw new Error("Can't connect to MongoDB: Could not start mongodb-memory-server and MONGO_URL env is not set");
41
51
  };
42
52
  export const stopDb = async () => {
43
- try {
53
+ const cleanup = async () => {
44
54
  await mongoClient?.close();
45
- await (await mongod)?.stop();
55
+ const server = await mongod;
56
+ await server?.stop({ doCleanup: useEphemeralStorage, force: true });
57
+ };
58
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Database cleanup timeout')), CLEANUP_TIMEOUT_MS));
59
+ try {
60
+ await Promise.race([cleanup(), timeout]);
46
61
  }
47
62
  catch {
63
+ const server = await mongod;
64
+ await server?.stop({ force: true }).catch(() => {
65
+ });
66
+ }
67
+ finally {
68
+ mongoClient = null;
69
+ mongod = null;
48
70
  }
49
71
  };
72
+ export const createDatabaseResource = async (options) => {
73
+ const url = options?.forceInMemory
74
+ ? await startDb({ forceInMemory: true, port: options.port })
75
+ : process.env.MONGO_URL || (await startDb({ port: options?.port }));
76
+ mongoClient = new MongoClient(url, {
77
+ compressors: zstdEnabled ? 'zstd' : undefined,
78
+ });
79
+ await mongoClient.connect();
80
+ const db = mongoClient.db();
81
+ return {
82
+ db,
83
+ client: mongoClient,
84
+ [Symbol.asyncDispose]: stopDb,
85
+ };
86
+ };
50
87
  const initDb = async (options) => {
51
88
  const url = options?.forceInMemory
52
- ? await startDb({ forceInMemory: true })
53
- : process.env.MONGO_URL || (await startDb());
89
+ ? await startDb({ forceInMemory: true, port: options.port })
90
+ : process.env.MONGO_URL || (await startDb({ port: options?.port }));
54
91
  mongoClient = new MongoClient(url, {
55
92
  compressors: zstdEnabled ? 'zstd' : undefined,
56
93
  });
@@ -1,3 +1,4 @@
1
+ export declare function escapeRegexString(string: string): string;
1
2
  export declare function insensitiveTrimmedRegexOperator(string: any): {
2
3
  $regex: RegExp;
3
4
  };
@@ -1,11 +1,17 @@
1
+ export function escapeRegexString(string) {
2
+ if (typeof string !== 'string') {
3
+ throw new TypeError('Expected a string');
4
+ }
5
+ return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
6
+ }
1
7
  export function insensitiveTrimmedRegexOperator(string) {
2
8
  if (typeof string !== 'string') {
3
9
  throw new TypeError('Expected a string');
4
10
  }
5
- const escapped = string.trim().replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
6
- if (escapped.length === 0)
11
+ const escaped = escapeRegexString(string.trim());
12
+ if (escaped.length === 0)
7
13
  throw new Error('String is empty after trimming');
8
- if (escapped.length > 255)
14
+ if (escaped.length > 255)
9
15
  throw new Error('String exceeds maximum allowed length');
10
- return { $regex: new RegExp(`^${escapped}$`, 'i') };
16
+ return { $regex: new RegExp(`^${escaped}$`, 'i') };
11
17
  }
@@ -1,12 +1,13 @@
1
1
  import * as mongodb from 'mongodb';
2
- export { initDb, startDb, stopDb } from './initDb.ts';
2
+ export { initDb, startDb, stopDb, createDatabaseResource } from './initDb.ts';
3
+ export type { DatabaseResource } from './initDb.ts';
3
4
  export { generateDbObjectId } from './generate-db-object-id.ts';
4
5
  export { generateDbFilterById } from './generate-db-filter-by-id.ts';
5
6
  export { buildDbIndexes } from './build-db-indexes.ts';
6
7
  export { findPreservingIds } from './find-preserving-ids.ts';
7
8
  export { buildSortOptions } from './build-sort-option.ts';
8
9
  export { findLocalizedText } from './find-localized-text.ts';
9
- export { insensitiveTrimmedRegexOperator } from './insensitive-trimmed-regex-operator.ts';
10
+ export { insensitiveTrimmedRegexOperator, escapeRegexString, } from './insensitive-trimmed-regex-operator.ts';
10
11
  export * from './documentdb-compat-mode.ts';
11
12
  export { mongodb };
12
13
  export interface LogFields {
@@ -1,11 +1,11 @@
1
1
  import * as mongodb from 'mongodb';
2
- export { initDb, startDb, stopDb } from "./initDb.js";
2
+ export { initDb, startDb, stopDb, createDatabaseResource } from "./initDb.js";
3
3
  export { generateDbObjectId } from "./generate-db-object-id.js";
4
4
  export { generateDbFilterById } from "./generate-db-filter-by-id.js";
5
5
  export { buildDbIndexes } from "./build-db-indexes.js";
6
6
  export { findPreservingIds } from "./find-preserving-ids.js";
7
7
  export { buildSortOptions } from "./build-sort-option.js";
8
8
  export { findLocalizedText } from "./find-localized-text.js";
9
- export { insensitiveTrimmedRegexOperator } from "./insensitive-trimmed-regex-operator.js";
9
+ export { insensitiveTrimmedRegexOperator, escapeRegexString, } from "./insensitive-trimmed-regex-operator.js";
10
10
  export * from "./documentdb-compat-mode.js";
11
11
  export { mongodb };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unchainedshop/mongodb",
3
- "version": "4.5.0",
4
- "description": "MongoDB provider for unchained platform",
3
+ "version": "4.6.0",
4
+ "description": "MongoDB database abstraction layer for the Unchained Engine",
5
5
  "main": "lib/mongodb-index.js",
6
6
  "types": "lib/mongodb-index.d.ts",
7
7
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "homepage": "https://github.com/unchainedshop/unchained#readme",
35
35
  "dependencies": {
36
- "@unchainedshop/utils": "^4.5.0"
36
+ "@unchainedshop/utils": "^4.6.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@mongodb-js/zstd": ">= 7 < 8",