@web-ts-toolkit/moo 0.43.0 → 0.47.1

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,14 +1,58 @@
1
1
  import { Schema } from 'mongoose';
2
2
 
3
- type QueryFilter<TDocument extends Record<string, unknown> = Record<string, unknown>> = Partial<TDocument> & Record<string, unknown>;
3
+ /**
4
+ * MongoDB query operators accepted for a single document field in
5
+ * {@link QueryFilter}. The runtime forwards relationship and full filters
6
+ * straight to Mongoose `find()`, so operator objects (for example
7
+ * `{ price: { $gt: 5 } }`) are evaluated by MongoDB rather than by this
8
+ * package. Each field therefore accepts either its plain document value or
9
+ * an operator object; top-level logical operators (`$and`/`$or`/`$not`/`$nor`)
10
+ * remain available through the trailing `Record<string, unknown>` index.
11
+ */
12
+ type QueryOperators<TValue = unknown> = {
13
+ $eq?: TValue;
14
+ $ne?: TValue;
15
+ $gt?: TValue;
16
+ $gte?: TValue;
17
+ $lt?: TValue;
18
+ $lte?: TValue;
19
+ $in?: TValue[];
20
+ $nin?: TValue[];
21
+ $exists?: boolean;
22
+ $regex?: string | RegExp;
23
+ $type?: string | string[];
24
+ $not?: QueryOperators<TValue> | RegExp;
25
+ $size?: number;
26
+ $all?: TValue[];
27
+ $elemMatch?: Record<string, unknown>;
28
+ };
29
+ /**
30
+ * Typed dependent-collection filter. Each declared document field accepts its
31
+ * plain value, a {@link QueryOperators} object, or `null`; undeclared paths
32
+ * (including `$and`/`$or` operator payloads composed by
33
+ * `extraForeignFilter`) are accepted through the index signature and passed
34
+ * to Mongoose unchanged.
35
+ */
36
+ type QueryFilter<TDocument extends Record<string, unknown> = Record<string, unknown>> = {
37
+ [K in keyof TDocument]?: TDocument[K] | QueryOperators<TDocument[K]> | null;
38
+ } & Record<string, unknown>;
39
+ /**
40
+ * Dependents grouped by configured model name. The no-argument
41
+ * `findDependents()`/`findOrphans()` overloads return a `Partial` map:
42
+ * entries whose lookup is unsupported (for example `findOrphans()` for a
43
+ * non-`_id` local key, dotted foreign path, dynamic `refPath`, or
44
+ * `foreignFilter`-only relationship) or whose resolver produced a
45
+ * nullish/non-object result are omitted rather than reported as empty
46
+ * arrays, matching the `mergeResults` runtime behavior.
47
+ */
4
48
  type CascadeDeleteDependencyMap<TModelName extends string, TDependentDocument = unknown> = Record<TModelName, TDependentDocument[]>;
5
49
  type CascadeDeleteDocumentMethods<TModelName extends string, TDependentDocument = unknown> = {
6
- findDependents(): Promise<CascadeDeleteDependencyMap<TModelName, TDependentDocument> & Record<string, unknown[]>>;
50
+ findDependents(): Promise<Partial<CascadeDeleteDependencyMap<TModelName, TDependentDocument>> & Record<string, unknown[]>>;
7
51
  findDependents(modelName: TModelName): Promise<TDependentDocument[]>;
8
52
  findDependents(modelName: string): Promise<unknown[] | null>;
9
53
  };
10
54
  type CascadeDeleteModelStatics<TModelName extends string, TDependentDocument = unknown> = {
11
- findOrphans(): Promise<CascadeDeleteDependencyMap<TModelName, TDependentDocument> & Record<string, unknown[]>>;
55
+ findOrphans(): Promise<Partial<CascadeDeleteDependencyMap<TModelName, TDependentDocument>> & Record<string, unknown[]>>;
12
56
  findOrphans(modelName: TModelName): Promise<TDependentDocument[] | null>;
13
57
  findOrphans(modelName: string): Promise<unknown[] | null>;
14
58
  };
