@wxn0brp/db-core 0.12.11 → 0.12.13

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.
@@ -1,7 +1,15 @@
1
+ import { QueryValidationError } from "./errors.js";
1
2
  import { matchObj, updateObj } from "./utils/process.js";
2
3
  import { updateFindObject } from "./utils/updateFindObject.js";
3
4
  export function pathRepair(path) {
4
- return path.replace(/\/+/g, "/");
5
+ const repaired = path.replace(/\/+/g, "/");
6
+ if (repaired.split(/[/\\]+/).includes("..")) {
7
+ throw new QueryValidationError("Path traversal is not allowed", {
8
+ op: "pathRepair",
9
+ path,
10
+ });
11
+ }
12
+ return repaired;
5
13
  }
6
14
  export class CustomFileCpu {
7
15
  requireClone;
@@ -33,15 +33,12 @@ export class MultiBackend extends ActionsBase {
33
33
  }
34
34
  async findOne(config) {
35
35
  for (const backend of this.backends) {
36
- try {
37
- const result = await backend.findOne({
38
- ...config,
39
- });
40
- if (result !== null && result !== undefined) {
41
- return result;
42
- }
36
+ const result = await backend.findOne({
37
+ ...config,
38
+ });
39
+ if (result !== null && result !== undefined) {
40
+ return result;
43
41
  }
44
- catch { }
45
42
  }
46
43
  return null;
47
44
  }
@@ -53,14 +50,11 @@ export class MultiBackend extends ActionsBase {
53
50
  }
54
51
  async updateOne(config) {
55
52
  for (const backend of this.backends) {
56
- try {
57
- const result = await backend.updateOne({
58
- ...config,
59
- });
60
- if (result)
61
- return result;
62
- }
63
- catch (error) { }
53
+ const result = await backend.updateOne({
54
+ ...config,
55
+ });
56
+ if (result)
57
+ return result;
64
58
  }
65
59
  return null;
66
60
  }
@@ -72,14 +66,11 @@ export class MultiBackend extends ActionsBase {
72
66
  }
73
67
  async removeOne(config) {
74
68
  for (const backend of this.backends) {
75
- try {
76
- const result = await backend.removeOne({
77
- ...config,
78
- });
79
- if (result)
80
- return result;
81
- }
82
- catch (error) { }
69
+ const result = await backend.removeOne({
70
+ ...config,
71
+ });
72
+ if (result)
73
+ return result;
83
74
  }
84
75
  return null;
85
76
  }
@@ -23,10 +23,10 @@ export declare class Transaction {
23
23
  updateOneOrAdd<T = Data>(q: VQueryT.UpdateOneOrAdd<T>): Promise<VQueryT.UpdateOneOrAddResult<T>>;
24
24
  toggleOne<T = Data>(q: VQueryT.ToggleOne<T>): Promise<VQueryT.ToggleOneResult<T>>;
25
25
  count<T = Data>(q: VQueryT.Count<T>): Promise<number>;
26
- ensureCollection(collection: string): Promise<boolean>;
27
- issetCollection(collection: string): Promise<boolean>;
26
+ ensureCollection(collection: string | VQueryT.Collection): Promise<boolean>;
27
+ issetCollection(collection: string | VQueryT.Collection): Promise<boolean>;
28
28
  getCollections(): Promise<string[]>;
29
- removeCollection(collection: string): Promise<boolean>;
29
+ removeCollection(target: string | VQueryT.Collection): Promise<boolean>;
30
30
  commit(): Promise<void>;
31
31
  rollback(): Promise<void>;
32
32
  }
@@ -1,5 +1,5 @@
1
1
  import { wrapAdapterError } from "../errors.js";
2
- import { Collection } from "../helpers/collection.js";
2
+ import { Collection, resolveCollection } from "../helpers/collection.js";
3
3
  import { applyAddDefaults, applyBulkAddDefaults, applyCountDefaults, applyFindDefaults, applyFindOneDefaults, applyRemoveDefaults, applyToggleOneDefaults, applyUpdateDefaults, applyUpdateOneOrAddDefaults, } from "../helpers/queryDefaults.js";
4
4
  /**
5
5
  * Transaction context. Operations are executed directly through the parent
@@ -60,16 +60,16 @@ export class Transaction {
60
60
  return this._parent.execute("count", q, this.handle);
61
61
  }
62
62
  ensureCollection(collection) {
63
- return this._parent.execute("ensureCollection", collection, this.handle);
63
+ return this._parent.execute("ensureCollection", resolveCollection(collection), this.handle);
64
64
  }
65
65
  issetCollection(collection) {
66
- return this._parent.execute("issetCollection", collection, this.handle);
66
+ return this._parent.execute("issetCollection", resolveCollection(collection), this.handle);
67
67
  }
68
68
  getCollections() {
69
69
  return this._parent.execute("getCollections", {}, this.handle);
70
70
  }
71
- removeCollection(collection) {
72
- return this._parent.execute("removeCollection", collection, this.handle);
71
+ removeCollection(target) {
72
+ return this._parent.execute("removeCollection", resolveCollection(target), this.handle);
73
73
  }
74
74
  async commit() {
75
75
  try {
@@ -20,9 +20,9 @@ export declare class ValtheraClass implements ValtheraCompatible {
20
20
  adapter: ActionsBase;
21
21
  executor: ExecutorInterface;
22
22
  emitter: VEE<{
23
- [K in keyof Omit<ValtheraCompatible, "c">]: (query: VQuery, result: Awaited<ReturnType<ValtheraCompatible[K]>>) => void;
23
+ [K in keyof Omit<ValtheraCompatible, "c">]: (query: VQuery, result: Awaited<ReturnType<ValtheraCompatible[K]>>, collection?: string) => void;
24
24
  } & {
25
- "*": (name: keyof Omit<ValtheraCompatible, "c">, query: VQuery, result: any) => void;
25
+ "*": (name: keyof Omit<ValtheraCompatible, "c">, query: VQuery, result: any, collection?: string) => void;
26
26
  }>;
27
27
  version: string;
28
28
  _plugins: ValtheraPlugin[];
@@ -43,11 +43,11 @@ export declare class ValtheraClass implements ValtheraCompatible {
43
43
  /**
44
44
  * Check and create the specified collection if it doesn't exist.
45
45
  */
46
- ensureCollection(collection: string): Promise<boolean>;
46
+ ensureCollection(collection: string | VQueryT.Collection): Promise<boolean>;
47
47
  /**
48
48
  * Check if a collection exists.
49
49
  */
50
- issetCollection(collection: string): Promise<boolean>;
50
+ issetCollection(collection: string | VQueryT.Collection): Promise<boolean>;
51
51
  /**
52
52
  * Add data to a database.
53
53
  */
@@ -88,7 +88,7 @@ export declare class ValtheraClass implements ValtheraCompatible {
88
88
  /**
89
89
  * Removes a database collection from the database.
90
90
  */
91
- removeCollection(collection: string): Promise<boolean>;
91
+ removeCollection(target: string | VQueryT.Collection): Promise<boolean>;
92
92
  /**
93
93
  * Create an index on a collection.
94
94
  */
@@ -1,9 +1,11 @@
1
1
  import { VEE } from "@wxn0brp/event-emitter";
2
- import { AdapterCloseError, AdapterInitError, ErrorCodes, isValtheraError, QueryValidationError, TransactionError, wrapAdapterError, } from "../errors.js";
3
- import { Collection } from "../helpers/collection.js";
2
+ import * as errors from "../errors.js";
3
+ import { Collection, resolveCollection } from "../helpers/collection.js";
4
4
  import { SmartExecutor } from "../helpers/executor.js";
5
5
  import { genId } from "../helpers/gen.js";
6
- import { applyAddDefaults, applyBulkAddDefaults, applyCountDefaults, applyFindDefaults, applyFindOneDefaults, applyRemoveDefaults, applyToggleOneDefaults, applyUpdateDefaults, applyUpdateOneOrAddDefaults, } from "../helpers/queryDefaults.js";
6
+ import { rewriteOp } from "../helpers/plugin.js";
7
+ import * as queryDefaults from "../helpers/queryDefaults.js";
8
+ import { getOpKind, isKnownOp } from "../utils/opKind.js";
7
9
  import { version } from "../version.js";
8
10
  import { Transaction } from "./transaction.js";
9
11
  /**
@@ -50,9 +52,9 @@ export class ValtheraClass {
50
52
  return;
51
53
  const self = this;
52
54
  const failInit = (err) => {
53
- if (isValtheraError(err))
55
+ if (errors.isValtheraError(err))
54
56
  throw err;
55
- throw new AdapterInitError(err, {
57
+ throw new errors.AdapterInitError(err, {
56
58
  adapter: self.adapter?.version,
57
59
  });
58
60
  };
@@ -99,9 +101,9 @@ export class ValtheraClass {
99
101
  await self.adapter.close(...args);
100
102
  }
101
103
  catch (err) {
102
- if (isValtheraError(err))
104
+ if (errors.isValtheraError(err))
103
105
  throw err;
104
- throw new AdapterCloseError(err, {
106
+ throw new errors.AdapterCloseError(err, {
105
107
  adapter: self.adapter?.version,
106
108
  });
107
109
  }
@@ -112,7 +114,7 @@ export class ValtheraClass {
112
114
  await this.init();
113
115
  const collection = typeof query === "string" ? query : query?.collection;
114
116
  if (!collection && name !== "getCollections")
115
- throw new QueryValidationError(`Missing "collection" for operation "${String(name)}"`, {
117
+ throw new errors.QueryValidationError(`Missing "collection" for operation "${String(name)}"`, {
116
118
  op: name,
117
119
  });
118
120
  if (txHandle && query && typeof query === "object")
@@ -122,26 +124,41 @@ export class ValtheraClass {
122
124
  let idx = 0;
123
125
  const ctx = {
124
126
  op: name,
127
+ kind: getOpKind(name),
128
+ collection,
125
129
  query,
126
130
  next: async () => {
127
131
  if (idx < plugins.length)
128
132
  return plugins[idx++].execute(ctx);
133
+ if (txHandle && ctx.query && typeof ctx.query === "object")
134
+ ctx.query.transaction = txHandle;
135
+ if (!isKnownOp(ctx.op))
136
+ throw new errors.QueryValidationError(`Unknown operation "${ctx.op}"`, {
137
+ op: ctx.op,
138
+ });
129
139
  try {
130
140
  if (txHandle)
131
- return await self.adapter[ctx.op](query, txHandle);
141
+ return await self.adapter[ctx.op](ctx.query, txHandle);
132
142
  return await self.executor.addOp(self.adapter[ctx.op].bind(self.adapter), ctx.query, typeof ctx.query === "string" ? ctx.query : ctx.query.collection);
133
143
  }
134
144
  catch (err) {
135
- throw wrapAdapterError(err, {
145
+ throw errors.wrapAdapterError(err, {
136
146
  op: ctx.op,
137
- collection,
147
+ collection: typeof ctx.query === "string" ? ctx.query : ctx.query?.collection,
138
148
  adapter: self.adapter?.version,
139
149
  });
140
150
  }
141
151
  },
152
+ setOp(op, q) {
153
+ rewriteOp(ctx, op, q);
154
+ },
155
+ setQuery(q) {
156
+ rewriteOp(ctx, ctx.op, q);
157
+ },
142
158
  };
143
159
  const result = await ctx.next();
144
- this.emitter.emit(ctx.op, ctx.query, result);
160
+ const emittedCollection = typeof ctx.query === "string" ? ctx.query : ctx.query?.collection;
161
+ this.emitter.emit(ctx.op, ctx.query, result, emittedCollection);
145
162
  return result;
146
163
  }
147
164
  /**
@@ -164,85 +181,86 @@ export class ValtheraClass {
164
181
  * Check and create the specified collection if it doesn't exist.
165
182
  */
166
183
  async ensureCollection(collection) {
167
- return await this.execute("ensureCollection", collection);
184
+ return await this.execute("ensureCollection", resolveCollection(collection));
168
185
  }
169
186
  /**
170
187
  * Check if a collection exists.
171
188
  */
172
189
  async issetCollection(collection) {
173
- return await this.execute("issetCollection", collection);
190
+ return await this.execute("issetCollection", resolveCollection(collection));
174
191
  }
175
192
  /**
176
193
  * Add data to a database.
177
194
  */
178
195
  add(query) {
179
- applyAddDefaults(query);
196
+ queryDefaults.applyAddDefaults(query);
180
197
  return this.execute("add", query);
181
198
  }
182
199
  bulkAdd(query) {
183
- applyBulkAddDefaults(query);
200
+ queryDefaults.applyBulkAddDefaults(query);
184
201
  return this.execute("bulkAdd", query);
185
202
  }
186
203
  /**
187
204
  * Find data in a database.
188
205
  */
189
206
  find(query) {
190
- applyFindDefaults(query);
207
+ queryDefaults.applyFindDefaults(query);
191
208
  return this.execute("find", query);
192
209
  }
193
210
  /**
194
211
  * Find one data entry in a database.
195
212
  */
196
213
  findOne(query) {
197
- applyFindOneDefaults(query);
214
+ queryDefaults.applyFindOneDefaults(query);
198
215
  return this.execute("findOne", query);
199
216
  }
200
217
  /**
201
218
  * Update data in a database.
202
219
  */
203
220
  update(query) {
204
- applyUpdateDefaults(query);
221
+ queryDefaults.applyUpdateDefaults(query);
205
222
  return this.execute("update", query);
206
223
  }
207
224
  /**
208
225
  * Update one data entry in a database.
209
226
  */
210
227
  updateOne(query) {
211
- applyUpdateDefaults(query);
228
+ queryDefaults.applyUpdateDefaults(query);
212
229
  return this.execute("updateOne", query);
213
230
  }
214
231
  /**
215
232
  * Remove data from a database.
216
233
  */
217
234
  remove(query) {
218
- applyRemoveDefaults(query);
235
+ queryDefaults.applyRemoveDefaults(query);
219
236
  return this.execute("remove", query);
220
237
  }
221
238
  /**
222
239
  * Remove one data entry from a database.
223
240
  */
224
241
  removeOne(query) {
225
- applyRemoveDefaults(query);
242
+ queryDefaults.applyRemoveDefaults(query);
226
243
  return this.execute("removeOne", query);
227
244
  }
228
245
  /**
229
246
  * Asynchronously updates one entry in a database or adds a new one if it doesn't exist.
230
247
  */
231
248
  updateOneOrAdd(query) {
232
- applyUpdateOneOrAddDefaults(query);
249
+ queryDefaults.applyUpdateOneOrAddDefaults(query);
233
250
  return this.execute("updateOneOrAdd", query);
234
251
  }
235
252
  /**
236
253
  * Asynchronously removes one entry in a database or adds a new one if it doesn't exist. Usage e.g. for toggling a flag.
237
254
  */
238
255
  toggleOne(query) {
239
- applyToggleOneDefaults(query);
256
+ queryDefaults.applyToggleOneDefaults(query);
240
257
  return this.execute("toggleOne", query);
241
258
  }
242
259
  /**
243
260
  * Removes a database collection from the database.
244
261
  */
245
- removeCollection(collection) {
262
+ removeCollection(target) {
263
+ const collection = resolveCollection(target);
246
264
  this._collections.delete(collection);
247
265
  return this.execute("removeCollection", collection);
248
266
  }
@@ -274,7 +292,7 @@ export class ValtheraClass {
274
292
  });
275
293
  }
276
294
  count(query) {
277
- applyCountDefaults(query);
295
+ queryDefaults.applyCountDefaults(query);
278
296
  return this.execute("count", query);
279
297
  }
280
298
  /**
@@ -319,7 +337,7 @@ export class ValtheraClass {
319
337
  handle = await this.adapter.beginTransaction(genId(), collections);
320
338
  }
321
339
  catch (err) {
322
- throw wrapAdapterError(err, {
340
+ throw errors.wrapAdapterError(err, {
323
341
  op: "beginTransaction",
324
342
  adapter: this.adapter?.version,
325
343
  });
@@ -335,8 +353,8 @@ export class ValtheraClass {
335
353
  await tx.rollback();
336
354
  }
337
355
  catch (rollbackErr) {
338
- throw new TransactionError("Transaction failed and rollback failed", {
339
- code: ErrorCodes.TRANSACTION_ROLLBACK_FAILED,
356
+ throw new errors.TransactionError("Transaction failed and rollback failed", {
357
+ code: errors.ErrorCodes.TRANSACTION_ROLLBACK_FAILED,
340
358
  cause: new AggregateError([
341
359
  err,
342
360
  rollbackErr,
@@ -1,4 +1,14 @@
1
+ import { QueryValidationError } from "../errors.js";
2
+ import { isUnsafeKey } from "../utils/security.js";
1
3
  import { genId } from "./gen.js";
4
+ function assertSafeIdKey(idKey) {
5
+ if (!isUnsafeKey(idKey))
6
+ return;
7
+ throw new QueryValidationError("Unsafe idKey", {
8
+ op: "addId",
9
+ idKey,
10
+ });
11
+ }
2
12
  export async function addId(query, actions, defaultGen = true) {
3
13
  const collection = query.collection;
4
14
  const data = query.data;
@@ -6,6 +16,7 @@ export async function addId(query, actions, defaultGen = true) {
6
16
  const { idKey = "_id", numberId } = actions.adapterOpts;
7
17
  if (!id_gen)
8
18
  return data;
19
+ assertSafeIdKey(idKey);
9
20
  if (data[idKey])
10
21
  return data;
11
22
  if (!numberId) {
@@ -45,6 +56,7 @@ export async function addIdBulk(query, actions, defaultGen = true) {
45
56
  const { idKey = "_id", numberId } = actions.adapterOpts;
46
57
  if (!id_gen)
47
58
  return;
59
+ assertSafeIdKey(idKey);
48
60
  if (!numberId) {
49
61
  for (const data of query.datas) {
50
62
  if (!data[idKey])
@@ -1,3 +1,4 @@
1
+ import { isUnsafeKey } from "../utils/security.js";
1
2
  export function assignDataPush(data) {
2
3
  if (!data)
3
4
  return {};
@@ -7,6 +8,8 @@ export function assignDataPush(data) {
7
8
  return {};
8
9
  const obj = {};
9
10
  for (const key of Object.keys(data)) {
11
+ if (isUnsafeKey(key))
12
+ continue;
10
13
  if (!key.startsWith("$")) {
11
14
  obj[key] = data[key];
12
15
  continue;
@@ -14,7 +17,10 @@ export function assignDataPush(data) {
14
17
  const dk = data[key];
15
18
  if (Array.isArray(dk))
16
19
  continue;
17
- Object.keys(dk).forEach(k => (obj[k] = dk[k]));
20
+ Object.keys(dk).forEach(k => {
21
+ if (!isUnsafeKey(k))
22
+ obj[k] = dk[k];
23
+ });
18
24
  }
19
25
  return obj;
20
26
  }
@@ -6,6 +6,7 @@ import { DbFindOpts, FindOpts } from "../types/options.js";
6
6
  import { VQueryT } from "../types/query.js";
7
7
  import { VContext } from "../types/types.js";
8
8
  import { ValtheraCompatible } from "../types/valthera.js";
9
+ export declare function resolveCollection(target: string | VQueryT.Collection): string;
9
10
  export declare class Collection<D = Data> {
10
11
  db: ValtheraCompatible;
11
12
  collection: string;
@@ -1,3 +1,6 @@
1
+ export function resolveCollection(target) {
2
+ return typeof target === "string" ? target : target.collection;
3
+ }
1
4
  export class Collection {
2
5
  db;
3
6
  collection;
@@ -1,9 +1,22 @@
1
+ import { isUnsafeKey } from "../utils/security.js";
2
+ function hasApiProperty(target, prop) {
3
+ if (isUnsafeKey(prop))
4
+ return false;
5
+ let current = target;
6
+ while (current && current !== Object.prototype) {
7
+ if (Object.hasOwn(current, prop))
8
+ return true;
9
+ current = Object.getPrototypeOf(current);
10
+ }
11
+ return false;
12
+ }
1
13
  export function forgeTypedValthera(target) {
2
14
  return new Proxy(target, {
3
15
  get(target, prop, receiver) {
4
- if (prop in target) {
16
+ if (typeof prop === "symbol")
17
+ return Reflect.get(target, prop, receiver);
18
+ if (hasApiProperty(target, prop))
5
19
  return Reflect.get(target, prop, receiver);
6
- }
7
20
  return target.c(prop);
8
21
  },
9
22
  set(target, prop, value, receiver) {
@@ -0,0 +1,2 @@
1
+ import { PluginContext } from "../types/plugin.js";
2
+ export declare function rewriteOp(ctx: PluginContext, op: string, query?: any): void;
@@ -0,0 +1,9 @@
1
+ import { getOpKind } from "../utils/opKind.js";
2
+ export function rewriteOp(ctx, op, query) {
3
+ ctx.op = op;
4
+ if (query !== undefined)
5
+ ctx.query = query;
6
+ ctx.kind = getOpKind(op);
7
+ const q = ctx.query;
8
+ ctx.collection = typeof q === "string" ? q : q?.collection;
9
+ }
@@ -1,21 +1,32 @@
1
1
  import { QueryValidationError } from "../errors.js";
2
+ import { hasUnsafeSegment, isUnsafeKey } from "../utils/security.js";
2
3
  export function pickByPath(obj, paths) {
3
4
  const result = {};
4
5
  for (const path of paths) {
6
+ if (hasUnsafeSegment(path))
7
+ continue;
5
8
  let src = obj;
6
9
  let dst = result;
7
10
  for (let i = 0; i < path.length; i++) {
8
11
  const k = path[i];
9
12
  if (src == null)
10
13
  break;
14
+ const isOwn = Object.hasOwn(src, k);
15
+ if (!isOwn && k in Object(src))
16
+ break;
11
17
  if (i === path.length - 1) {
12
18
  dst[k] = src[k];
19
+ break;
13
20
  }
14
- else {
15
- dst[k] ||= {};
16
- dst = dst[k];
17
- src = src[k];
18
- }
21
+ if (!Object.hasOwn(dst, k) ||
22
+ typeof dst[k] !== "object" ||
23
+ dst[k] === null ||
24
+ Array.isArray(dst[k]))
25
+ dst[k] = {};
26
+ if (!isOwn)
27
+ break;
28
+ dst = dst[k];
29
+ src = src[k];
19
30
  }
20
31
  }
21
32
  return result;
@@ -71,10 +82,12 @@ export async function processRelations(dbs, cfg, data, parentList = null) {
71
82
  if (targets.length === 0)
72
83
  return;
73
84
  for (const key in cfg) {
74
- if (!Object.hasOwn(cfg, key))
85
+ if (!Object.hasOwn(cfg, key) || isUnsafeKey(key))
75
86
  continue;
76
87
  const rel = cfg[key];
77
88
  const { pk = "_id", fk = "_id", type = "1", path, as = key, select, dbFindOpts = {}, through, } = rel;
89
+ if (isUnsafeKey(as))
90
+ continue;
78
91
  const [dbKey, collection] = path;
79
92
  const db = dbs[dbKey];
80
93
  if (type === "1") {
@@ -150,7 +163,7 @@ export async function processRelations(dbs, cfg, data, parentList = null) {
150
163
  acc[id] ||= [];
151
164
  acc[id].push(row);
152
165
  return acc;
153
- }, {});
166
+ }, Object.create(null));
154
167
  for (const item of targets)
155
168
  item[as] = grouped[item[pk]] || [];
156
169
  if (rel.relations)
@@ -1,8 +1,13 @@
1
1
  import type { ValtheraClass } from "../db/valthera.js";
2
+ import type { PluginOpKind } from "../utils/opKind.js";
2
3
  export interface PluginContext {
3
4
  op: string;
5
+ kind: PluginOpKind;
6
+ collection?: string;
4
7
  query: any;
5
8
  next: () => Promise<any>;
9
+ setOp(op: string, query?: any): void;
10
+ setQuery(query: any): void;
6
11
  }
7
12
  export interface ValtheraPlugin {
8
13
  name: string;
@@ -94,4 +94,7 @@ export declare namespace VQueryT {
94
94
  type Count<T = Data> = QueryBase<T> & {
95
95
  search: Search<T, false>;
96
96
  };
97
+ interface Collection {
98
+ collection: string;
99
+ }
97
100
  }
@@ -6,8 +6,8 @@ import { VQueryT } from "./query.js";
6
6
  export interface ValtheraCompatible {
7
7
  c<T = Data>(collection: string): Collection<T>;
8
8
  getCollections(): Promise<string[]>;
9
- ensureCollection(collection: string): Promise<boolean>;
10
- issetCollection(collection: string): Promise<boolean>;
9
+ ensureCollection(collection: string | VQueryT.Collection): Promise<boolean>;
10
+ issetCollection(collection: string | VQueryT.Collection): Promise<boolean>;
11
11
  add<T = Data>(query: VQueryT.Add<T>): Promise<T>;
12
12
  find<T = Data>(query: VQueryT.Find<T>): Promise<T[]>;
13
13
  findOne<T = Data>(query: VQueryT.FindOne<T>): Promise<T | null>;
@@ -15,7 +15,7 @@ export interface ValtheraCompatible {
15
15
  updateOne<T = Data>(query: VQueryT.Update<T>): Promise<T | null>;
16
16
  remove<T = Data>(query: VQueryT.Remove<T>): Promise<T[]>;
17
17
  removeOne<T = Data>(query: VQueryT.Remove<T>): Promise<T | null>;
18
- removeCollection(collection: string): Promise<boolean>;
18
+ removeCollection(collection: string | VQueryT.Collection): Promise<boolean>;
19
19
  updateOneOrAdd<T = Data>(query: VQueryT.UpdateOneOrAdd<T>): Promise<VQueryT.UpdateOneOrAddResult<T>>;
20
20
  toggleOne<T = Data>(query: VQueryT.ToggleOne<T>): Promise<VQueryT.ToggleOneResult<T>>;
21
21
  createIndex(collection: string, fields: string[], opts?: IndexOpts): Promise<void>;
@@ -1,4 +1,8 @@
1
1
  import { compareSafe } from "./compare.js";
2
+ import { isUnsafeKey } from "./security.js";
3
+ function getOwnValue(obj, key) {
4
+ return Object.hasOwn(obj, key) ? obj[key] : undefined;
5
+ }
2
6
  export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
3
7
  const { dbFindOpts = {} } = query;
4
8
  const { reverse = false, offset = 0, sortBy, sortAsc = true, min, max, avg, sum, distinct, groupBy, count, } = dbFindOpts;
@@ -69,18 +73,18 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
69
73
  const hasAggregations = min || max || avg || sum || groupBy || count;
70
74
  if (hasAggregations) {
71
75
  const groups = new Map();
72
- const groupKeys = Array.isArray(groupBy)
76
+ const groupKeys = (Array.isArray(groupBy)
73
77
  ? groupBy
74
78
  : groupBy
75
79
  ? [
76
80
  groupBy,
77
81
  ]
78
- : [];
82
+ : []).filter(key => !isUnsafeKey(key));
79
83
  if (groupKeys.length) {
80
84
  for (const data of datas) {
81
- let key = String(data[groupKeys[0]] ?? "");
85
+ let key = String(getOwnValue(data, groupKeys[0]) ?? "");
82
86
  for (let i = 1; i < groupKeys.length; i++) {
83
- key += "|" + String(data[groupKeys[i]] ?? "");
87
+ key += "|" + String(getOwnValue(data, groupKeys[i]) ?? "");
84
88
  }
85
89
  let group = groups.get(key);
86
90
  if (!group) {
@@ -98,14 +102,18 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
98
102
  const result = {};
99
103
  if (groupKeys.length) {
100
104
  const sample = groupItems[0];
101
- for (const k of groupKeys)
102
- result[k] = sample[k];
105
+ for (const k of groupKeys) {
106
+ if (Object.hasOwn(sample, k))
107
+ result[k] = sample[k];
108
+ }
103
109
  }
104
110
  if (count) {
105
111
  for (const [outKey, srcKey] of Object.entries(count)) {
112
+ if (isUnsafeKey(outKey))
113
+ continue;
106
114
  let c = 0;
107
115
  for (let i = 0; i < groupItems.length; i++) {
108
- const v = groupItems[i][srcKey];
116
+ const v = getOwnValue(groupItems[i], srcKey);
109
117
  if (v !== undefined && v !== null)
110
118
  c++;
111
119
  }
@@ -118,9 +126,11 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
118
126
  const sumEntries = sum ? Object.entries(sum) : null;
119
127
  if (minEntries) {
120
128
  for (const [outKey, srcField] of minEntries) {
129
+ if (isUnsafeKey(outKey))
130
+ continue;
121
131
  let best = null;
122
132
  for (let i = 0; i < groupItems.length; i++) {
123
- const v = groupItems[i][srcField];
133
+ const v = getOwnValue(groupItems[i], srcField);
124
134
  if (typeof v === "number") {
125
135
  if (best === null || v < best)
126
136
  best = v;
@@ -131,9 +141,11 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
131
141
  }
132
142
  if (maxEntries) {
133
143
  for (const [outKey, srcField] of maxEntries) {
144
+ if (isUnsafeKey(outKey))
145
+ continue;
134
146
  let best = null;
135
147
  for (let i = 0; i < groupItems.length; i++) {
136
- const v = groupItems[i][srcField];
148
+ const v = getOwnValue(groupItems[i], srcField);
137
149
  if (typeof v === "number") {
138
150
  if (best === null || v > best)
139
151
  best = v;
@@ -144,10 +156,12 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
144
156
  }
145
157
  if (avgEntries) {
146
158
  for (const [outKey, srcField] of avgEntries) {
159
+ if (isUnsafeKey(outKey))
160
+ continue;
147
161
  let total = 0;
148
162
  let c = 0;
149
163
  for (let i = 0; i < groupItems.length; i++) {
150
- const v = groupItems[i][srcField];
164
+ const v = getOwnValue(groupItems[i], srcField);
151
165
  if (typeof v === "number") {
152
166
  total += v;
153
167
  c++;
@@ -158,10 +172,12 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
158
172
  }
159
173
  if (sumEntries) {
160
174
  for (const [outKey, srcField] of sumEntries) {
175
+ if (isUnsafeKey(outKey))
176
+ continue;
161
177
  let total = 0;
162
178
  let c = 0;
163
179
  for (let i = 0; i < groupItems.length; i++) {
164
- const v = groupItems[i][srcField];
180
+ const v = getOwnValue(groupItems[i], srcField);
165
181
  if (typeof v === "number") {
166
182
  total += v;
167
183
  c++;
@@ -174,11 +190,11 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
174
190
  }
175
191
  datas = aggregated;
176
192
  }
177
- if (distinct) {
193
+ if (distinct && !isUnsafeKey(distinct)) {
178
194
  const seen = new Set();
179
195
  const result = [];
180
196
  for (let i = 0; i < datas.length; i++) {
181
- const val = datas[i][distinct];
197
+ const val = getOwnValue(datas[i], distinct);
182
198
  const key = typeof val === "object" && val !== null ? JSON.stringify(val) : val;
183
199
  if (!seen.has(key)) {
184
200
  seen.add(key);
@@ -195,7 +211,7 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
195
211
  datas.sort((a, b) => {
196
212
  for (const { field, asc = true } of sortBy) {
197
213
  const dir = asc ? 1 : -1;
198
- const cmp = compareSafe(a[field], b[field]) * dir;
214
+ const cmp = compareSafe(getOwnValue(a, field), getOwnValue(b, field)) * dir;
199
215
  if (cmp !== 0)
200
216
  return cmp;
201
217
  }
@@ -204,7 +220,7 @@ export async function findUtil(query, fileCpuOrData, files, fileCpuOpts) {
204
220
  }
205
221
  else {
206
222
  const dir = sortAsc ? 1 : -1;
207
- datas.sort((a, b) => compareSafe(a[sortBy], b[sortBy]) * dir);
223
+ datas.sort((a, b) => compareSafe(getOwnValue(a, sortBy), getOwnValue(b, sortBy)) * dir);
208
224
  }
209
225
  }
210
226
  if (offset > 0 || limit !== -1) {
@@ -7,7 +7,7 @@
7
7
  export function hasFields(obj, fields) {
8
8
  const keys = Object.keys(fields);
9
9
  return keys.every(key => {
10
- if (obj[key] !== undefined) {
10
+ if (Object.hasOwn(obj, key)) {
11
11
  if (typeof fields[key] === "object" && fields[key] !== null) {
12
12
  return hasFields(obj[key], fields[key]);
13
13
  }
@@ -1,6 +1,7 @@
1
1
  import { QueryValidationError } from "../errors.js";
2
2
  import { hasFields } from "./hasFields.js";
3
3
  import { compareIds } from "./id.js";
4
+ import { isUnsafeKey } from "./security.js";
4
5
  /**
5
6
  * Checks if an object meets the criteria specified in the fields with operators.
6
7
  */
@@ -16,6 +17,8 @@ export function hasFieldsAdvanced(obj, fields) {
16
17
  const subsetFields = {};
17
18
  let hasSubset = false;
18
19
  for (const key in fields) {
20
+ if (!Object.hasOwn(fields, key) || isUnsafeKey(key))
21
+ continue;
19
22
  if (key.charCodeAt(0) === 36) {
20
23
  $fields[key.toLowerCase()] = fields[key];
21
24
  }
@@ -31,17 +34,17 @@ export function hasFieldsAdvanced(obj, fields) {
31
34
  if (Object.keys($fields).length === 0)
32
35
  return true;
33
36
  // Subset check
34
- if ("$subset" in $fields && !hasFields(obj, $fields["$subset"]))
37
+ if (Object.hasOwn($fields, "$subset") && !hasFields(obj, $fields["$subset"]))
35
38
  return false;
36
39
  // Advanced operators
37
40
  if (!mainCheck(obj, $fields))
38
41
  return false;
39
42
  if (!checkNot(obj, $fields))
40
43
  return false;
41
- if ("$and" in $fields) {
44
+ if (Object.hasOwn($fields, "$and")) {
42
45
  return $fields["$and"].every((subFields) => hasFieldsAdvanced(obj, subFields));
43
46
  }
44
- if ("$or" in $fields) {
47
+ if (Object.hasOwn($fields, "$or")) {
45
48
  return $fields["$or"].some((subFields) => hasFieldsAdvanced(obj, subFields));
46
49
  }
47
50
  return true;
@@ -57,9 +60,9 @@ function mainCheck(obj, fields) {
57
60
  nin: (data, value) => !value.includes(data),
58
61
  type: (data, value) => typeof data === value,
59
62
  exists: (targetObj, shouldExist, key) => {
60
- if (shouldExist && !(key in targetObj))
63
+ if (shouldExist && !Object.hasOwn(targetObj, key))
61
64
  return false;
62
- if (!shouldExist && key in targetObj)
65
+ if (!shouldExist && Object.hasOwn(targetObj, key))
63
66
  return false;
64
67
  return true;
65
68
  },
@@ -93,7 +96,7 @@ function mainCheck(obj, fields) {
93
96
  });
94
97
  }
95
98
  function checkNot(obj, fields) {
96
- if ("$not" in fields) {
99
+ if (Object.hasOwn(fields, "$not")) {
97
100
  return !hasFieldsAdvanced(obj, fields["$not"]);
98
101
  }
99
102
  return true;
@@ -101,11 +104,13 @@ function checkNot(obj, fields) {
101
104
  function _for(fields, obj, opts) {
102
105
  for (const [field, value] of Object.entries(fields)) {
103
106
  const fieldRaw = field.slice(1);
104
- const fieldFn = opts[fieldRaw];
107
+ const fieldFn = Object.hasOwn(opts, fieldRaw) ? opts[fieldRaw] : undefined;
105
108
  if (!fieldFn)
106
109
  continue;
107
110
  for (const [key, val] of Object.entries(value)) {
108
- const targetValue = obj?.[key];
111
+ if (isUnsafeKey(key))
112
+ continue;
113
+ const targetValue = obj != null && Object.hasOwn(obj, key) ? obj[key] : undefined;
109
114
  if (typeof val === "object" &&
110
115
  val !== null &&
111
116
  !Array.isArray(val) &&
@@ -131,7 +136,9 @@ function deepCheck(valueObj, targetObj, fieldFn, fieldRaw) {
131
136
  if (typeof targetObj !== "object" || targetObj === null)
132
137
  return false;
133
138
  for (const [k, v] of Object.entries(valueObj)) {
134
- const targetValue = targetObj[k];
139
+ if (isUnsafeKey(k))
140
+ continue;
141
+ const targetValue = Object.hasOwn(targetObj, k) ? targetObj[k] : undefined;
135
142
  if (typeof v === "object" &&
136
143
  v !== null &&
137
144
  !Array.isArray(v) &&
@@ -1,3 +1,4 @@
1
+ import { isUnsafeKey } from "./security.js";
1
2
  export function _deepMerge(target, ...sources) {
2
3
  if (!sources.length)
3
4
  return target;
@@ -6,6 +7,8 @@ export function _deepMerge(target, ...sources) {
6
7
  target = {};
7
8
  if (isObject(target) && isObject(source)) {
8
9
  for (const key in source) {
10
+ if (!Object.hasOwn(source, key) || isUnsafeKey(key))
11
+ continue;
9
12
  if (isObject(source[key])) {
10
13
  if (!target[key])
11
14
  Object.assign(target, {
@@ -30,8 +33,11 @@ function fastClone(obj) {
30
33
  if (obj instanceof Date)
31
34
  return new Date(obj.getTime());
32
35
  const result = {};
33
- for (const key in obj)
36
+ for (const key in obj) {
37
+ if (!Object.hasOwn(obj, key) || isUnsafeKey(key))
38
+ continue;
34
39
  result[key] = fastClone(obj[key]);
40
+ }
35
41
  return result;
36
42
  }
37
43
  export function deepMerge(target, ...sources) {
@@ -0,0 +1,9 @@
1
+ export type PluginOpKind = "read" | "write" | "delete" | "collection" | "index" | "meta" | "control";
2
+ export declare function getOpKind(op: string): PluginOpKind;
3
+ export declare function isKnownOp(op: string): boolean;
4
+ export declare function isMutation(kind: PluginOpKind): kind is "delete" | "write";
5
+ export declare function isCollectionOp(op: string): boolean;
6
+ export declare function isIndexOp(op: string): boolean;
7
+ export declare function isMetaOp(op: string): boolean;
8
+ export declare function isControlOp(op: string): boolean;
9
+ export declare function isNoQueryOp(op: string): boolean;
@@ -0,0 +1,48 @@
1
+ const OP_KINDS = {
2
+ find: "read",
3
+ findOne: "read",
4
+ count: "read",
5
+ add: "write",
6
+ bulkAdd: "write",
7
+ update: "write",
8
+ updateOne: "write",
9
+ updateOneOrAdd: "write",
10
+ toggleOne: "write",
11
+ remove: "delete",
12
+ removeOne: "delete",
13
+ ensureCollection: "collection",
14
+ issetCollection: "collection",
15
+ removeCollection: "collection",
16
+ createIndex: "index",
17
+ removeIndex: "index",
18
+ getCollections: "meta",
19
+ capabilities: "meta",
20
+ beginTransaction: "control",
21
+ commitTransaction: "control",
22
+ rollbackTransaction: "control",
23
+ };
24
+ export function getOpKind(op) {
25
+ return Object.hasOwn(OP_KINDS, op) ? OP_KINDS[op] : "write";
26
+ }
27
+ export function isKnownOp(op) {
28
+ return Object.hasOwn(OP_KINDS, op);
29
+ }
30
+ export function isMutation(kind) {
31
+ return kind === "write" || kind === "delete";
32
+ }
33
+ export function isCollectionOp(op) {
34
+ return getOpKind(op) === "collection";
35
+ }
36
+ export function isIndexOp(op) {
37
+ return getOpKind(op) === "index";
38
+ }
39
+ export function isMetaOp(op) {
40
+ return getOpKind(op) === "meta";
41
+ }
42
+ export function isControlOp(op) {
43
+ return getOpKind(op) === "control";
44
+ }
45
+ export function isNoQueryOp(op) {
46
+ const kind = getOpKind(op);
47
+ return kind === "collection" || kind === "meta";
48
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isUnsafeKey(key: string): boolean;
2
+ export declare function hasUnsafeSegment(path: string[]): boolean;
@@ -0,0 +1,11 @@
1
+ const UNSAFE_KEYS = new Set([
2
+ "__proto__",
3
+ "constructor",
4
+ "prototype",
5
+ ]);
6
+ export function isUnsafeKey(key) {
7
+ return UNSAFE_KEYS.has(key);
8
+ }
9
+ export function hasUnsafeSegment(path) {
10
+ return path.some(segment => isUnsafeKey(segment));
11
+ }
@@ -1,14 +1,17 @@
1
+ import { hasUnsafeSegment } from "./security.js";
1
2
  function resolvePath(path) {
2
3
  return Array.isArray(path) ? path : path.split(".");
3
4
  }
4
5
  function deleteNested(obj, path) {
5
6
  const segs = resolvePath(path);
6
- if (segs.length === 0)
7
+ if (segs.length === 0 || hasUnsafeSegment(segs))
7
8
  return;
8
9
  let cur = obj;
9
10
  for (let i = 0; i < segs.length - 1; i++) {
10
11
  if (cur == null || typeof cur !== "object")
11
12
  return;
13
+ if (!Object.hasOwn(cur, segs[i]))
14
+ return;
12
15
  cur = cur[segs[i]];
13
16
  }
14
17
  if (cur != null && typeof cur === "object") {
@@ -39,12 +42,14 @@ export function updateFindObject(obj, findOpts) {
39
42
  const result = {};
40
43
  for (const field of select) {
41
44
  const segs = typeof field === "string" ? field.split(".") : field;
42
- if (!segs.length)
45
+ if (!segs.length || hasUnsafeSegment(segs))
43
46
  continue;
44
47
  let src = obj;
45
48
  let found = true;
46
49
  for (let i = 0; i < segs.length; i++) {
47
- if (src == null || typeof src !== "object" || !(segs[i] in src)) {
50
+ if (src == null ||
51
+ typeof src !== "object" ||
52
+ !Object.hasOwn(src, segs[i])) {
48
53
  found = false;
49
54
  break;
50
55
  }
@@ -54,7 +59,7 @@ export function updateFindObject(obj, findOpts) {
54
59
  let dst = result;
55
60
  for (let i = 0; i < segs.length - 1; i++) {
56
61
  const s = segs[i];
57
- if (!(s in dst) ||
62
+ if (!Object.hasOwn(dst, s) ||
58
63
  typeof dst[s] !== "object" ||
59
64
  Array.isArray(dst[s])) {
60
65
  dst[s] = {};
@@ -1,5 +1,6 @@
1
1
  import { QueryValidationError } from "../errors.js";
2
2
  import { _deepMerge } from "./merge.js";
3
+ import { isUnsafeKey } from "./security.js";
3
4
  /**
4
5
  * Updates an object with new values.
5
6
  * @param obj - The object to update.
@@ -17,6 +18,8 @@ export function updateObjectAdvanced(obj, field) {
17
18
  let hasDollar = false;
18
19
  let hasSubset = false;
19
20
  for (const key in field) {
21
+ if (!Object.hasOwn(field, key) || isUnsafeKey(key))
22
+ continue;
20
23
  if (key.charCodeAt(0) === 36) {
21
24
  $fields[key.toLowerCase()] = field[key];
22
25
  hasDollar = true;
@@ -34,7 +37,7 @@ export function updateObjectAdvanced(obj, field) {
34
37
  updateObject(obj, subsetFields);
35
38
  }
36
39
  function deepMerge(obj, fields) {
37
- if (!("$deepmerge" in fields))
40
+ if (!Object.hasOwn(fields, "$deepmerge"))
38
41
  return obj;
39
42
  if (Object.keys(obj).length === 0)
40
43
  return fields.$deepmerge;
@@ -86,10 +89,16 @@ function mainUpdate(obj, fields) {
86
89
  ];
87
90
  }
88
91
  if (typeof item === "object" &&
92
+ item !== null &&
89
93
  typeof updater === "object" &&
94
+ updater !== null &&
90
95
  !Array.isArray(item) &&
91
96
  !Array.isArray(updater)) {
92
- Object.assign(item, updater);
97
+ for (const key of Object.keys(updater)) {
98
+ if (isUnsafeKey(key))
99
+ continue;
100
+ item[key] = updater[key];
101
+ }
93
102
  return item;
94
103
  }
95
104
  return updater;
@@ -98,7 +107,7 @@ function mainUpdate(obj, fields) {
98
107
  if (typeof item === "number" && typeof updater === "number") {
99
108
  return item + updater;
100
109
  }
101
- if (!(key in deepObj))
110
+ if (!Object.hasOwn(deepObj, key))
102
111
  return updater;
103
112
  throw new QueryValidationError(`Cannot increment non-numeric value at key: ${key}`, {
104
113
  op: "update",
@@ -109,7 +118,7 @@ function mainUpdate(obj, fields) {
109
118
  if (typeof item === "number" && typeof updater === "number") {
110
119
  return item - updater;
111
120
  }
112
- if (!(key in deepObj))
121
+ if (!Object.hasOwn(deepObj, key))
113
122
  return -updater;
114
123
  throw new QueryValidationError(`Cannot decrement non-numeric value at key: ${key}`, {
115
124
  op: "update",
@@ -117,14 +126,16 @@ function mainUpdate(obj, fields) {
117
126
  });
118
127
  },
119
128
  rename: (_, newKey, oldKey, deepObj) => {
120
- if (oldKey in deepObj) {
129
+ if (isUnsafeKey(oldKey) || isUnsafeKey(newKey))
130
+ return;
131
+ if (Object.hasOwn(deepObj, oldKey)) {
121
132
  deepObj[newKey] = deepObj[oldKey];
122
133
  delete deepObj[oldKey];
123
134
  }
124
135
  },
125
136
  set: (_, value) => value,
126
137
  unset: (_, __, key, deepObj) => {
127
- if (key in deepObj)
138
+ if (Object.hasOwn(deepObj, key))
128
139
  delete deepObj[key];
129
140
  },
130
141
  });
@@ -135,20 +146,24 @@ function mainUpdate(obj, fields) {
135
146
  * @param newVal - An object containing new values to update in the target object.
136
147
  */
137
148
  function updateObject(obj, newVal) {
138
- for (const key in newVal)
139
- if (Object.hasOwn(newVal, key))
140
- obj[key] = newVal[key];
149
+ for (const key in newVal) {
150
+ if (!Object.hasOwn(newVal, key) || isUnsafeKey(key))
151
+ continue;
152
+ obj[key] = newVal[key];
153
+ }
141
154
  return obj;
142
155
  }
143
156
  function _for(fields, obj, opts) {
144
157
  for (const [field, value] of Object.entries(fields)) {
145
158
  const fieldRaw = field.slice(1);
146
- const fieldFn = opts[fieldRaw];
159
+ const fieldFn = Object.hasOwn(opts, fieldRaw) ? opts[fieldRaw] : undefined;
147
160
  if (!fieldFn)
148
161
  continue;
149
162
  for (const [key, val] of Object.entries(value)) {
163
+ if (isUnsafeKey(key))
164
+ continue;
150
165
  if (typeof val === "object" && val !== null && !Array.isArray(val)) {
151
- if (!obj[key])
166
+ if (!Object.hasOwn(obj, key) || !obj[key])
152
167
  obj[key] = {};
153
168
  deepUpdateCheck(val, obj[key], fieldFn);
154
169
  }
@@ -162,8 +177,10 @@ function _for(fields, obj, opts) {
162
177
  }
163
178
  function deepUpdateCheck(valueObj, targetObj, fieldFn) {
164
179
  for (const [k, v] of Object.entries(valueObj)) {
180
+ if (isUnsafeKey(k))
181
+ continue;
165
182
  if (typeof v === "object" && v !== null && !Array.isArray(v)) {
166
- if (!targetObj[k])
183
+ if (!Object.hasOwn(targetObj, k) || !targetObj[k])
167
184
  targetObj[k] = {};
168
185
  deepUpdateCheck(v, targetObj[k], fieldFn);
169
186
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.12.11";
1
+ export declare const version = "0.12.13";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.12.11";
1
+ export const version = "0.12.13";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wxn0brp/db-core",
3
- "version": "0.12.11",
3
+ "version": "0.12.13",
4
4
  "types": "dist/index.d.ts",
5
5
  "main": "dist/index.js",
6
6
  "author": "wxn0brP",