@zudojs/database 0.1.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -17
  3. package/dist/cache/cache.memory.d.ts +167 -0
  4. package/dist/cache/cache.memory.js +186 -40
  5. package/dist/cache/index.d.ts +2 -2
  6. package/dist/cache/index.js +2 -2
  7. package/dist/database/database.core.d.ts +91 -0
  8. package/dist/database/database.core.js +21 -4
  9. package/dist/databaseClient/databaseClient.core.d.ts +171 -14
  10. package/dist/databaseClient/databaseClient.core.js +350 -70
  11. package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
  12. package/dist/databaseClient/databaseClient.errors.js +425 -0
  13. package/dist/databaseClient/index.d.ts +2 -1
  14. package/dist/databaseClient/index.js +2 -1
  15. package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
  16. package/dist/databaseConnection/databaseConnection.manager.js +138 -76
  17. package/dist/databaseConnection/index.d.ts +1 -1
  18. package/dist/databaseType/databaseType.type.d.ts +224 -0
  19. package/dist/databaseType/databaseType.type.js +0 -35
  20. package/dist/health/health.check.d.ts +74 -0
  21. package/dist/health/health.check.js +78 -47
  22. package/dist/health/index.d.ts +1 -1
  23. package/dist/health/index.js +1 -1
  24. package/dist/index.d.ts +14 -14
  25. package/dist/index.js +11 -11
  26. package/dist/locks/index.d.ts +2 -2
  27. package/dist/locks/index.js +2 -2
  28. package/dist/locks/locks.core.d.ts +132 -0
  29. package/dist/locks/locks.core.js +150 -85
  30. package/dist/migration/index.d.ts +3 -1
  31. package/dist/migration/index.js +2 -0
  32. package/dist/migration/migration.dialect.d.ts +65 -0
  33. package/dist/migration/migration.dialect.js +63 -0
  34. package/dist/migration/migration.helpers.d.ts +65 -0
  35. package/dist/migration/migration.helpers.js +60 -18
  36. package/dist/migration/migration.runner.d.ts +75 -0
  37. package/dist/migration/migration.runner.js +195 -107
  38. package/dist/migration/migration.types.d.ts +85 -0
  39. package/dist/pagination/index.d.ts +2 -1
  40. package/dist/pagination/index.js +2 -1
  41. package/dist/pagination/pagination.core.d.ts +163 -0
  42. package/dist/pagination/pagination.core.js +78 -9
  43. package/dist/pagination/pagination.keyset.d.ts +55 -0
  44. package/dist/pagination/pagination.keyset.js +108 -0
  45. package/dist/queryBuilder/index.d.ts +4 -3
  46. package/dist/queryBuilder/index.js +3 -2
  47. package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
  48. package/dist/queryBuilder/queryBuilder.core.js +92 -14
  49. package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
  50. package/dist/queryBuilder/queryBuilder.factory.js +0 -25
  51. package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
  52. package/dist/queryBuilder/queryBuilder.filter.js +191 -30
  53. package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
  54. package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
  55. package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
  56. package/dist/relations/index.d.ts +1 -1
  57. package/dist/relations/index.js +1 -1
  58. package/dist/relations/relations.definition.d.ts +184 -0
  59. package/dist/relations/relations.definition.js +186 -19
  60. package/dist/repository/index.d.ts +2 -1
  61. package/dist/repository/index.js +1 -0
  62. package/dist/repository/repository.base.d.ts +283 -0
  63. package/dist/repository/repository.base.js +370 -89
  64. package/dist/repository/repository.errors.d.ts +60 -0
  65. package/dist/repository/repository.errors.js +235 -0
  66. package/dist/seed/index.d.ts +2 -2
  67. package/dist/seed/index.js +1 -1
  68. package/dist/seed/seed.runner.d.ts +160 -0
  69. package/dist/seed/seed.runner.js +194 -225
  70. package/dist/transaction/index.d.ts +1 -1
  71. package/dist/transaction/index.js +1 -1
  72. package/dist/transaction/transaction.core.d.ts +117 -0
  73. package/dist/transaction/transaction.core.js +83 -56
  74. package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
  75. package/dist/unitOfWork/unitOfWork.core.js +9 -20
  76. package/package.json +44 -24
  77. package/dist/cache/cache.memory.d.ts.map +0 -1
  78. package/dist/cache/cache.memory.js.map +0 -1
  79. package/dist/cache/index.d.ts.map +0 -1
  80. package/dist/cache/index.js.map +0 -1
  81. package/dist/database/database.core.d.ts.map +0 -1
  82. package/dist/database/database.core.js.map +0 -1
  83. package/dist/database/index.d.ts.map +0 -1
  84. package/dist/database/index.js.map +0 -1
  85. package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
  86. package/dist/databaseClient/databaseClient.core.js.map +0 -1
  87. package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
  88. package/dist/databaseClient/databaseClient.logger.js.map +0 -1
  89. package/dist/databaseClient/index.d.ts.map +0 -1
  90. package/dist/databaseClient/index.js.map +0 -1
  91. package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
  92. package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
  93. package/dist/databaseConnection/index.d.ts.map +0 -1
  94. package/dist/databaseConnection/index.js.map +0 -1
  95. package/dist/databaseType/databaseType.type.d.ts.map +0 -1
  96. package/dist/databaseType/databaseType.type.js.map +0 -1
  97. package/dist/databaseType/index.d.ts.map +0 -1
  98. package/dist/databaseType/index.js.map +0 -1
  99. package/dist/health/health.check.d.ts.map +0 -1
  100. package/dist/health/health.check.js.map +0 -1
  101. package/dist/health/index.d.ts.map +0 -1
  102. package/dist/health/index.js.map +0 -1
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js.map +0 -1
  105. package/dist/locks/index.d.ts.map +0 -1
  106. package/dist/locks/index.js.map +0 -1
  107. package/dist/locks/locks.core.d.ts.map +0 -1
  108. package/dist/locks/locks.core.js.map +0 -1
  109. package/dist/migration/index.d.ts.map +0 -1
  110. package/dist/migration/index.js.map +0 -1
  111. package/dist/migration/migration.helpers.d.ts.map +0 -1
  112. package/dist/migration/migration.helpers.js.map +0 -1
  113. package/dist/migration/migration.runner.d.ts.map +0 -1
  114. package/dist/migration/migration.runner.js.map +0 -1
  115. package/dist/migration/migration.types.d.ts.map +0 -1
  116. package/dist/migration/migration.types.js.map +0 -1
  117. package/dist/pagination/index.d.ts.map +0 -1
  118. package/dist/pagination/index.js.map +0 -1
  119. package/dist/pagination/pagination.core.d.ts.map +0 -1
  120. package/dist/pagination/pagination.core.js.map +0 -1
  121. package/dist/queryBuilder/index.d.ts.map +0 -1
  122. package/dist/queryBuilder/index.js.map +0 -1
  123. package/dist/queryBuilder/queryBuilder.core.d.ts.map +0 -1
  124. package/dist/queryBuilder/queryBuilder.core.js.map +0 -1
  125. package/dist/queryBuilder/queryBuilder.factory.d.ts.map +0 -1
  126. package/dist/queryBuilder/queryBuilder.factory.js.map +0 -1
  127. package/dist/queryBuilder/queryBuilder.filter.d.ts.map +0 -1
  128. package/dist/queryBuilder/queryBuilder.filter.js.map +0 -1
  129. package/dist/queryBuilder/queryBuilder.type.d.ts.map +0 -1
  130. package/dist/queryBuilder/queryBuilder.type.js.map +0 -1
  131. package/dist/relations/index.d.ts.map +0 -1
  132. package/dist/relations/index.js.map +0 -1
  133. package/dist/relations/relations.definition.d.ts.map +0 -1
  134. package/dist/relations/relations.definition.js.map +0 -1
  135. package/dist/repository/index.d.ts.map +0 -1
  136. package/dist/repository/index.js.map +0 -1
  137. package/dist/repository/repository.base.d.ts.map +0 -1
  138. package/dist/repository/repository.base.js.map +0 -1
  139. package/dist/seed/index.d.ts.map +0 -1
  140. package/dist/seed/index.js.map +0 -1
  141. package/dist/seed/seed.runner.d.ts.map +0 -1
  142. package/dist/seed/seed.runner.js.map +0 -1
  143. package/dist/transaction/index.d.ts.map +0 -1
  144. package/dist/transaction/index.js.map +0 -1
  145. package/dist/transaction/transaction.core.d.ts.map +0 -1
  146. package/dist/transaction/transaction.core.js.map +0 -1
  147. package/dist/unitOfWork/index.d.ts.map +0 -1
  148. package/dist/unitOfWork/index.js.map +0 -1
  149. package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
  150. package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