@@ -19,7 +63,110 @@ interface CascadeDeletePluginOptions<TModelName extends string = string, TDepend
19
63
  foreignField?: string;
20
64
  foreignFilter?: FilterResolver<TDependentDocument>;
21
65
  extraForeignFilter?: FilterResolver<TDependentDocument>;
66
+ /**
67
+ * Maximum dependent deletes allowed in flight at once for this relationship.
68
+ * Must be an integer >= 1. Defaults to 8. Inside an active transaction the
69
+ * effective limit is forced to 1 (MOO-04 serialization) regardless of this
70
+ * value. Public `findDependents()` is unaffected: it still returns an array.
71
+ */
72
+ maxConcurrency?: number;
73
+ /**
74
+ * Maximum dependent `_id`s acquired per internal ID-page query. Must be an
75
+ * integer >= 1. Defaults to 100. Bounds acquisition memory; deletion work is
76
+ * never all materialized or scheduled up front.
77
+ */
78
+ batchSize?: number;
22
79
  }
80
+ /**
81
+ * Cascades document `deleteOne()` to configured dependent models.
82
+ *
83
+ * Supported operations: only document `deleteOne()` (and legacy document
84
+ * `remove()` for dependents) is intercepted, through
85
+ * `pre('deleteOne', { document: true, query: false })` and
86
+ * `post('deleteOne', { document: true, query: false })`. Query helpers such
87
+ * as `deleteMany()`, `findOneAndDelete()`, `updateOne()`, and query
88
+ * `deleteOne()` bypass the cascade entirely. `findDependents()` is a
89
+ * document method; `findOrphans()` is a model static with the support matrix
90
+ * documented on {@link CascadeDeleteDependencyMap} (unsupported shapes
91
+ * resolve to `null` and are omitted from the no-argument map).
92
+ *
93
+ * Post-delete failure timing: the `post('deleteOne')` hook runs after the
94
+ * parent document is already removed. A later dependent failure therefore
95
+ * rejects the parent `deleteOne()` promise without restoring the parent —
96
+ * see the traversal contract below for the transactional abort path.
97
+ *
98
+ * Supported behavior: dependent models resolve through the owning model's
99
+ * connection (duplicate names on other connections stay isolated); the
100
+ * effective session (explicit `deleteOne({ session })` option, else the
101
+ * document-bound `$session()`) flows through dependent reads and deletes,
102
+ * including nested cascades; inside an active transaction dependent deletes run
103
+ * serially on that session so replica-set commit/abort covers parent and
104
+ * dependents together. Unsupported modes fail before the parent is removed: a
105
+ * session from a different connection/client, or an active transaction on a
106
+ * standalone (`Single`) topology.
107
+ *
108
+ * Predicate fail-closed contract (MOO-05):
109
+ *
110
+ * - Relationship mode (`localField` + `foreignField`) never issues a
111
+ * destructive query without a valid relationship. Missing/`null`/empty
112
+ * local keys (including empty reference arrays and arrays containing only
113
+ * nullish entries) resolve to zero dependents and delete nothing, so
114
+ * unrelated records with a missing/`null` foreign key survive. A local field
115
+ * that was omitted by a projection is distinguished from an intentionally
116
+ * empty relation: when the path is provably deselected (`$isSelected()`
117
+ * returns `false`) the lookup throws instead of silently reporting zero
118
+ * dependents.
119
+ * - Supplemental `extraForeignFilter` constraints are composed conjunctively
120
+ * (`{ $and: [relationship, extra] }`). An extra filter on the relationship
121
+ * field therefore intersects instead of replacing or widening the deletion
122
+ * set; `$or`/`$and`/operator payloads inside the extra filter stay
123
+ * constrained by the relationship predicate. An absent, nullish,
124
+ * non-object, or empty extra filter leaves the relationship unchanged.
125
+ * - Explicit full-filter mode (`foreignFilter`) is a separately documented
126
+ * contract: the resolved filter is used as-is with no relationship
127
+ * predicate, and any configured `extraForeignFilter` is ignored. A resolver
128
+ * returning `null`/`undefined`/non-object resolves to zero dependents.
129
+ * Maintainer decision: an intentionally empty full filter (`{}`) is rejected
130
+ * by default — statically at `schema.plugin(...)` time and dynamically when
131
+ * a resolver returns `{}` — because it would match the whole dependent
132
+ * collection. There is currently no broad-delete opt-in flag.
133
+ * - Option validation runs at `schema.plugin(...)` time: `model` must be a
134
+ * non-empty string, relationship mode requires both `localField` and
135
+ * `foreignField`, static `foreignFilter`/`extraForeignFilter` values must be
136
+ * plain objects (or resolver functions), and a static empty `foreignFilter`
137
+ * throws. Invalid runtime resolver results fail closed as described above.
138
+ *
139
+ * Traversal/bounds contract (MOO-06):
140
+ *
141
+ * - Deletion never uses bulk writes: every dependent is removed through its
142
+ * own hydrated document `deleteOne()` (or legacy `remove()`), so nested
143
+ * cascade middleware and custom document hooks always run with full field
144
+ * state. Public `findDependents()` keeps its array-returning API for
145
+ * convenience; internal deletion traversal pages `_id`s (`batchSize`,
146
+ * default 100) and loads full documents per page instead.
147
+ * - In-flight deletes are bounded by `maxConcurrency` (default 8, integer
148
+ * >= 1). Inside an active transaction the effective limit is forced to 1 to
149
+ * preserve MOO-04 serialization. No new deletes start after the first
150
+ * failure (fail-fast).
151
+ * - Partial failure: `post('deleteOne')` runs after the parent is already
152
+ * removed. Outside a transaction a dependent failure rejects the parent
153
+ * `deleteOne()` promise but cannot restore the parent or already-deleted
154
+ * dependents; remaining batches are never fetched. Inside a transaction the
155
+ * caller observes the rejection before commit and can abort to restore
156
+ * parent and dependents together. A dependent row that is already gone when
157
+ * re-loaded (concurrent diamond, delayed retry) is skipped, not treated as
158
+ * a failure.
159
+ * - Repeated references to the same `_id` within one relationship delete that
160
+ * row once per level (deduplicated by `_id`). Diamonds (two middles sharing
161
+ * one leaf) and cycles (A <-> B) always terminate: deletions are monotonic
162
+ * (deleted rows never reappear in later pages/levels) and every level
163
+ * re-queries live state, so already-deleted parents resolve to zero further
164
+ * dependents. Cross-level diamonds run leaf hooks at least once; concurrent
165
+ * copies may each fire leaf middleware before either observes the other's
166
+ * delete (at-least-once, never suppressed via bulk paths). No performance
167
+ * claim beyond the measured fan-out experiment recorded with the tests is
168
+ * made here.
169
+ */
23
170
  declare function cascadeDeletePlugin<TModelName extends string = string, TDependentDocument extends Record<string, unknown> = Record<string, unknown>>(schema: Schema, options: CascadeDeletePluginOptions<TModelName, TDependentDocument>): void;
