@web-ts-toolkit/moo 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/index.d.mts +1 -0
- package/index.d.ts +1 -0
- package/index.js +343 -2
- package/index.mjs +340 -2
- package/llms.txt +40 -1
- package/package.json +8 -2
- package/plugins/derived-fields.d.mts +108 -0
- package/plugins/derived-fields.d.ts +108 -0
- package/plugins/derived-fields.js +368 -0
- package/plugins/derived-fields.mjs +339 -0
- package/plugins/index.d.mts +1 -0
- package/plugins/index.d.ts +1 -0
- package/plugins/index.js +341 -0
- package/plugins/index.mjs +338 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Peer dependencies:
|
|
|
20
20
|
- model-function plugin
|
|
21
21
|
- new-document plugin
|
|
22
22
|
- cascade-delete plugin
|
|
23
|
+
- persisted derived fields for ordinary MongoDB queries and indexes
|
|
23
24
|
- optional Keycloak user-sync plugin through a dedicated subpath
|
|
24
25
|
|
|
25
26
|
## Quick Start
|
|
@@ -49,6 +50,7 @@ Subpath entrypoints:
|
|
|
49
50
|
- `@web-ts-toolkit/moo/utils` — `isSchema`, `isObjectIdType`, `isReference`, and the `ReferenceShape` type
|
|
50
51
|
- `@web-ts-toolkit/moo/plugins` — plugin entrypoint
|
|
51
52
|
- `@web-ts-toolkit/moo/plugins/cascade-delete` — cascade-delete plugin
|
|
53
|
+
- `@web-ts-toolkit/moo/plugins/derived-fields` — `derivedFieldsPlugin`, `defineDerivedField`, dependency error and authoring types
|
|
52
54
|
- `@web-ts-toolkit/moo/plugins/model-function` — model-function plugin
|
|
53
55
|
- `@web-ts-toolkit/moo/plugins/new-document` — new-document plugin
|
|
54
56
|
- `@web-ts-toolkit/moo/plugins/keycloak-user-sync` — Keycloak user-sync plugin
|
|
@@ -73,6 +75,174 @@ userSchema.plugin(cascadeDeletePlugin, {
|
|
|
73
75
|
});
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
## Persisted Derived Fields
|
|
79
|
+
|
|
80
|
+
Use `derived` on a stored schema path to compute a value during async document
|
|
81
|
+
validation and persist it with hydrated `save()` / `Model.create()`. The stored
|
|
82
|
+
value supports ordinary MongoDB filtering, searching, sorting, and indexes; reads
|
|
83
|
+
and property access do not compute or write anything.
|
|
84
|
+
|
|
85
|
+
Named root imports are canonical. `defineDerivedField<User, 'fullName'>` checks
|
|
86
|
+
dependency names, a readonly partial raw-model input, and the target's result type
|
|
87
|
+
without globally augmenting Mongoose:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import mongoose, { Schema } from 'mongoose';
|
|
91
|
+
import { defineDerivedField, derivedFieldsPlugin } from '@web-ts-toolkit/moo';
|
|
92
|
+
|
|
93
|
+
type User = { firstName: string; lastName: string; fullName: string };
|
|
94
|
+
|
|
95
|
+
const userSchema = new Schema<User>(
|
|
96
|
+
{
|
|
97
|
+
firstName: { type: String, required: true },
|
|
98
|
+
lastName: { type: String, required: true },
|
|
99
|
+
fullName: {
|
|
100
|
+
type: String,
|
|
101
|
+
required: true,
|
|
102
|
+
index: true,
|
|
103
|
+
derived: defineDerivedField<User, 'fullName'>({
|
|
104
|
+
dependsOn: ['firstName', 'lastName'],
|
|
105
|
+
get: (doc) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim(),
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{ optimisticConcurrency: true },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Define all paths and source-mutating middleware before registering the plugin.
|
|
113
|
+
userSchema.plugin(derivedFieldsPlugin);
|
|
114
|
+
const User = mongoose.model<User>('User', userSchema);
|
|
115
|
+
|
|
116
|
+
await mongoose.connect('mongodb://127.0.0.1:27017/app');
|
|
117
|
+
await User.create({ firstName: 'Ada', lastName: 'Lovelace' });
|
|
118
|
+
const matches = await User.find({ fullName: /^Ada/ }).sort({ fullName: 1 });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The same API (including types) is available from the grouped plugins entrypoint
|
|
122
|
+
and the direct entrypoint:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import {
|
|
126
|
+
defineDerivedField,
|
|
127
|
+
derivedFieldsPlugin,
|
|
128
|
+
DerivedFieldsDependencyError,
|
|
129
|
+
type DerivedFieldDescriptor,
|
|
130
|
+
type DerivedFieldGetter,
|
|
131
|
+
type DerivedFieldInput,
|
|
132
|
+
} from '@web-ts-toolkit/moo/plugins/derived-fields';
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Dependencies and projections
|
|
136
|
+
|
|
137
|
+
Declare every source read by `get` in `dependsOn`. Existing documents request
|
|
138
|
+
recomputation for a dependency modification or explicit target change, including
|
|
139
|
+
actual pending default writes to either:
|
|
140
|
+
|
|
141
|
+
| Recompute requested? | All dependencies available? | Result |
|
|
142
|
+
| -------------------- | --------------------------- | ----------------------------------------------------------------- |
|
|
143
|
+
| No | Either | Skip and preserve the persisted derived value. |
|
|
144
|
+
| Yes | Yes | Recompute and save with the source changes. |
|
|
145
|
+
| Yes | No | Reject before persistence, without invoking any scheduled getter. |
|
|
146
|
+
|
|
147
|
+
Mongoose can queue a non-null default for persistence on an existing document
|
|
148
|
+
without marking the path modified. These pending writes count as changes, including
|
|
149
|
+
`''`, `0`, and `false`. Defaults of `null`/`undefined` alone do not queue writes and
|
|
150
|
+
do not request computation. A target's pending fallback default is recomputed rather
|
|
151
|
+
than accepted as an arbitrary override. Mere target absence/projection omission,
|
|
152
|
+
without a pending write, does not request repair.
|
|
153
|
+
|
|
154
|
+
New documents compute using supplied/defaulted inputs before ordinary validation.
|
|
155
|
+
`dependsOn: []` is valid for creation and target-change/default-write constants.
|
|
156
|
+
|
|
157
|
+
**Available** means selected by the query or fully replaced on the hydrated
|
|
158
|
+
document. Selected defaults are available; selected genuine absence without a
|
|
159
|
+
default is `undefined`, and stored `null` is also a known value. An unselected field
|
|
160
|
+
is unavailable even if a default or property getter could manufacture a value. Assigning an
|
|
161
|
+
excluded dependency explicitly (including `null`, `undefined`, or its default
|
|
162
|
+
value) supplies a replacement; that availability is retained on the same document
|
|
163
|
+
across saves. Child-only modifications and relative `$inc()` operations do not
|
|
164
|
+
prove an excluded input fully available. Fetch the needed fields yourself or
|
|
165
|
+
explicitly replace them; the plugin never fetches omitted inputs. Applying defaults
|
|
166
|
+
to excluded dependencies may queue writes, but does not supply replacements: an
|
|
167
|
+
otherwise unrelated save then rejects before getters or persistence rather than
|
|
168
|
+
writing manufactured sources with stale results.
|
|
169
|
+
|
|
170
|
+
A newly inserted document retains availability of its supplied, defaulted, and
|
|
171
|
+
absent inputs across later saves, including when constructed with projection
|
|
172
|
+
metadata.
|
|
173
|
+
|
|
174
|
+
A bare function (`derived: defineDerivedField<User, 'fullName'>(get)`) is supported
|
|
175
|
+
as conservative shorthand. It recomputes on every async validation/save operation
|
|
176
|
+
and requires **every supported non-derived input** to be available, including
|
|
177
|
+
hidden paths and `_id`, even if the function only reads one field. Prefer explicit
|
|
178
|
+
descriptors for projection-friendly updates.
|
|
179
|
+
|
|
180
|
+
### Getter inputs, results, and errors
|
|
181
|
+
|
|
182
|
+
Getters may be sync or async; return `undefined` to clear a stored target. Each
|
|
183
|
+
receives an isolated frozen plain input view with available supported scalars
|
|
184
|
+
only, no document methods/`this`, virtuals, derived targets, managed version key, or
|
|
185
|
+
containers. References are persisted ids rather than populated documents;
|
|
186
|
+
Date/ObjectId/Decimal128 values
|
|
187
|
+
are copied per invocation. Property getters, transforms, and automatic database
|
|
188
|
+
fetches are bypassed. Results are staged before assignment through normal Mongoose
|
|
189
|
+
setters/casting. Getters and validators must not mutate the hydrated document via
|
|
190
|
+
captured references; keep computations deterministic and declare their real inputs.
|
|
191
|
+
|
|
192
|
+
Missing-input preflight aggregates every affected target before any getter runs.
|
|
193
|
+
`DerivedFieldsDependencyError.code` is
|
|
194
|
+
`MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES`, with frozen path-only
|
|
195
|
+
`issues: [{ targetPath, missingPaths }]` (no source values). The code is a stable
|
|
196
|
+
discriminator across separately bundled entrypoints. Getter failures propagate;
|
|
197
|
+
target casting and ordinary validation failures use Mongoose errors. Rejected saves
|
|
198
|
+
do not persist that save, but retain application assignments in memory; no
|
|
199
|
+
in-memory rollback is promised.
|
|
200
|
+
|
|
201
|
+
### Lifecycle and write boundaries
|
|
202
|
+
|
|
203
|
+
- Register source-mutating pre-validation and pre-save hooks **before** this
|
|
204
|
+
plugin, then compile the model. Normal async validation sees the computed/cast
|
|
205
|
+
derived results. Earlier pre-save source/target changes after initial validation
|
|
206
|
+
trigger recomputation and revalidation (preserving `validateModifiedOnly`). A
|
|
207
|
+
normal save reuses its validation computation when inputs/results are unchanged;
|
|
208
|
+
separate `validate()` calls/saves are separate operations.
|
|
209
|
+
- Later pre-save mutations are outside the guarantee. Later validation-hook or
|
|
210
|
+
validator mutations are not a standalone-validation guarantee; save attempts one
|
|
211
|
+
revalidation and rejects an ordering error if inputs/results remain unstable.
|
|
212
|
+
Timestamp sources assigned in earlier pre-save hooks are covered, but a new
|
|
213
|
+
document's first validation can see an unset timestamp. Ordinary Mongoose change
|
|
214
|
+
tracking still applies (for example, mark in-place Date mutations modified).
|
|
215
|
+
- With schema-level or per-save `validateBeforeSave: false`, the plugin still
|
|
216
|
+
preflights, computes, and casts, but skips ordinary required/custom validators.
|
|
217
|
+
Dependency, getter, and target casting errors still reject before writing.
|
|
218
|
+
- Query updates/replacements and raw-driver writes bypass computation.
|
|
219
|
+
`insertMany()` and bulk insert paths may incidentally invoke async document
|
|
220
|
+
validation, but are not guaranteed all-writer support; lean/skip-validation paths
|
|
221
|
+
and bulk updates can bypass it. `validateSync()`, middleware bypass, and restricted
|
|
222
|
+
`pathsToSave` are outside this contract.
|
|
223
|
+
- Changing a formula or adopting the plugin for old rows requires an explicit
|
|
224
|
+
backfill/repair. Load all dependencies, `doc.markModified('fullName')`, then
|
|
225
|
+
`await doc.save()` to request recomputation even if sources are unchanged. This
|
|
226
|
+
is not an automatic read-time repair. Multiple writers retain ordinary Mongoose
|
|
227
|
+
concurrency semantics: use `optimisticConcurrency` where the source/result
|
|
228
|
+
invariant matters and retry version conflicts by reloading complete inputs.
|
|
229
|
+
|
|
230
|
+
### Initial scope
|
|
231
|
+
|
|
232
|
+
Targets and dependencies must be root persisted **String, Number, Boolean, Date,
|
|
233
|
+
ObjectId, Decimal128, or BigInt** paths. Nested paths, subdocuments, arrays, maps,
|
|
234
|
+
Mixed, Union, Buffer/Binary/UUID, BSON Double/Int32, and unknown custom scalar kinds
|
|
235
|
+
are unsupported as targets/dependencies. Unsupported ordinary fields may coexist
|
|
236
|
+
but are absent from getter views. Self/derived-to-derived dependencies, unknown or
|
|
237
|
+
duplicate dependency names, malformed options, virtual/immutable targets, and
|
|
238
|
+
duplicate plugin registration throw at `schema.plugin(...)` time. Paths and
|
|
239
|
+
definitions are snapshotted at registration; later schema/option edits are not
|
|
240
|
+
discovered. Mongoose's managed `versionKey` (default `__v` or a custom name) is
|
|
241
|
+
rejected as a target or explicit dependency and excluded from all getter views,
|
|
242
|
+
including shorthand: Mongoose initializes/increments it after pre-save middleware.
|
|
243
|
+
With `versionKey: false`, explicitly declared version-like fields are ordinary
|
|
244
|
+
supported scalar targets/inputs.
|
|
245
|
+
|
|
76
246
|
## Model Function Plugin
|
|
77
247
|
|
|
78
248
|
```ts
|
package/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ModelDocument, ModelFunctionInstanceMethods, ModelFunctionPluginOptions, ModelFunctionStaticMethods, modelFunctionPlugin } from './plugins/model-function.mjs';
|
|
2
2
|
export { CascadeDeleteDependencyMap, CascadeDeleteDocumentMethods, CascadeDeleteModelStatics, CascadeDeletePluginOptions, FilterResolver, QueryFilter, QueryOperators, cascadeDeletePlugin } from './plugins/cascade-delete.mjs';
|
|
3
|
+
export { DerivedFieldDefinition, DerivedFieldDependencyIssue, DerivedFieldDescriptor, DerivedFieldGetter, DerivedFieldInput, DerivedFieldsDependencyError, defineDerivedField, derivedFieldsPlugin } from './plugins/derived-fields.mjs';
|
|
3
4
|
export { NewDocumentPluginOptions, newDocumentPlugin } from './plugins/new-document.mjs';
|
|
4
5
|
export { SchemaOptionOverrides, uniqueEmptiableString, uniqueNullableString } from './schema.mjs';
|
|
5
6
|
export { isObjectId } from './is.mjs';
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ModelDocument, ModelFunctionInstanceMethods, ModelFunctionPluginOptions, ModelFunctionStaticMethods, modelFunctionPlugin } from './plugins/model-function.js';
|
|
2
2
|
export { CascadeDeleteDependencyMap, CascadeDeleteDocumentMethods, CascadeDeleteModelStatics, CascadeDeletePluginOptions, FilterResolver, QueryFilter, QueryOperators, cascadeDeletePlugin } from './plugins/cascade-delete.js';
|
|
3
|
+
export { DerivedFieldDefinition, DerivedFieldDependencyIssue, DerivedFieldDescriptor, DerivedFieldGetter, DerivedFieldInput, DerivedFieldsDependencyError, defineDerivedField, derivedFieldsPlugin } from './plugins/derived-fields.js';
|
|
3
4
|
export { NewDocumentPluginOptions, newDocumentPlugin } from './plugins/new-document.js';
|
|
4
5
|
export { SchemaOptionOverrides, uniqueEmptiableString, uniqueNullableString } from './schema.js';
|
|
5
6
|
export { isObjectId } from './is.js';
|
package/index.js
CHANGED
|
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
DerivedFieldsDependencyError: () => DerivedFieldsDependencyError,
|
|
33
34
|
cascadeDeletePlugin: () => cascadeDeletePlugin,
|
|
35
|
+
defineDerivedField: () => defineDerivedField,
|
|
36
|
+
derivedFieldsPlugin: () => derivedFieldsPlugin,
|
|
34
37
|
isObjectId: () => isObjectId,
|
|
35
38
|
modelFunctionPlugin: () => modelFunctionPlugin,
|
|
36
39
|
newDocumentPlugin: () => newDocumentPlugin,
|
|
@@ -521,6 +524,341 @@ function cascadeDeletePlugin(schema, options) {
|
|
|
521
524
|
});
|
|
522
525
|
}
|
|
523
526
|
|
|
527
|
+
// src/plugins/derived-fields/definitions.ts
|
|
528
|
+
function defineDerivedField(definition) {
|
|
529
|
+
return definition;
|
|
530
|
+
}
|
|
531
|
+
var DerivedFieldsDependencyError = class extends Error {
|
|
532
|
+
constructor(issues) {
|
|
533
|
+
const snapshot = Object.freeze(
|
|
534
|
+
issues.map(
|
|
535
|
+
({ targetPath, missingPaths }) => Object.freeze({ targetPath, missingPaths: Object.freeze([...missingPaths]) })
|
|
536
|
+
)
|
|
537
|
+
);
|
|
538
|
+
const details = snapshot.map(
|
|
539
|
+
({ targetPath, missingPaths }) => `${JSON.stringify(targetPath)} requires ${missingPaths.map((path) => JSON.stringify(path)).join(", ")}`
|
|
540
|
+
).join("; ");
|
|
541
|
+
super(`derivedFieldsPlugin missing dependencies: ${details}`);
|
|
542
|
+
/** Stable discriminator, including across separately bundled entrypoints. */
|
|
543
|
+
this.code = "MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES";
|
|
544
|
+
this.name = "DerivedFieldsDependencyError";
|
|
545
|
+
this.issues = snapshot;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/plugins/derived-fields/lifecycle.ts
|
|
550
|
+
var import_node_util = require("util");
|
|
551
|
+
var import_mongoose5 = require("mongoose");
|
|
552
|
+
|
|
553
|
+
// src/plugins/derived-fields/planner.ts
|
|
554
|
+
var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
555
|
+
var isFullySelected = (document, path) => {
|
|
556
|
+
const projection = document.$__?.selected;
|
|
557
|
+
if (projection) {
|
|
558
|
+
if (hasOwn(projection, path) && projection[path] !== 1 && projection[path] !== true) return false;
|
|
559
|
+
const keys = Object.keys(projection);
|
|
560
|
+
if (keys.some((key) => key.startsWith(`${path}.`))) return false;
|
|
561
|
+
if (path !== "_id" && keys.length === 1 && projection._id === false) return true;
|
|
562
|
+
}
|
|
563
|
+
return document.isDirectSelected(path);
|
|
564
|
+
};
|
|
565
|
+
function planDerivedFields(registration, document, options = {}) {
|
|
566
|
+
const replacedPaths = registration.inputPaths.filter(
|
|
567
|
+
(path) => options.knownReplacedPaths?.has(path) || document.isDirectModified(path) && !document.$__?.primitiveAtomics?.[path]
|
|
568
|
+
);
|
|
569
|
+
const replacements = new Set(replacedPaths);
|
|
570
|
+
const availablePaths = registration.inputPaths.filter(
|
|
571
|
+
(path) => document.isNew || replacements.has(path) || isFullySelected(document, path)
|
|
572
|
+
);
|
|
573
|
+
const available = new Set(availablePaths);
|
|
574
|
+
const fields = registration.fields.filter(
|
|
575
|
+
(field) => document.isNew || field.mode === "always" || document.isDirectModified(field.targetPath) || options.pendingWritePaths?.has(field.targetPath) || field.dependsOn.some((path) => document.isModified(path) || options.pendingWritePaths?.has(path))
|
|
576
|
+
);
|
|
577
|
+
const issues = [];
|
|
578
|
+
for (const field of fields) {
|
|
579
|
+
const dependencies = field.mode === "always" ? registration.inputPaths : field.dependsOn;
|
|
580
|
+
const missingPaths = dependencies.filter((path) => !available.has(path));
|
|
581
|
+
if (missingPaths.length) issues.push({ targetPath: field.targetPath, missingPaths });
|
|
582
|
+
}
|
|
583
|
+
if (issues.length) throw new DerivedFieldsDependencyError(issues);
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
fields: Object.freeze(fields),
|
|
586
|
+
availablePaths: Object.freeze(availablePaths),
|
|
587
|
+
replacedPaths: Object.freeze(replacedPaths)
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/plugins/derived-fields/registration.ts
|
|
592
|
+
var supportedDerivedFieldTypes = Object.freeze([
|
|
593
|
+
"String",
|
|
594
|
+
"Number",
|
|
595
|
+
"Boolean",
|
|
596
|
+
"Date",
|
|
597
|
+
"ObjectId",
|
|
598
|
+
"Decimal128",
|
|
599
|
+
"BigInt"
|
|
600
|
+
]);
|
|
601
|
+
var registrationKey = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/moo/derived-fields/registration");
|
|
602
|
+
var hasOwn2 = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
603
|
+
var isRootPath = (path) => path.length > 0 && !/[.\s$\0]/.test(path);
|
|
604
|
+
var isSupportedPath = (path, type) => isRootPath(path) && supportedDerivedFieldTypes.includes(type.instance);
|
|
605
|
+
var configurationError = (targetPath, reason) => {
|
|
606
|
+
throw new TypeError(`derivedFieldsPlugin target ${JSON.stringify(targetPath)}: ${reason}`);
|
|
607
|
+
};
|
|
608
|
+
var parseDefinition = (targetPath, value) => {
|
|
609
|
+
if (typeof value === "function") {
|
|
610
|
+
return Object.freeze({
|
|
611
|
+
targetPath,
|
|
612
|
+
mode: "always",
|
|
613
|
+
dependsOn: Object.freeze([]),
|
|
614
|
+
get: value
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
618
|
+
return configurationError(targetPath, "derived must be a getter function or a plain { dependsOn, get } descriptor");
|
|
619
|
+
}
|
|
620
|
+
if (Reflect.ownKeys(value).some((key) => key !== "dependsOn" && key !== "get") || !hasOwn2(value, "dependsOn") || !hasOwn2(value, "get")) {
|
|
621
|
+
return configurationError(targetPath, "derived descriptors require exactly dependsOn and get");
|
|
622
|
+
}
|
|
623
|
+
const descriptor = value;
|
|
624
|
+
if (!Array.isArray(descriptor.dependsOn) || typeof descriptor.get !== "function") {
|
|
625
|
+
return configurationError(targetPath, "dependsOn must be an array of root path names and get must be a function");
|
|
626
|
+
}
|
|
627
|
+
const dependencies = [];
|
|
628
|
+
for (const dependency of descriptor.dependsOn) {
|
|
629
|
+
if (typeof dependency !== "string" || !isRootPath(dependency)) {
|
|
630
|
+
return configurationError(
|
|
631
|
+
targetPath,
|
|
632
|
+
"dependsOn must contain only non-empty root path names (no dots, whitespace, or $)"
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
if (dependencies.includes(dependency)) {
|
|
636
|
+
return configurationError(targetPath, `duplicate dependency path ${JSON.stringify(dependency)}`);
|
|
637
|
+
}
|
|
638
|
+
dependencies.push(dependency);
|
|
639
|
+
}
|
|
640
|
+
return Object.freeze({
|
|
641
|
+
targetPath,
|
|
642
|
+
mode: "dependencies",
|
|
643
|
+
dependsOn: Object.freeze(dependencies),
|
|
644
|
+
get: descriptor.get
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
function registerDerivedFields(schema) {
|
|
648
|
+
const markedSchema = schema;
|
|
649
|
+
if (hasOwn2(markedSchema, registrationKey)) {
|
|
650
|
+
throw new TypeError("derivedFieldsPlugin is already registered on this schema");
|
|
651
|
+
}
|
|
652
|
+
const fields = [];
|
|
653
|
+
const versionKey = schema.options.versionKey;
|
|
654
|
+
const visitSchema = (current, prefix, ancestors) => {
|
|
655
|
+
current.eachPath((path, type) => {
|
|
656
|
+
const fullPath = `${prefix}${path}`;
|
|
657
|
+
if (!hasOwn2(type.options, "derived")) return;
|
|
658
|
+
if (prefix || !isSupportedPath(path, type)) {
|
|
659
|
+
configurationError(
|
|
660
|
+
fullPath,
|
|
661
|
+
`only root scalar persisted paths are supported (${supportedDerivedFieldTypes.join(", ")})`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (path === versionKey) {
|
|
665
|
+
configurationError(
|
|
666
|
+
fullPath,
|
|
667
|
+
`Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a derived target`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (path === "_id" || (type.$immutable ?? type.options.immutable)) {
|
|
671
|
+
configurationError(fullPath, "immutable targets are not supported");
|
|
672
|
+
}
|
|
673
|
+
fields.push(parseDefinition(fullPath, type.options.derived));
|
|
674
|
+
});
|
|
675
|
+
const virtuals = current.virtuals;
|
|
676
|
+
for (const [path, virtual] of Object.entries(virtuals)) {
|
|
677
|
+
if (hasOwn2(virtual.options, "derived")) {
|
|
678
|
+
configurationError(`${prefix}${path}`, "derived targets must be persisted schema paths, not virtuals");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (ancestors.has(current)) return;
|
|
682
|
+
const nextAncestors = /* @__PURE__ */ new Set([...ancestors, current]);
|
|
683
|
+
const visitEmbedded = (type, path) => {
|
|
684
|
+
if (type.schema) {
|
|
685
|
+
visitSchema(type.schema, `${path}.`, nextAncestors);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
for (const member of type.schemaTypes ?? []) {
|
|
689
|
+
if (hasOwn2(member.options, "derived")) {
|
|
690
|
+
configurationError(path, "derived targets inside union containers are not supported");
|
|
691
|
+
}
|
|
692
|
+
visitEmbedded(member, path);
|
|
693
|
+
}
|
|
694
|
+
const embedded = type.getEmbeddedSchemaType?.() ?? type.caster ?? type.$embeddedSchemaType ?? type.$__schemaType;
|
|
695
|
+
if (!embedded || embedded === type) return;
|
|
696
|
+
const embeddedPath = `${path}.${type.instance === "Map" ? "$*" : "$"}`;
|
|
697
|
+
if (hasOwn2(embedded.options, "derived")) {
|
|
698
|
+
configurationError(embeddedPath, "derived targets inside containers are not supported");
|
|
699
|
+
}
|
|
700
|
+
visitEmbedded(embedded, embeddedPath);
|
|
701
|
+
};
|
|
702
|
+
current.eachPath((path, type) => visitEmbedded(type, `${prefix}${path}`));
|
|
703
|
+
};
|
|
704
|
+
visitSchema(schema, "", /* @__PURE__ */ new Set());
|
|
705
|
+
const targets = new Set(fields.map((field) => field.targetPath));
|
|
706
|
+
for (const field of fields) {
|
|
707
|
+
for (const dependency of field.dependsOn) {
|
|
708
|
+
if (dependency === versionKey) {
|
|
709
|
+
configurationError(
|
|
710
|
+
field.targetPath,
|
|
711
|
+
`Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a dependency`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (targets.has(dependency)) {
|
|
715
|
+
configurationError(
|
|
716
|
+
field.targetPath,
|
|
717
|
+
`dependency path ${JSON.stringify(dependency)} is itself a derived target`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const type = schema.path(dependency);
|
|
721
|
+
if (!type) {
|
|
722
|
+
configurationError(field.targetPath, `unknown or non-persisted dependency path ${JSON.stringify(dependency)}`);
|
|
723
|
+
}
|
|
724
|
+
if (!isSupportedPath(dependency, type)) {
|
|
725
|
+
configurationError(
|
|
726
|
+
field.targetPath,
|
|
727
|
+
`dependency path ${JSON.stringify(dependency)} must be a supported root scalar persisted path`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const inputPaths = [];
|
|
733
|
+
schema.eachPath((path, type) => {
|
|
734
|
+
if (path !== versionKey && !targets.has(path) && isSupportedPath(path, type)) inputPaths.push(path);
|
|
735
|
+
});
|
|
736
|
+
const registration = Object.freeze({ fields: Object.freeze(fields), inputPaths: Object.freeze(inputPaths) });
|
|
737
|
+
Object.defineProperty(markedSchema, registrationKey, { value: registration });
|
|
738
|
+
return registration;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/plugins/derived-fields/snapshot.ts
|
|
742
|
+
var import_mongoose4 = require("mongoose");
|
|
743
|
+
function cloneDerivedScalar(value) {
|
|
744
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
745
|
+
if (value !== null && typeof value === "object") {
|
|
746
|
+
const bson = value;
|
|
747
|
+
if (bson._bsontype === "ObjectId") return new import_mongoose4.Types.ObjectId(Buffer.from(bson.id));
|
|
748
|
+
if (bson._bsontype === "Decimal128") return new import_mongoose4.Types.Decimal128(Buffer.from(bson.bytes));
|
|
749
|
+
}
|
|
750
|
+
return value;
|
|
751
|
+
}
|
|
752
|
+
function readDerivedScalar(document, path) {
|
|
753
|
+
const id = document.populated(path);
|
|
754
|
+
return id === void 0 ? document.get(path, void 0, { getters: false }) : id;
|
|
755
|
+
}
|
|
756
|
+
function snapshotDerivedInputs(document, paths) {
|
|
757
|
+
return copyDerivedSnapshot(Object.fromEntries(paths.map((path) => [path, readDerivedScalar(document, path)])));
|
|
758
|
+
}
|
|
759
|
+
function copyDerivedSnapshot(snapshot, paths = Object.keys(snapshot)) {
|
|
760
|
+
return Object.freeze(Object.fromEntries(paths.map((path) => [path, cloneDerivedScalar(snapshot[path])])));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/plugins/derived-fields/lifecycle.ts
|
|
764
|
+
var watchedPaths = (plan) => [
|
|
765
|
+
...new Set(plan.fields.flatMap((field) => field.mode === "always" ? plan.availablePaths : field.dependsOn))
|
|
766
|
+
];
|
|
767
|
+
function derivedFieldsPlugin(schema) {
|
|
768
|
+
const registration = registerDerivedFields(schema);
|
|
769
|
+
const trackedPaths = [...registration.inputPaths, ...registration.fields.map((field) => field.targetPath)];
|
|
770
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
771
|
+
const stateFor = (document) => {
|
|
772
|
+
let state = states.get(document);
|
|
773
|
+
if (!state) {
|
|
774
|
+
state = { replacedPaths: /* @__PURE__ */ new Set(), assignedPaths: /* @__PURE__ */ new Set(), applyingResults: false };
|
|
775
|
+
states.set(document, state);
|
|
776
|
+
}
|
|
777
|
+
return state;
|
|
778
|
+
};
|
|
779
|
+
for (const path of trackedPaths) {
|
|
780
|
+
schema.path(path).set(function derivedFieldsObserveAssignment(value, _priorValue, _schemaType, options) {
|
|
781
|
+
if (this instanceof import_mongoose5.Document && !this.isNew && !options?._skipMarkModified && !isFullySelected(this, path)) {
|
|
782
|
+
const state = stateFor(this);
|
|
783
|
+
if (!state.applyingResults) state.assignedPaths.add(path);
|
|
784
|
+
}
|
|
785
|
+
return value;
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const planFor = (document, state) => {
|
|
789
|
+
for (const path of state.assignedPaths) {
|
|
790
|
+
if (!document.$__.validationError?.errors[path] && !document.$__.primitiveAtomics?.[path]) {
|
|
791
|
+
document.markModified(path);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
state.assignedPaths.clear();
|
|
795
|
+
const pendingWritePaths = new Set(
|
|
796
|
+
document.isNew ? [] : trackedPaths.filter(
|
|
797
|
+
(path) => path !== "_id" && document.$isDefault(path) && document.$__.selected?.[path] !== 0 && document.get(path, void 0, { getters: false }) != null
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
const plan = planDerivedFields(registration, document, {
|
|
801
|
+
knownReplacedPaths: state.replacedPaths,
|
|
802
|
+
pendingWritePaths
|
|
803
|
+
});
|
|
804
|
+
state.replacedPaths = new Set(document.isNew ? plan.availablePaths : plan.replacedPaths);
|
|
805
|
+
return plan;
|
|
806
|
+
};
|
|
807
|
+
const matches = (document, plan, checkpoint) => checkpoint !== void 0 && checkpoint.saveOptions === document.$__.saveOptions && checkpoint.isNew === document.isNew && // Restoring a target to its original persisted value can clear its dirty
|
|
808
|
+
// flag. It was still computed; only newly scheduled fields invalidate reuse.
|
|
809
|
+
plan.fields.every((field) => checkpoint.targets.includes(field.targetPath)) && (0, import_node_util.isDeepStrictEqual)(checkpoint.inputs, snapshotDerivedInputs(document, Object.keys(checkpoint.inputs))) && (0, import_node_util.isDeepStrictEqual)(checkpoint.results, snapshotDerivedInputs(document, checkpoint.targets));
|
|
810
|
+
const compute = async (document) => {
|
|
811
|
+
const state = stateFor(document);
|
|
812
|
+
state.checkpoint = void 0;
|
|
813
|
+
const plan = planFor(document, state);
|
|
814
|
+
const inputs = snapshotDerivedInputs(document, plan.availablePaths);
|
|
815
|
+
const staged = [];
|
|
816
|
+
for (const { targetPath, get } of plan.fields) {
|
|
817
|
+
const result = await get(copyDerivedSnapshot(inputs));
|
|
818
|
+
staged.push({ path: targetPath, value: cloneDerivedScalar(result) });
|
|
819
|
+
}
|
|
820
|
+
state.applyingResults = true;
|
|
821
|
+
try {
|
|
822
|
+
for (const { path, value } of staged) document.set(path, value);
|
|
823
|
+
} finally {
|
|
824
|
+
state.applyingResults = false;
|
|
825
|
+
}
|
|
826
|
+
const validationError = document.$__.validationError;
|
|
827
|
+
if (staged.some(({ path }) => validationError?.errors[path]?.name === "CastError")) throw validationError;
|
|
828
|
+
const targets = plan.fields.map((field) => field.targetPath);
|
|
829
|
+
state.checkpoint = {
|
|
830
|
+
saveOptions: document.$__.saveOptions,
|
|
831
|
+
isNew: document.isNew,
|
|
832
|
+
targets,
|
|
833
|
+
inputs: copyDerivedSnapshot(inputs, watchedPaths(plan)),
|
|
834
|
+
results: snapshotDerivedInputs(document, targets)
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
schema.pre("validate", async function derivedFieldsPreValidate() {
|
|
838
|
+
await compute(this);
|
|
839
|
+
});
|
|
840
|
+
schema.pre("save", async function derivedFieldsPreSave() {
|
|
841
|
+
const state = stateFor(this);
|
|
842
|
+
const plan = planFor(this, state);
|
|
843
|
+
if (this.$__.saveOptions && matches(this, plan, state.checkpoint)) return;
|
|
844
|
+
const options = this.$__.saveOptions;
|
|
845
|
+
const validate = options && "validateBeforeSave" in options ? !!options.validateBeforeSave : schema.options.validateBeforeSave;
|
|
846
|
+
if (validate) {
|
|
847
|
+
await this.validate(
|
|
848
|
+
void 0,
|
|
849
|
+
options && "validateModifiedOnly" in options ? { validateModifiedOnly: options.validateModifiedOnly } : void 0
|
|
850
|
+
);
|
|
851
|
+
} else {
|
|
852
|
+
await compute(this);
|
|
853
|
+
}
|
|
854
|
+
if (!matches(this, planFor(this, state), state.checkpoint)) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
"derivedFieldsPlugin inputs or targets changed after computation; register source-mutating validation hooks before the plugin and keep getters/validators free of document mutations"
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
524
862
|
// src/plugins/new-document.ts
|
|
525
863
|
var stateKey = "newDocumentPlugin";
|
|
526
864
|
function newDocumentPlugin(schema, options) {
|
|
@@ -561,8 +899,8 @@ var uniqueEmptiableString = (field, options) => (
|
|
|
561
899
|
);
|
|
562
900
|
|
|
563
901
|
// src/is.ts
|
|
564
|
-
var
|
|
565
|
-
var { ObjectId } =
|
|
902
|
+
var import_mongoose6 = require("mongoose");
|
|
903
|
+
var { ObjectId } = import_mongoose6.Types;
|
|
566
904
|
var CANONICAL_OBJECT_ID_STRING = /^[0-9a-f]{24}$/;
|
|
567
905
|
function isObjectId(value) {
|
|
568
906
|
try {
|
|
@@ -579,7 +917,10 @@ function isObjectId(value) {
|
|
|
579
917
|
}
|
|
580
918
|
// Annotate the CommonJS export names for ESM import in node:
|
|
581
919
|
0 && (module.exports = {
|
|
920
|
+
DerivedFieldsDependencyError,
|
|
582
921
|
cascadeDeletePlugin,
|
|
922
|
+
defineDerivedField,
|
|
923
|
+
derivedFieldsPlugin,
|
|
583
924
|
isObjectId,
|
|
584
925
|
modelFunctionPlugin,
|
|
585
926
|
newDocumentPlugin,
|