@@ -1,4 +1,6 @@
1
- import { cloneFilter } from "./queryBuilder.factory.js";
1
+ import { cloneFilter } from "./queryBuilder.filter.js";
2
+ import { toPrismaArgs, } from "./queryBuilder.prisma.js";
3
+ import { includeRelation, } from "../relations/relations.definition.js";
2
4
  /**
3
5
  * Query builder used to construct database-neutral query definitions.
4
6
  *
@@ -11,11 +13,11 @@ export class QueryBuilder {
11
13
  paginationState;
12
14
  sortState = [];
13
15
  selectState = [];
14
- _revision = 0;
16
+ offsetState;
17
+ includeState = [];
15
18
  _cachedBuild;
16
19
  invalidateCache() {
17
20
  this._cachedBuild = undefined;
18
- this._revision++;
19
21
  }
20
22
  /**
21
23
  * Adds an equality condition.
@@ -189,6 +191,34 @@ export class QueryBuilder {
189
191
  this.invalidateCache();
190
192
  return this;
191
193
  }
194
+ /**
195
+ * Sets an explicit row offset. Takes precedence over `page()`.
196
+ */
197
+ offset(offset) {
198
+ if (!Number.isFinite(offset) || offset < 0) {
199
+ throw new TypeError("Query offset must be a non-negative number.");
200
+ }
201
+ this.offsetState = Math.floor(offset);
202
+ this.invalidateCache();
203
+ return this;
204
+ }
205
+ /**
206
+ * Adds relations to include. Accepts relation names or include
207
+ * definitions created with `includeRelation`.
208
+ */
209
+ include(...relations) {
210
+ for (const relation of relations) {
211
+ const include = typeof relation === "string" ? includeRelation(relation) : relation;
212
+ if (!include || typeof include.relation !== "string") {
213
+ throw new TypeError("A relation include is required.");
214
+ }
215
+ if (!this.includeState.some((e) => e.relation === include.relation)) {
216
+ this.includeState.push(include);
217
+ }
218
+ }
219
+ this.invalidateCache();
220
+ return this;
221
+ }
192
222
  /**
193
223
  * Sorts ascending by a field.
194
224
  */