24
171
 
25
- export { type CascadeDeleteDependencyMap, type CascadeDeleteDocumentMethods, type CascadeDeleteModelStatics, type CascadeDeletePluginOptions, type FilterResolver, type QueryFilter, cascadeDeletePlugin };
172
+ export { type CascadeDeleteDependencyMap, type CascadeDeleteDocumentMethods, type CascadeDeleteModelStatics, type CascadeDeletePluginOptions, type FilterResolver, type QueryFilter, type QueryOperators, cascadeDeletePlugin };
@@ -1,14 +1,58 @@
1
1
  import { Schema } from 'mongoose';
2
2
 
3
- type QueryFilter<TDocument extends Record<string, unknown> = Record<string, unknown>> = Partial<TDocument> & Record<string, unknown>;
3
+ /**
4
+ * MongoDB query operators accepted for a single document field in
5
+ * {@link QueryFilter}. The runtime forwards relationship and full filters
6
+ * straight to Mongoose `find()`, so operator objects (for example
7
+ * `{ price: { $gt: 5 } }`) are evaluated by MongoDB rather than by this
8
+ * package. Each field therefore accepts either its plain document value or
9
+ * an operator object; top-level logical operators (`$and`/`$or`/`$not`/`$nor`)
10
+ * remain available through the trailing `Record<string, unknown>` index.
11
+ */
12
+ type QueryOperators<TValue = unknown> = {
13
+ $eq?: TValue;
14
+ $ne?: TValue;
15
+ $gt?: TValue;
16
+ $gte?: TValue;
17
+ $lt?: TValue;
18
+ $lte?: TValue;
19
+ $in?: TValue[];
20
+ $nin?: TValue[];
21
+ $exists?: boolean;
22
+ $regex?: string | RegExp;
23
+ $type?: string | string[];
24
+ $not?: QueryOperators<TValue> | RegExp;
25
+ $size?: number;
26
+ $all?: TValue[];
27
+ $elemMatch?: Record<string, unknown>;
28
+ };
29
+ /**
30
+ * Typed dependent-collection filter. Each declared document field accepts its
31
+ * plain value, a {@link QueryOperators} object, or `null`; undeclared paths
32
+ * (including `$and`/`$or` operator payloads composed by
33
+ * `extraForeignFilter`) are accepted through the index signature and passed
34
+ * to Mongoose unchanged.
35
+ */
36
+ type QueryFilter<TDocument extends Record<string, unknown> = Record<string, unknown>> = {
37
+ [K in keyof TDocument]?: TDocument[K] | QueryOperators<TDocument[K]> | null;
38
+ } & Record<string, unknown>;
39
+ /**
40
+ * Dependents grouped by configured model name. The no-argument
41
+ * `findDependents()`/`findOrphans()` overloads return a `Partial` map:
42
+ * entries whose lookup is unsupported (for example `findOrphans()` for a
43
+ * non-`_id` local key, dotted foreign path, dynamic `refPath`, or
44
+ * `foreignFilter`-only relationship) or whose resolver produced a
45
+ * nullish/non-object result are omitted rather than reported as empty
46
+ * arrays, matching the `mergeResults` runtime behavior.
47
+ */
4
48
  type CascadeDeleteDependencyMap<TModelName extends string, TDependentDocument = unknown> = Record<TModelName, TDependentDocument[]>;
