@semics-tech/mongolite 1.0.0 → 1.1.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.
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractRawIndexColumns = extractRawIndexColumns;
4
+ function extractRawIndexColumns(sql) {
5
+ const match = sql.match(/\(([\s\S]+)\)/); // allow multiline matching
6
+ if (!match)
7
+ return [];
8
+ const colsPart = match[1];
9
+ // Split on commas not inside parentheses
10
+ const columns = [];
11
+ let current = '';
12
+ let parens = 0;
13
+ for (let i = 0; i < colsPart.length; i++) {
14
+ const char = colsPart[i];
15
+ if (char === '(')
16
+ parens++;
17
+ if (char === ')')
18
+ parens--;
19
+ if (char === ',' && parens === 0) {
20
+ columns.push(current.trim());
21
+ current = '';
22
+ }
23
+ else {
24
+ current += char;
25
+ }
26
+ }
27
+ if (current.trim())
28
+ columns.push(current.trim());
29
+ return columns.map((col) => {
30
+ // Extract direction (ASC or DESC)
31
+ const directionMatch = col.match(/\s+(ASC|DESC)\s*$/i);
32
+ const direction = directionMatch ? directionMatch[1].toUpperCase() : null;
33
+ // Remove direction from column string
34
+ const colWithoutDirection = directionMatch
35
+ ? col.slice(0, directionMatch.index).trim()
36
+ : col.trim();
37
+ // Check for json_extract and pull out path
38
+ const jsonMatch = colWithoutDirection.match(/json_extract\s*\(\s*[^,]+,\s*'([^']+)'\s*\)/i);
39
+ let column = jsonMatch
40
+ ? jsonMatch[1] // the JSON path like '$.age'
41
+ : colWithoutDirection; // fallback to raw column like testCol
42
+ column = column.replace('$.', ''); // Remove leading $ if present
43
+ return { column, direction };
44
+ });
45
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProjectionMode = resolveProjectionMode;
4
+ exports.applyProjectionToDocument = applyProjectionToDocument;
5
+ /**
6
+ * Determines whether a projection is an inclusion or exclusion projection, matching MongoDB's
7
+ * rule: any explicit non-`_id` inclusion (1/true) puts it in inclusion mode; otherwise any
8
+ * explicit non-`_id` exclusion (0/false) puts it in exclusion mode; with only `_id` specified
9
+ * (or nothing), `_id: 0`/`false` means exclusion, everything else defaults to inclusion.
10
+ */
11
+ function resolveProjectionMode(projection) {
12
+ let hasExplicitInclusion = false;
13
+ let hasExplicitExclusion = false;
14
+ for (const key of Object.keys(projection)) {
15
+ if (key === '_id')
16
+ continue;
17
+ const value = projection[key];
18
+ if (value === 1 || value === true) {
19
+ hasExplicitInclusion = true;
20
+ break;
21
+ }
22
+ if (value === 0 || value === false) {
23
+ hasExplicitExclusion = true;
24
+ }
25
+ }
26
+ if (hasExplicitInclusion)
27
+ return 'include';
28
+ if (hasExplicitExclusion)
29
+ return 'exclude';
30
+ return projection._id === 0 || projection._id === false ? 'exclude' : 'include';
31
+ }
32
+ /**
33
+ * Applies a MongoDB-style projection to a single document, matching real MongoDB find()
34
+ * projection semantics: an empty/missing projection means no restriction, inclusion mode
35
+ * copies only listed fields (plus `_id` unless explicitly excluded), and exclusion mode copies
36
+ * everything except the listed fields. Supports dot-notation nested paths in both modes.
37
+ */
38
+ function applyProjectionToDocument(doc, projection) {
39
+ if (!projection || Object.keys(projection).length === 0)
40
+ return doc;
41
+ const mode = resolveProjectionMode(projection);
42
+ const projectedDoc = {};
43
+ if (mode === 'include') {
44
+ for (const key in projection) {
45
+ const value = projection[key];
46
+ if (value !== 1 && value !== true)
47
+ continue;
48
+ if (key.includes('.')) {
49
+ const path = key.split('.');
50
+ let current = doc;
51
+ let target = projectedDoc;
52
+ for (let i = 0; i < path.length - 1; i++) {
53
+ const segment = path[i];
54
+ if (current[segment] === undefined || current[segment] === null)
55
+ break;
56
+ if (target[segment] === undefined) {
57
+ target[segment] = {};
58
+ }
59
+ current = current[segment];
60
+ target = target[segment];
61
+ }
62
+ const lastSegment = path[path.length - 1];
63
+ if (current && current[lastSegment] !== undefined) {
64
+ target[lastSegment] = current[lastSegment];
65
+ }
66
+ }
67
+ else if (key in doc) {
68
+ projectedDoc[key] = doc[key];
69
+ }
70
+ }
71
+ if (projection._id !== 0 && projection._id !== false && '_id' in doc) {
72
+ projectedDoc._id = doc._id;
73
+ }
74
+ }
75
+ else {
76
+ Object.assign(projectedDoc, doc);
77
+ for (const key in projection) {
78
+ const value = projection[key];
79
+ if (value !== 0 && value !== false)
80
+ continue;
81
+ if (key.includes('.')) {
82
+ const path = key.split('.');
83
+ let current = projectedDoc;
84
+ for (let i = 0; i < path.length - 1; i++) {
85
+ const segment = path[i];
86
+ if (current[segment] === undefined)
87
+ break;
88
+ current = current[segment];
89
+ }
90
+ const lastSegment = path[path.length - 1];
91
+ if (current && current[lastSegment] !== undefined) {
92
+ delete current[lastSegment];
93
+ }
94
+ }
95
+ else {
96
+ delete projectedDoc[key];
97
+ }
98
+ }
99
+ }
100
+ return projectedDoc;
101
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SUPPORTED_QUERY_OPERATORS = void 0;
4
+ exports.evaluateOperator = evaluateOperator;
5
+ exports.evaluateOperators = evaluateOperators;
6
+ /**
7
+ * Shared query-operator registry used by in-memory ($match) filtering. Kept in sync with the
8
+ * operator set `buildComparisonCondition` (src/cursors/findCursor.ts) supports for SQL-generated
9
+ * find() filtering, so the two engines can't silently drift apart on what an operator means —
10
+ * see tests/operator-coverage.test.ts, which runs the same filter through both engines.
11
+ */
12
+ exports.SUPPORTED_QUERY_OPERATORS = [
13
+ '$eq',
14
+ '$ne',
15
+ '$gt',
16
+ '$gte',
17
+ '$lt',
18
+ '$lte',
19
+ '$in',
20
+ '$nin',
21
+ '$all',
22
+ '$exists',
23
+ '$not',
24
+ '$regex',
25
+ '$size',
26
+ '$type',
27
+ '$mod',
28
+ ];
29
+ function matchesBsonType(value, bsonType) {
30
+ switch (bsonType) {
31
+ case 1:
32
+ case 'double':
33
+ case 'number':
34
+ return typeof value === 'number';
35
+ case 2:
36
+ case 'string':
37
+ return typeof value === 'string';
38
+ case 3:
39
+ case 'object':
40
+ return (typeof value === 'object' &&
41
+ value !== null &&
42
+ !Array.isArray(value) &&
43
+ !(value instanceof Date));
44
+ case 4:
45
+ case 'array':
46
+ return Array.isArray(value);
47
+ case 8:
48
+ case 'bool':
49
+ case 'boolean':
50
+ return typeof value === 'boolean';
51
+ case 9:
52
+ case 'date':
53
+ return value instanceof Date;
54
+ case 10:
55
+ case 'null':
56
+ return value === null;
57
+ case 16:
58
+ case 'int':
59
+ case 18:
60
+ case 'long':
61
+ return typeof value === 'number' && Number.isInteger(value);
62
+ default:
63
+ return false;
64
+ }
65
+ }
66
+ /**
67
+ * Evaluates a single query operator against a document's field value. Used for in-memory
68
+ * filtering (e.g. aggregation `$match`) — the SQL-generating side (`buildComparisonCondition`)
69
+ * emits SQL fragments instead of booleans and stays separate, but should support the same
70
+ * operator set.
71
+ */
72
+ function evaluateOperator(op, docValue, opValue, regexOptions) {
73
+ switch (op) {
74
+ case '$eq':
75
+ return JSON.stringify(docValue) === JSON.stringify(opValue);
76
+ case '$ne':
77
+ return JSON.stringify(docValue) !== JSON.stringify(opValue);
78
+ case '$gt':
79
+ return docValue > opValue;
80
+ case '$gte':
81
+ return docValue >= opValue;
82
+ case '$lt':
83
+ return docValue < opValue;
84
+ case '$lte':
85
+ return docValue <= opValue;
86
+ case '$in':
87
+ if (!Array.isArray(opValue))
88
+ return false;
89
+ if (Array.isArray(docValue)) {
90
+ return docValue.some((el) => opValue.some((v) => JSON.stringify(el) === JSON.stringify(v)));
91
+ }
92
+ return opValue.some((v) => JSON.stringify(v) === JSON.stringify(docValue));
93
+ case '$nin':
94
+ if (!Array.isArray(opValue))
95
+ return false;
96
+ if (Array.isArray(docValue)) {
97
+ return !docValue.some((el) => opValue.some((v) => JSON.stringify(el) === JSON.stringify(v)));
98
+ }
99
+ return !opValue.some((v) => JSON.stringify(v) === JSON.stringify(docValue));
100
+ case '$all':
101
+ if (!Array.isArray(opValue) || !Array.isArray(docValue))
102
+ return false;
103
+ return opValue.every((item) => docValue.some((el) => JSON.stringify(el) === JSON.stringify(item)));
104
+ case '$exists':
105
+ return opValue ? docValue !== undefined : docValue === undefined;
106
+ case '$not':
107
+ return !evaluateOperators(docValue, opValue);
108
+ case '$regex': {
109
+ const pattern = opValue instanceof RegExp
110
+ ? opValue
111
+ : new RegExp(opValue, typeof regexOptions === 'string' ? regexOptions : undefined);
112
+ return pattern.test(String(docValue));
113
+ }
114
+ case '$size':
115
+ return Array.isArray(docValue) && docValue.length === opValue;
116
+ case '$type': {
117
+ const types = Array.isArray(opValue) ? opValue : [opValue];
118
+ return types.some((t) => matchesBsonType(docValue, t));
119
+ }
120
+ case '$mod': {
121
+ if (!Array.isArray(opValue) || opValue.length !== 2 || typeof docValue !== 'number')
122
+ return false;
123
+ const [divisor, remainder] = opValue;
124
+ return Math.trunc(docValue) % divisor === remainder;
125
+ }
126
+ case '$options':
127
+ return true; // consumed alongside $regex, not an independent predicate
128
+ default:
129
+ return false; // unsupported operator — conservative: never matches, rather than silently no-op
130
+ }
131
+ }
132
+ /** Evaluates every operator in `ops` against `docValue`, ANDing the results together. */
133
+ function evaluateOperators(docValue, ops) {
134
+ const regexOptions = ops['$options'];
135
+ for (const [op, opValue] of Object.entries(ops)) {
136
+ if (op === '$options')
137
+ continue;
138
+ if (!evaluateOperator(op, docValue, opValue, regexOptions))
139
+ return false;
140
+ }
141
+ return true;
142
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IDatabaseAdapter, MongoLiteOptions as DBMongoLiteOptions } from './db.js';
1
+ import type { IDatabaseAdapter, MongoLiteOptions as DBMongoLiteOptions } from './db.js';
2
2
  import { MongoLite as MongoLiteBase, MongoLiteBaseOptions } from './mongo-client.js';
3
3
  export { MongoLiteCollection } from './collection.js';
4
4
  export * from './types.js';
@@ -9,13 +9,16 @@ export { CloudflareDurableObjectAdapter, } from './adapters/cloudflare.js';
9
9
  export type { SqlStorage, SqlStorageCursor, SqlStorageValue, } from './adapters/cloudflare.js';
10
10
  export { BrowserSqliteAdapter } from './adapters/browser.js';
11
11
  export type { SqlJsDatabase, SqlJsStatement } from './adapters/browser.js';
12
+ export { NodeSqliteAdapter } from './adapters/node-sqlite.js';
12
13
  export interface MongoLiteClientOptions extends DBMongoLiteOptions {
13
14
  }
14
15
  /**
15
16
  * MongoLite class is the main entry point for interacting with the SQLite-backed database.
16
17
  *
17
18
  * You can construct it with:
18
- * - A file path string — uses the built-in `better-sqlite3` adapter.
19
+ * - A file path string — uses the built-in `node:sqlite` adapter (`NodeSqliteAdapter`).
20
+ * Requires Node.js 22.5.0+; for older runtimes, import `MongoLite` from
21
+ * `@semics-tech/mongolite/better-sqlite3` instead.
19
22
  * - A `MongoLiteClientOptions` object — uses the built-in adapter with options.
20
23
  * - An `IDatabaseAdapter` instance — use a custom adapter such as
21
24
  * `CloudflareDurableObjectAdapter` for Cloudflare Durable Objects.
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
- import { SQLiteDB } from './db.js';
1
+ import { NodeSqliteAdapter } from './adapters/node-sqlite.js';
2
2
  import { MongoLite as MongoLiteBase } from './mongo-client.js';
3
3
  export { MongoLiteCollection } from './collection.js';
4
4
  export * from './types.js';
5
5
  export { ChangeStream } from './changeStream.js';
6
6
  export { CloudflareDurableObjectAdapter, } from './adapters/cloudflare.js';
7
7
  export { BrowserSqliteAdapter } from './adapters/browser.js';
8
+ export { NodeSqliteAdapter } from './adapters/node-sqlite.js';
8
9
  /**
9
10
  * MongoLite class is the main entry point for interacting with the SQLite-backed database.
10
11
  *
11
12
  * You can construct it with:
12
- * - A file path string — uses the built-in `better-sqlite3` adapter.
13
+ * - A file path string — uses the built-in `node:sqlite` adapter (`NodeSqliteAdapter`).
14
+ * Requires Node.js 22.5.0+; for older runtimes, import `MongoLite` from
15
+ * `@semics-tech/mongolite/better-sqlite3` instead.
13
16
  * - A `MongoLiteClientOptions` object — uses the built-in adapter with options.
14
17
  * - An `IDatabaseAdapter` instance — use a custom adapter such as
15
18
  * `CloudflareDurableObjectAdapter` for Cloudflare Durable Objects.
@@ -33,7 +36,7 @@ export class MongoLite extends MongoLiteBase {
33
36
  super(dbPathOrOptions, options);
34
37
  }
35
38
  else {
36
- super(new SQLiteDB(dbPathOrOptions), options);
39
+ super(new NodeSqliteAdapter(dbPathOrOptions), options);
37
40
  }
38
41
  }
39
42
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA4D,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,SAAS,IAAI,aAAa,EAAwB,MAAM,mBAAmB,CAAC;AAErF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAOjD,OAAO,EACL,8BAA8B,GAC/B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAK7D;;;;;;;;GAQG;AACH,MAAM,OAAO,SAAU,SAAQ,aAAa;IAC1C;;;;OAIG;IACH,YACE,eAAmE,EACnE,UAAgC,EAAE;QAElC,IACE,eAAe;YACf,OAAO,eAAe,KAAK,QAAQ;YACnC,OAAQ,eAAoC,CAAC,OAAO,KAAK,UAAU;YACnE,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,IAAI,KAAK,UAAU;YAChE,OAAQ,eAAoC,CAAC,KAAK,KAAK,UAAU,EACjE,CAAC;YACD,uDAAuD;YACvD,KAAK,CAAC,eAAmC,EAAE,OAAO,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,QAAQ,CAAC,eAAkD,CAAC,EAAE,OAAO,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,SAAS,IAAI,aAAa,EAAwB,MAAM,mBAAmB,CAAC;AAErF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAOjD,OAAO,EACL,8BAA8B,GAC/B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAI9D;;;;;;;;;;GAUG;AACH,MAAM,OAAO,SAAU,SAAQ,aAAa;IAC1C;;;;OAIG;IACH,YACE,eAAmE,EACnE,UAAgC,EAAE;QAElC,IACE,eAAe;YACf,OAAO,eAAe,KAAK,QAAQ;YACnC,OAAQ,eAAoC,CAAC,OAAO,KAAK,UAAU;YACnE,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,IAAI,KAAK,UAAU;YAChE,OAAQ,eAAoC,CAAC,KAAK,KAAK,UAAU,EACjE,CAAC;YACD,uDAAuD;YACvD,KAAK,CAAC,eAAmC,EAAE,OAAO,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,iBAAiB,CAAC,eAAkD,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;CACF"}
package/package.json CHANGED
@@ -1,25 +1,33 @@
1
1
  {
2
2
  "name": "@semics-tech/mongolite",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A MongoDB-like client using SQLite as a persistent store, written in TypeScript.",
5
- "main": "dist/index.js",
5
+ "main": "dist/cjs/index.js",
6
6
  "type": "module",
7
7
  "types": "dist/index.d.ts",
8
+ "sideEffects": false,
8
9
  "exports": {
9
10
  ".": {
10
11
  "types": "./dist/index.d.ts",
11
- "default": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/cjs/index.js"
12
14
  },
13
15
  "./cloudflare": {
14
16
  "types": "./dist/cloudflare.d.ts",
15
- "default": "./dist/cloudflare.js"
17
+ "import": "./dist/cloudflare.js",
18
+ "require": "./dist/cjs/cloudflare.js"
19
+ },
20
+ "./better-sqlite3": {
21
+ "types": "./dist/better-sqlite3.d.ts",
22
+ "import": "./dist/better-sqlite3.js",
23
+ "require": "./dist/cjs/better-sqlite3.js"
16
24
  }
17
25
  },
18
26
  "bin": {
19
27
  "mongolite-debug": "dist/bin/mongolite-debug.js"
20
28
  },
21
29
  "scripts": {
22
- "build": "tsc",
30
+ "build": "tsc && tsc -p tsconfig.cjs.json && cp src/cjs-package-stub.json dist/cjs/package.json",
23
31
  "test": "node --import tsx --test tests/*.test.ts",
24
32
  "test:parity": "node --import tsx --test tests/parity/*.parity.test.ts",
25
33
  "test:coverage": "c8 --reporter=lcov --reporter=text node --import tsx --test tests/*.test.ts",
@@ -63,30 +71,32 @@
63
71
  "homepage": "https://github.com/semics-tech/mongolite#readme",
64
72
  "devDependencies": {
65
73
  "@types/better-sqlite3": "^7.6.13",
66
- "@types/node": "^25.5.0",
74
+ "@types/node": "^26.1.2",
67
75
  "@typescript-eslint/eslint-plugin": "^8.58.1",
68
76
  "@typescript-eslint/parser": "^8.58.1",
69
- "c8": "^11.0.0",
77
+ "c8": "^12.0.0",
70
78
  "eslint": "^10.1.0",
71
79
  "eslint-config-prettier": "^10.1.8",
72
80
  "eslint-plugin-prettier": "^5.1.3",
73
81
  "expect": "^30.0.0",
74
- "mongodb-memory-server": "^10.4.3",
82
+ "mongodb-memory-server": "^11.2.0",
75
83
  "prettier": "^3.2.5",
76
84
  "tsx": "^4.19.4",
77
85
  "typescript": "^6.0.2"
78
86
  },
79
87
  "dependencies": {
80
88
  "@types/uuid": "^11.0.0",
81
- "better-sqlite3": "^12.1.1",
82
89
  "bson": "^7.2.0",
83
90
  "uuid": "^14.0.0"
84
91
  },
92
+ "optionalDependencies": {
93
+ "better-sqlite3": "^13.0.2"
94
+ },
85
95
  "files": [
86
96
  "dist/**/*",
87
97
  "README.md"
88
98
  ],
89
99
  "engines": {
90
- "node": ">=22.0.0"
100
+ "node": ">=22.5.0"
91
101
  }
92
102
  }