@@ -260,6 +290,27 @@ export class QueryBuilder {
260
290
  this.invalidateCache();
261
291
  return this;
262
292
  }
293
+ /**
294
+ * Clears included relations.
295
+ */
296
+ clearInclude() {
297
+ this.includeState = [];
298
+ this.invalidateCache();
299
+ return this;
300
+ }
301
+ /**
302
+ * Resets the builder to its initial empty state.
303
+ */
304
+ reset() {
305
+ this.filterState = undefined;
306
+ this.paginationState = undefined;
307
+ this.offsetState = undefined;
308
+ this.sortState = [];
309
+ this.selectState = [];
310
+ this.includeState = [];
311
+ this.invalidateCache();
312
+ return this;
313
+ }
263
314
  /**
264
315
  * Returns the immutable query definition.
265
316
  */
@@ -267,32 +318,41 @@ export class QueryBuilder {
267
318
  if (this._cachedBuild) {
268
319
  return this._cachedBuild;
269
320
  }
270
- const result = Object.freeze({
321
+ const result = deepFreeze({
271
322
  filter: cloneFilter(this.filterState),
272
323
  pagination: this.paginationState
273
- ? Object.freeze({
274
- ...this.paginationState,
275
- })
324
+ ? { ...this.paginationState }
276
325
  : undefined,
326
+ offset: this.offsetState,
277
327
  sort: this.sortState.length > 0
278
- ? Object.freeze([...this.sortState])
279
- : undefined,
280
- select: this.selectState.length > 0
281
- ? Object.freeze([...this.selectState])
328
+ ? this.sortState.map((entry) => ({ ...entry }))
282
329
  : undefined,
330
+ select: this.selectState.length > 0 ? [...this.selectState] : undefined,
331
+ include: this.includeState.length > 0 ? [...this.includeState] : undefined,
283
332
  });
284
333
  this._cachedBuild = result;
285
334
  return result;
286
335
  }
287
336
  /**
288
- * Converts the builder into generic query options.
337
+ * Converts the builder into generic query options (pagination, sort and
338
+ * the built filter).
289
339
  */
290
340
  toQueryOptions() {
291
341
  return {
292
- pagination: this.paginationState,
342
+ filter: cloneFilter(this.filterState),
343
+ pagination: this.paginationState
344
+ ? { ...this.paginationState }
345
+ : undefined,
346
+ offset: this.offsetState,
293
347
  sort: this.sortState.length > 0 ? [...this.sortState] : undefined,
294
348
  };
295
349
  }
350
+ /**
351
+ * Translates the builder into Prisma `findMany`-style arguments.
352
+ */
353
+ toPrismaArgs(options) {
354
+ return toPrismaArgs(this.build(), options);
355
+ }
296
356
  /**
297
357
  * Creates an independent copy of the builder.
298
358
  */
@@ -304,8 +364,10 @@ export class QueryBuilder {
304
364
  ...this.paginationState,
305
365
  }
306
366
  : undefined;
307
- builder.sortState = [...this.sortState];
367
+ builder.offsetState = this.offsetState;
368
+ builder.sortState = this.sortState.map((entry) => ({ ...entry }));
308
369
  builder.selectState = [...this.selectState];
370
+ builder.includeState = [...this.includeState];
309
371
  return builder;
310
372
  }
311
373
  /**
@@ -339,4 +401,20 @@ export class QueryBuilder {
339
401
  return this;
340
402
  }
341
403
  }
404
+ /**
405
+ * Recursively freezes plain objects and arrays.
406
+ */
407
+ function deepFreeze(value) {
408
+ if (value === null || typeof value !== "object" || Object.isFrozen(value)) {
409
+ return value;
410
+ }
411
+ if (value instanceof Date) {
412
+ return value;
413
+ }
414
+ Object.freeze(value);
415
+ for (const entry of Object.values(value)) {
416
+ deepFreeze(entry);
417
+ }
418
+ return value;
419
+ }
342
420
  //# sourceMappingURL=queryBuilder.core.js.map
@@ -0,0 +1,6 @@
1
+ import { QueryBuilder } from "./queryBuilder.core.js";
2
+ /**
3
+ * Creates a new query builder.
4
+ */
5
+ export declare function createQueryBuilder<TField extends string = string>(): QueryBuilder<TField>;
6
+ //# sourceMappingURL=queryBuilder.factory.d.ts.map
@@ -5,29 +5,4 @@ import { QueryBuilder } from "./queryBuilder.core.js";
5
5
  export function createQueryBuilder() {
6
6
  return new QueryBuilder();
7
7
  }
8
- /**
9
- * Clones a query filter without sharing mutable arrays.
10
- */
11
- export function cloneFilter(filter) {
12
- if (!filter) {
13
- return undefined;
14
- }
15
- return {
16
- conditions: filter.conditions
17
- ? filter.conditions.map((condition) => ({
18
- ...condition,
19
- ...(Array.isArray(condition.value)
20
- ? {
21
- value: [...condition.value],
22
- }
23
- : {}),
24
- }))
25
- : undefined,
26
- and: filter.and
27
- ? filter.and.map((child) => cloneFilter(child))
28
- : undefined,
29
- or: filter.or ? filter.or.map((child) => cloneFilter(child)) : undefined,
30
- not: filter.not ? cloneFilter(filter.not) : undefined,
31
- };
32
- }
33
8
  //# sourceMappingURL=queryBuilder.factory.js.map
@@ -0,0 +1,170 @@
1
+ import type { QueryCondition, QueryFilter, QueryOperator, RelationOperator } from "./queryBuilder.type.js";
2
+ /**
3
+ * Creates an equality filter.
4
+ */
5
+ export declare function equals(field: string, value: unknown): QueryFilter;
6
+ /**
7
+ * Creates a not-equal filter.
8
+ */
9
+ export declare function notEquals(field: string, value: unknown): QueryFilter;
10
+ /**
11
+ * Creates an IN filter.
12
+ */
13
+ export declare function inList(field: string, values: readonly unknown[]): QueryFilter;
14
+ /**
15
+ * Creates a NOT IN filter.
16
+ */
17
+ export declare function notInList(field: string, values: readonly unknown[]): QueryFilter;
18
+ /**
19
+ * Creates a less-than filter.
20
+ */
21
+ export declare function lessThan(field: string, value: unknown): QueryFilter;
22
+ /**
23
+ * Creates a less-than-or-equal filter.
24
+ */
25
+ export declare function lessThanOrEqual(field: string, value: unknown): QueryFilter;
26
+ /**
27
+ * Creates a greater-than filter.
28
+ */
29
+ export declare function greaterThan(field: string, value: unknown): QueryFilter;
30
+ /**
31
+ * Creates a greater-than-or-equal filter.
32
+ */
33
+ export declare function greaterThanOrEqual(field: string, value: unknown): QueryFilter;
34
+ /**
35
+ * Creates a contains filter.
36
+ */
37
+ export declare function contains(field: string, value: string): QueryFilter;
38
+ /**
39
+ * Creates a starts-with filter.
40
+ */
41
+ export declare function startsWith(field: string, value: string): QueryFilter;
42
+ /**
43
+ * Creates an ends-with filter.
44
+ */
45
+ export declare function endsWith(field: string, value: string): QueryFilter;
46
+ /**
47
+ * Creates an IS NULL filter.
48
+ */
49
+ export declare function isNull(field: string): QueryFilter;
50
+ /**
51
+ * Creates an IS NOT NULL filter.
52
+ */
53
+ export declare function isNotNull(field: string): QueryFilter;
54
+ /**
55
+ * Combines filters using AND.
56
+ */
57
+ export declare function and(...filters: readonly QueryFilter[]): QueryFilter;
58
+ /**
59
+ * Combines filters using OR.
60
+ */
61
+ export declare function or(...filters: readonly QueryFilter[]): QueryFilter;
62
+ /**
63
+ * Negates a filter.
64
+ */
65
+ export declare function not(filter: QueryFilter): QueryFilter;
66
+ /**
67
+ * Creates a generic filter condition.
68
+ */
69
+ export declare function condition(field: string, operator: QueryOperator, value?: unknown): QueryFilter;
70
+ /**
71
+ * Combines multiple filters with AND only when necessary.
72
+ */
73
+ export declare function allOf(filters: readonly QueryFilter[]): QueryFilter;
74
+ /**
75
+ * Combines multiple filters with OR only when necessary.
76
+ */
77
+ export declare function anyOf(filters: readonly QueryFilter[]): QueryFilter;
78
+ /**
79
+ * Creates a filter from a plain object.
80
+ *
81
+ * Every property becomes an equality condition.
82
+ */
83
+ export declare function fromObject<T extends Record<string, unknown>>(values: T): QueryFilter;
84
+ /**
85
+ * Creates filters for a date range.
86
+ */
87
+ export declare function dateRange(field: string, options: {
88
+ readonly from?: Date;
89
+ readonly to?: Date;
90
+ }): QueryFilter;
91
+ /**
92
+ * Creates an inclusive range filter (`field >= from AND field <= to`).
93
+ */
94
+ export declare function between(field: string, from: number | string | Date | bigint, to: number | string | Date | bigint): QueryFilter;
95
+ /**
96
+ * Creates a SQL-style pattern filter using `%` wildcards.
97
+ *
98
+ * Only leading and/or trailing wildcards are supported (`%abc%`, `abc%`,
99
+ * `%abc`); the pattern is translated to `contains`, `startsWith`,
100
+ * `endsWith` or `equals` when converted for an ORM.
101
+ */
102
+ export declare function matchesPattern(field: string, pattern: string): QueryFilter;
103
+ /**
104
+ * Creates a filter matching null or empty-string values.
105
+ */
106
+ export declare function isEmpty(field: string): QueryFilter;
107
+ /**
108
+ * Creates a filter matching values that are neither null nor empty.
109
+ */
110
+ export declare function isNotEmpty(field: string): QueryFilter;
111
+ /**
112
+ * Creates a filter matching any timestamp on the given UTC calendar day.
113
+ */
114
+ export declare function dateOnly(field: string, date: Date): QueryFilter;
115
+ /**
116
+ * Creates a filter for timestamps strictly before a date.
117
+ */
118
+ export declare function isBefore(field: string, date: Date): QueryFilter;
119
+ /**
120
+ * Creates a filter for timestamps strictly after a date.
121
+ */
122
+ export declare function isAfter(field: string, date: Date): QueryFilter;
123
+ /**
124
+ * Creates an inclusive date range filter.
125
+ */
126
+ export declare function isBetween(field: string, from: Date, to: Date): QueryFilter;
127
+ /**
128
+ * Creates a negated single condition.
129
+ */
130
+ export declare function notCondition(field: string, operator: QueryOperator, value?: unknown): QueryFilter;
131
+ /**
132
+ * Creates a filter on a related entity.
133
+ *
134
+ * `some` / `every` / `none` target collection relations; `is` / `isNot`
135
+ * target single relations.
136
+ */
137
+ export declare function relational(relation: string, filter: QueryFilter, operator?: RelationOperator): QueryFilter;
138
+ /**
139
+ * Creates a filter for a value being one of several options.
140
+ */
141
+ export declare function oneOf<T>(field: string, values: readonly T[]): QueryFilter;
142
+ /**
143
+ * Creates a filter for a value not being one of several options.
144
+ */
145
+ export declare function noneOf<T>(field: string, values: readonly T[]): QueryFilter;
146
+ /**
147
+ * Creates an optional equality filter.
148
+ *
149
+ * Returns an empty filter when the value is undefined.
150
+ */
151
+ export declare function optionalEquals(field: string, value: unknown): QueryFilter;
152
+ /**
153
+ * Creates an optional text search filter.
154
+ */
155
+ export declare function optionalContains(field: string, value?: string | null): QueryFilter;
156
+ /**
157
+ * Checks whether a filter contains any actual constraints.
158
+ */
159
+ export declare function hasConditions(filter?: QueryFilter): boolean;
160
+ /**
161
+ * Flattens an AND-only filter into individual conditions.
162
+ */
163
+ export declare function flattenAnd(filter: QueryFilter): QueryCondition[];
164
+ /**
165
+ * Deeply clones a filter (conditions, nested groups, Date/array/object
166
+ * values) so the copy shares no mutable state with the source.
167
+ */
168
+ export declare function cloneFilter(filter: QueryFilter): QueryFilter;
169
+ export declare function cloneFilter(filter?: QueryFilter): QueryFilter | undefined;
170
+ //# sourceMappingURL=queryBuilder.filter.d.ts.map
@@ -1,3 +1,7 @@
1
+ const EMPTY_FILTER = Object.freeze({
2
+ conditions: Object.freeze([]),
3
+ });
4
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
1
5
  /**
2
6
  * Creates an equality filter.
3
7
  */
@@ -81,7 +85,7 @@ export function isNotNull(field) {
81
85
  */
82
86
  export function and(...filters) {
83
87
  return {
84
- and: filters.map(cloneFilter),
88
+ and: filters.map((filter) => cloneFilter(filter)),
85
89
  };
86
90
  }
87
91
  /**
@@ -89,7 +93,7 @@ export function and(...filters) {
89
93
  */
90
94
  export function or(...filters) {
91
95
  return {
92
- or: filters.map(cloneFilter),
96
+ or: filters.map((filter) => cloneFilter(filter)),
93
97
  };
94
98
  }
95
99
  /**
@@ -120,11 +124,9 @@ export function condition(field, operator, value) {
120
124
  * Combines multiple filters with AND only when necessary.
121
125
  */
122
126
  export function allOf(filters) {
123
- const normalized = filters.filter(Boolean).map(cloneFilter);
127
+ const normalized = filters.filter(hasConditions).map((filter) => cloneFilter(filter));
124
128
  if (normalized.length === 0) {
125
- return {
126
- conditions: [],
127
- };
129
+ return EMPTY_FILTER;
128
130
  }
129
131
  if (normalized.length === 1) {
130
132
  return normalized[0];
@@ -137,11 +139,9 @@ export function allOf(filters) {
137
139
  * Combines multiple filters with OR only when necessary.
138
140
  */
139
141
  export function anyOf(filters) {
140
- const normalized = filters.filter(Boolean).map(cloneFilter);
142
+ const normalized = filters.filter(hasConditions).map((filter) => cloneFilter(filter));
141
143
  if (normalized.length === 0) {
142
- return {
143
- conditions: [],
144
- };
144
+ return EMPTY_FILTER;
145
145
  }
146
146
  if (normalized.length === 1) {
147
147
  return normalized[0];
@@ -156,11 +156,16 @@ export function anyOf(filters) {
156
156
  * Every property becomes an equality condition.
157
157
  */
158
158
  export function fromObject(values) {
159
- const conditions = Object.entries(values).map(([field, value]) => ({
160
- field,
161
- operator: "equals",
162
- value: cloneValue(value),
163
- }));
159
+ if (values === null || typeof values !== "object" || Array.isArray(values)) {
160
+ throw new TypeError("fromObject requires a plain object.");
161
+ }
162
+ const conditions = [];
163
+ for (const [field, value] of Object.entries(values)) {
164
+ if (FORBIDDEN_KEYS.has(field)) {
165
+ throw new TypeError(`Invalid filter field "${field}".`);
166
+ }
167
+ conditions.push(...condition(field, "equals", value).conditions);
168
+ }
164
169
  return {
165
170
  conditions,
166
171
  };
@@ -187,20 +192,144 @@ export function dateRange(field, options) {
187
192
  value: new Date(options.to.getTime()),
188
193
  });
189
194
  }
190
- if (conditions.length === 0) {
191
- return {
192
- conditions: [],
193
- };
195
+ if (options.from && options.to && options.from.getTime() > options.to.getTime()) {
196
+ throw new RangeError("dateRange requires `from` to be on or before `to`.");
194
197
  }
195
- if (conditions.length === 1) {
196
- return {
197
- conditions,
198
- };
198
+ return {
199
+ conditions,
200
+ };
201
+ }
202
+ /**
203
+ * Creates an inclusive range filter (`field >= from AND field <= to`).
204
+ */
205
+ export function between(field, from, to) {
206
+ validateField(field);
207
+ validateRangeBound(from, "from");
208
+ validateRangeBound(to, "to");
209
+ if (compareBounds(from, to) > 0) {
210
+ throw new RangeError("between requires `from` to be on or before `to`.");
199
211
  }
200
212
  return {
201
- and: [
213
+ conditions: [
214
+ {
215
+ field,
216
+ operator: "gte",
217
+ value: cloneValue(from),
218
+ },
202
219
  {
203
- conditions,
220
+ field,
221
+ operator: "lte",
222
+ value: cloneValue(to),
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ /**
228
+ * Creates a SQL-style pattern filter using `%` wildcards.
229
+ *
230
+ * Only leading and/or trailing wildcards are supported (`%abc%`, `abc%`,
231
+ * `%abc`); the pattern is translated to `contains`, `startsWith`,
232
+ * `endsWith` or `equals` when converted for an ORM.
233
+ */
234
+ export function matchesPattern(field, pattern) {
235
+ if (typeof pattern !== "string" || pattern.length === 0) {
236
+ throw new TypeError("A pattern is required.");
237
+ }
238
+ // The lookbehind pins the trailing match to the start of a `%` run; plain
239
+ // `%+$` retries from every `%` in an interior run, which is quadratic.
240
+ const inner = pattern.replace(/^%+/, "").replace(/(?<!%)%+$/, "");
241
+ if (inner.length === 0 || inner.includes("%") || inner.includes("_")) {
242
+ throw new TypeError("matchesPattern only supports leading and/or trailing % wildcards.");
243
+ }
244
+ return condition(field, "like", pattern);
245
+ }
246
+ /**
247
+ * Creates a filter matching null or empty-string values.
248
+ */
249
+ export function isEmpty(field) {
250
+ return or(isNull(field), equals(field, ""));
251
+ }
252
+ /**
253
+ * Creates a filter matching values that are neither null nor empty.
254
+ */
255
+ export function isNotEmpty(field) {
256
+ return and(isNotNull(field), notEquals(field, ""));
257
+ }
258
+ /**
259
+ * Creates a filter matching any timestamp on the given UTC calendar day.
260
+ */
261
+ export function dateOnly(field, date) {
262
+ validateField(field);
263
+ validateDate(date, "date");
264
+ const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
265
+ const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
266
+ return {
267
+ conditions: [
268
+ {
269
+ field,
270
+ operator: "gte",
271
+ value: start,
272
+ },
273
+ {
274
+ field,
275
+ operator: "lt",
276
+ value: end,
277
+ },
278
+ ],
279
+ };
280
+ }
281
+ /**
282
+ * Creates a filter for timestamps strictly before a date.
283
+ */
284
+ export function isBefore(field, date) {
285
+ validateDate(date, "date");
286
+ return condition(field, "lt", date);
287
+ }
288
+ /**
289
+ * Creates a filter for timestamps strictly after a date.
290
+ */
291
+ export function isAfter(field, date) {
292
+ validateDate(date, "date");
293
+ return condition(field, "gt", date);
294
+ }
295
+ /**
296
+ * Creates an inclusive date range filter.
297
+ */
298
+ export function isBetween(field, from, to) {
299
+ validateDate(from, "from");
300
+ validateDate(to, "to");
301
+ return between(field, from, to);
302
+ }
303
+ /**
304
+ * Creates a negated single condition.
305
+ */
306
+ export function notCondition(field, operator, value) {
307
+ return not(condition(field, operator, value));
308
+ }
309
+ /**
310
+ * Creates a filter on a related entity.
311
+ *
312
+ * `some` / `every` / `none` target collection relations; `is` / `isNot`
313
+ * target single relations.
314
+ */
315
+ export function relational(relation, filter, operator = "some") {
316
+ if (operator !== "some" &&
317
+ operator !== "every" &&
318
+ operator !== "none" &&
319
+ operator !== "is" &&
320
+ operator !== "isNot") {
321
+ throw new TypeError(`Invalid relation operator "${String(operator)}".`);
322
+ }
323
+ if (!filter || typeof filter !== "object") {
324
+ throw new TypeError("A relation filter is required.");
325
+ }
326
+ validateField(relation);
327
+ return {
328
+ conditions: [
329
+ {
330
+ field: relation,
331
+ operator,
332
+ value: cloneFilter(filter),
204
333
  },
205
334
  ],
206
335
  };
@@ -263,6 +392,9 @@ export function hasConditions(filter) {
263
392
  * Flattens an AND-only filter into individual conditions.
264
393
  */
265
394
  export function flattenAnd(filter) {
395
+ if ((filter.or && filter.or.length > 0) || filter.not) {
396
+ throw new TypeError("flattenAnd only accepts AND-only filters; OR/NOT branches cannot be flattened.");
397
+ }
266
398
  const result = [];
267
399
  if (filter.conditions) {
268
400
  result.push(...filter.conditions.map(cloneCondition));
@@ -272,16 +404,16 @@ export function flattenAnd(filter) {
272
404
  }
273
405
  return result;
274
406
  }
275
- /**
276
- * Creates a new filter without mutating the source.
277
- */
278
407
  export function cloneFilter(filter) {
408
+ if (!filter) {
409
+ return undefined;
410
+ }
279
411
  return {
280
412
  conditions: filter.conditions
281
413
  ? filter.conditions.map(cloneCondition)
282
414
  : undefined,
283
- and: filter.and ? filter.and.map(cloneFilter) : undefined,
284
- or: filter.or ? filter.or.map(cloneFilter) : undefined,
415
+ and: filter.and ? filter.and.map((child) => cloneFilter(child)) : undefined,
416
+ or: filter.or ? filter.or.map((child) => cloneFilter(child)) : undefined,
285
417
  not: filter.not ? cloneFilter(filter.not) : undefined,
286
418
  };
287
419
  }
@@ -311,12 +443,38 @@ function cloneValue(value) {
311
443
  if (value && typeof value === "object") {
312
444
  const result = {};
313
445
  for (const [key, entry] of Object.entries(value)) {
446
+ if (FORBIDDEN_KEYS.has(key)) {
447
+ continue;
448
+ }
314
449
  result[key] = cloneValue(entry);
315
450
  }
316
451
  return result;
317
452
  }
318
453
  return value;
319
454
  }
455
+ function validateRangeBound(value, name) {
456
+ if (value instanceof Date) {
457
+ validateDate(value, name);
458
+ return;
459
+ }
460
+ if (typeof value === "number" && Number.isNaN(value)) {
461
+ throw new TypeError(`Invalid ${name} bound.`);
462
+ }
463
+ if (typeof value !== "number" &&
464
+ typeof value !== "string" &&
465
+ typeof value !== "bigint") {
466
+ throw new TypeError(`Invalid ${name} bound.`);
467
+ }
468
+ }
469
+ function compareBounds(from, to) {
470
+ if (from instanceof Date && to instanceof Date) {
471
+ return from.getTime() - to.getTime();
472
+ }
473
+ if (typeof from === typeof to && from > to) {
474
+ return 1;
475
+ }
476
+ return 0;
477
+ }
320
478
  /**
321
479
  * Validates a filter field.
322
480
  */
@@ -324,6 +482,9 @@ function validateField(field) {
324
482
  if (typeof field !== "string" || field.trim().length === 0) {
325
483
  throw new TypeError("A filter field is required.");
326
484
  }
485
+ if (FORBIDDEN_KEYS.has(field)) {
486
+ throw new TypeError(`Invalid filter field "${field}".`);
487
+ }
327
488
  }
328
489
  /**
329
490
  * Validates a date value.