5
49
  type CascadeDeleteDocumentMethods<TModelName extends string, TDependentDocument = unknown> = {
6
- findDependents(): Promise<CascadeDeleteDependencyMap<TModelName, TDependentDocument> & Record<string, unknown[]>>;
50
+ findDependents(): Promise<Partial<CascadeDeleteDependencyMap<TModelName, TDependentDocument>> & Record<string, unknown[]>>;
7
51
  findDependents(modelName: TModelName): Promise<TDependentDocument[]>;
8
52
  findDependents(modelName: string): Promise<unknown[] | null>;
9
53
  };
10
54
  type CascadeDeleteModelStatics<TModelName extends string, TDependentDocument = unknown> = {
11
- findOrphans(): Promise<CascadeDeleteDependencyMap<TModelName, TDependentDocument> & Record<string, unknown[]>>;
55
+ findOrphans(): Promise<Partial<CascadeDeleteDependencyMap<TModelName, TDependentDocument>> & Record<string, unknown[]>>;
12
56
  findOrphans(modelName: TModelName): Promise<TDependentDocument[] | null>;
13
57
  findOrphans(modelName: string): Promise<unknown[] | null>;
14
58
  };
@@ -19,7 +63,110 @@ interface CascadeDeletePluginOptions<TModelName extends string = string, TDepend
19
63
  foreignField?: string;
20
64
  foreignFilter?: FilterResolver<TDependentDocument>;
21
65
  extraForeignFilter?: FilterResolver<TDependentDocument>;
66
+ /**
67
+ * Maximum dependent deletes allowed in flight at once for this relationship.
68
+ * Must be an integer >= 1. Defaults to 8. Inside an active transaction the
69
+ * effective limit is forced to 1 (MOO-04 serialization) regardless of this
70
+ * value. Public `findDependents()` is unaffected: it still returns an array.
71
+ */
72
+ maxConcurrency?: number;
73
+ /**
74
+ * Maximum dependent `_id`s acquired per internal ID-page query. Must be an
75
+ * integer >= 1. Defaults to 100. Bounds acquisition memory; deletion work is
76
+ * never all materialized or scheduled up front.
77
+ */
78
+ batchSize?: number;
22
79
  }
80
+ /**
81
+ * Cascades document `deleteOne()` to configured dependent models.
82
+ *
83
+ * Supported operations: only document `deleteOne()` (and legacy document
84
+ * `remove()` for dependents) is intercepted, through
85
+ * `pre('deleteOne', { document: true, query: false })` and
86
+ * `post('deleteOne', { document: true, query: false })`. Query helpers such
87
+ * as `deleteMany()`, `findOneAndDelete()`, `updateOne()`, and query
88
+ * `deleteOne()` bypass the cascade entirely. `findDependents()` is a
89
+ * document method; `findOrphans()` is a model static with the support matrix
90
+ * documented on {@link CascadeDeleteDependencyMap} (unsupported shapes
91
+ * resolve to `null` and are omitted from the no-argument map).
92
+ *
93
+ * Post-delete failure timing: the `post('deleteOne')` hook runs after the
94
+ * parent document is already removed. A later dependent failure therefore
95
+ * rejects the parent `deleteOne()` promise without restoring the parent —
96
+ * see the traversal contract below for the transactional abort path.
97
+ *
98
+ * Supported behavior: dependent models resolve through the owning model's
99
+ * connection (duplicate names on other connections stay isolated); the
100
+ * effective session (explicit `deleteOne({ session })` option, else the
101
+ * document-bound `$session()`) flows through dependent reads and deletes,
102
+ * including nested cascades; inside an active transaction dependent deletes run
103
+ * serially on that session so replica-set commit/abort covers parent and
104
+ * dependents together. Unsupported modes fail before the parent is removed: a
105
+ * session from a different connection/client, or an active transaction on a
106
+ * standalone (`Single`) topology.
107
+ *
108
+ * Predicate fail-closed contract (MOO-05):
109
+ *
110
+ * - Relationship mode (`localField` + `foreignField`) never issues a
111
+ * destructive query without a valid relationship. Missing/`null`/empty
112
+ * local keys (including empty reference arrays and arrays containing only
113
+ * nullish entries) resolve to zero dependents and delete nothing, so
114
+ * unrelated records with a missing/`null` foreign key survive. A local field
115
+ * that was omitted by a projection is distinguished from an intentionally
116
+ * empty relation: when the path is provably deselected (`$isSelected()`
117
+ * returns `false`) the lookup throws instead of silently reporting zero
118
+ * dependents.
119
+ * - Supplemental `extraForeignFilter` constraints are composed conjunctively
120
+ * (`{ $and: [relationship, extra] }`). An extra filter on the relationship
121
+ * field therefore intersects instead of replacing or widening the deletion
122
+ * set; `$or`/`$and`/operator payloads inside the extra filter stay
123
+ * constrained by the relationship predicate. An absent, nullish,
124
+ * non-object, or empty extra filter leaves the relationship unchanged.
125
+ * - Explicit full-filter mode (`foreignFilter`) is a separately documented
126
+ * contract: the resolved filter is used as-is with no relationship
127
+ * predicate, and any configured `extraForeignFilter` is ignored. A resolver
128
+ * returning `null`/`undefined`/non-object resolves to zero dependents.
129
+ * Maintainer decision: an intentionally empty full filter (`{}`) is rejected
130
+ * by default — statically at `schema.plugin(...)` time and dynamically when
131
+ * a resolver returns `{}` — because it would match the whole dependent
132
+ * collection. There is currently no broad-delete opt-in flag.
133
+ * - Option validation runs at `schema.plugin(...)` time: `model` must be a
134
+ * non-empty string, relationship mode requires both `localField` and
135
+ * `foreignField`, static `foreignFilter`/`extraForeignFilter` values must be
136
+ * plain objects (or resolver functions), and a static empty `foreignFilter`
137
+ * throws. Invalid runtime resolver results fail closed as described above.
138
+ *
139
+ * Traversal/bounds contract (MOO-06):
140
+ *
141
+ * - Deletion never uses bulk writes: every dependent is removed through its
142
+ * own hydrated document `deleteOne()` (or legacy `remove()`), so nested
143
+ * cascade middleware and custom document hooks always run with full field
144
+ * state. Public `findDependents()` keeps its array-returning API for
145
+ * convenience; internal deletion traversal pages `_id`s (`batchSize`,
146
+ * default 100) and loads full documents per page instead.
147
+ * - In-flight deletes are bounded by `maxConcurrency` (default 8, integer
148
+ * >= 1). Inside an active transaction the effective limit is forced to 1 to
149
+ * preserve MOO-04 serialization. No new deletes start after the first
150
+ * failure (fail-fast).
151
+ * - Partial failure: `post('deleteOne')` runs after the parent is already
152
+ * removed. Outside a transaction a dependent failure rejects the parent
153
+ * `deleteOne()` promise but cannot restore the parent or already-deleted
154
+ * dependents; remaining batches are never fetched. Inside a transaction the
155
+ * caller observes the rejection before commit and can abort to restore
156
+ * parent and dependents together. A dependent row that is already gone when
157
+ * re-loaded (concurrent diamond, delayed retry) is skipped, not treated as
158
+ * a failure.
159
+ * - Repeated references to the same `_id` within one relationship delete that
160
+ * row once per level (deduplicated by `_id`). Diamonds (two middles sharing
161
+ * one leaf) and cycles (A <-> B) always terminate: deletions are monotonic
162
+ * (deleted rows never reappear in later pages/levels) and every level
163
+ * re-queries live state, so already-deleted parents resolve to zero further
164
+ * dependents. Cross-level diamonds run leaf hooks at least once; concurrent
165
+ * copies may each fire leaf middleware before either observes the other's
166
+ * delete (at-least-once, never suppressed via bulk paths). No performance
167
+ * claim beyond the measured fan-out experiment recorded with the tests is
168
+ * made here.
169
+ */
23
170
  declare function cascadeDeletePlugin<TModelName extends string = string, TDependentDocument extends Record<string, unknown> = Record<string, unknown>>(schema: Schema, options: CascadeDeletePluginOptions<TModelName, TDependentDocument>): void;
24
171
 
25
- export { type CascadeDeleteDependencyMap, type CascadeDeleteDocumentMethods, type CascadeDeleteModelStatics, type CascadeDeletePluginOptions, type FilterResolver, type QueryFilter, cascadeDeletePlugin };
172
+ export { type CascadeDeleteDependencyMap, type CascadeDeleteDocumentMethods, type CascadeDeleteModelStatics, type CascadeDeletePluginOptions, type FilterResolver, type QueryFilter, type QueryOperators, cascadeDeletePlugin };