@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/index.mjs
CHANGED
|
@@ -480,6 +480,341 @@ function cascadeDeletePlugin(schema, options) {
|
|
|
480
480
|
});
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
// src/plugins/derived-fields/definitions.ts
|
|
484
|
+
function defineDerivedField(definition) {
|
|
485
|
+
return definition;
|
|
486
|
+
}
|
|
487
|
+
var DerivedFieldsDependencyError = class extends Error {
|
|
488
|
+
constructor(issues) {
|
|
489
|
+
const snapshot = Object.freeze(
|
|
490
|
+
issues.map(
|
|
491
|
+
({ targetPath, missingPaths }) => Object.freeze({ targetPath, missingPaths: Object.freeze([...missingPaths]) })
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
const details = snapshot.map(
|
|
495
|
+
({ targetPath, missingPaths }) => `${JSON.stringify(targetPath)} requires ${missingPaths.map((path) => JSON.stringify(path)).join(", ")}`
|
|
496
|
+
).join("; ");
|
|
497
|
+
super(`derivedFieldsPlugin missing dependencies: ${details}`);
|
|
498
|
+
/** Stable discriminator, including across separately bundled entrypoints. */
|
|
499
|
+
this.code = "MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES";
|
|
500
|
+
this.name = "DerivedFieldsDependencyError";
|
|
501
|
+
this.issues = snapshot;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/plugins/derived-fields/lifecycle.ts
|
|
506
|
+
import { isDeepStrictEqual } from "util";
|
|
507
|
+
import { Document } from "mongoose";
|
|
508
|
+
|
|
509
|
+
// src/plugins/derived-fields/planner.ts
|
|
510
|
+
var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
511
|
+
var isFullySelected = (document, path) => {
|
|
512
|
+
const projection = document.$__?.selected;
|
|
513
|
+
if (projection) {
|
|
514
|
+
if (hasOwn(projection, path) && projection[path] !== 1 && projection[path] !== true) return false;
|
|
515
|
+
const keys = Object.keys(projection);
|
|
516
|
+
if (keys.some((key) => key.startsWith(`${path}.`))) return false;
|
|
517
|
+
if (path !== "_id" && keys.length === 1 && projection._id === false) return true;
|
|
518
|
+
}
|
|
519
|
+
return document.isDirectSelected(path);
|
|
520
|
+
};
|
|
521
|
+
function planDerivedFields(registration, document, options = {}) {
|
|
522
|
+
const replacedPaths = registration.inputPaths.filter(
|
|
523
|
+
(path) => options.knownReplacedPaths?.has(path) || document.isDirectModified(path) && !document.$__?.primitiveAtomics?.[path]
|
|
524
|
+
);
|
|
525
|
+
const replacements = new Set(replacedPaths);
|
|
526
|
+
const availablePaths = registration.inputPaths.filter(
|
|
527
|
+
(path) => document.isNew || replacements.has(path) || isFullySelected(document, path)
|
|
528
|
+
);
|
|
529
|
+
const available = new Set(availablePaths);
|
|
530
|
+
const fields = registration.fields.filter(
|
|
531
|
+
(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))
|
|
532
|
+
);
|
|
533
|
+
const issues = [];
|
|
534
|
+
for (const field of fields) {
|
|
535
|
+
const dependencies = field.mode === "always" ? registration.inputPaths : field.dependsOn;
|
|
536
|
+
const missingPaths = dependencies.filter((path) => !available.has(path));
|
|
537
|
+
if (missingPaths.length) issues.push({ targetPath: field.targetPath, missingPaths });
|
|
538
|
+
}
|
|
539
|
+
if (issues.length) throw new DerivedFieldsDependencyError(issues);
|
|
540
|
+
return Object.freeze({
|
|
541
|
+
fields: Object.freeze(fields),
|
|
542
|
+
availablePaths: Object.freeze(availablePaths),
|
|
543
|
+
replacedPaths: Object.freeze(replacedPaths)
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/plugins/derived-fields/registration.ts
|
|
548
|
+
var supportedDerivedFieldTypes = Object.freeze([
|
|
549
|
+
"String",
|
|
550
|
+
"Number",
|
|
551
|
+
"Boolean",
|
|
552
|
+
"Date",
|
|
553
|
+
"ObjectId",
|
|
554
|
+
"Decimal128",
|
|
555
|
+
"BigInt"
|
|
556
|
+
]);
|
|
557
|
+
var registrationKey = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/moo/derived-fields/registration");
|
|
558
|
+
var hasOwn2 = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
559
|
+
var isRootPath = (path) => path.length > 0 && !/[.\s$\0]/.test(path);
|
|
560
|
+
var isSupportedPath = (path, type) => isRootPath(path) && supportedDerivedFieldTypes.includes(type.instance);
|
|
561
|
+
var configurationError = (targetPath, reason) => {
|
|
562
|
+
throw new TypeError(`derivedFieldsPlugin target ${JSON.stringify(targetPath)}: ${reason}`);
|
|
563
|
+
};
|
|
564
|
+
var parseDefinition = (targetPath, value) => {
|
|
565
|
+
if (typeof value === "function") {
|
|
566
|
+
return Object.freeze({
|
|
567
|
+
targetPath,
|
|
568
|
+
mode: "always",
|
|
569
|
+
dependsOn: Object.freeze([]),
|
|
570
|
+
get: value
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
574
|
+
return configurationError(targetPath, "derived must be a getter function or a plain { dependsOn, get } descriptor");
|
|
575
|
+
}
|
|
576
|
+
if (Reflect.ownKeys(value).some((key) => key !== "dependsOn" && key !== "get") || !hasOwn2(value, "dependsOn") || !hasOwn2(value, "get")) {
|
|
577
|
+
return configurationError(targetPath, "derived descriptors require exactly dependsOn and get");
|
|
578
|
+
}
|
|
579
|
+
const descriptor = value;
|
|
580
|
+
if (!Array.isArray(descriptor.dependsOn) || typeof descriptor.get !== "function") {
|
|
581
|
+
return configurationError(targetPath, "dependsOn must be an array of root path names and get must be a function");
|
|
582
|
+
}
|
|
583
|
+
const dependencies = [];
|
|
584
|
+
for (const dependency of descriptor.dependsOn) {
|
|
585
|
+
if (typeof dependency !== "string" || !isRootPath(dependency)) {
|
|
586
|
+
return configurationError(
|
|
587
|
+
targetPath,
|
|
588
|
+
"dependsOn must contain only non-empty root path names (no dots, whitespace, or $)"
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (dependencies.includes(dependency)) {
|
|
592
|
+
return configurationError(targetPath, `duplicate dependency path ${JSON.stringify(dependency)}`);
|
|
593
|
+
}
|
|
594
|
+
dependencies.push(dependency);
|
|
595
|
+
}
|
|
596
|
+
return Object.freeze({
|
|
597
|
+
targetPath,
|
|
598
|
+
mode: "dependencies",
|
|
599
|
+
dependsOn: Object.freeze(dependencies),
|
|
600
|
+
get: descriptor.get
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
function registerDerivedFields(schema) {
|
|
604
|
+
const markedSchema = schema;
|
|
605
|
+
if (hasOwn2(markedSchema, registrationKey)) {
|
|
606
|
+
throw new TypeError("derivedFieldsPlugin is already registered on this schema");
|
|
607
|
+
}
|
|
608
|
+
const fields = [];
|
|
609
|
+
const versionKey = schema.options.versionKey;
|
|
610
|
+
const visitSchema = (current, prefix, ancestors) => {
|
|
611
|
+
current.eachPath((path, type) => {
|
|
612
|
+
const fullPath = `${prefix}${path}`;
|
|
613
|
+
if (!hasOwn2(type.options, "derived")) return;
|
|
614
|
+
if (prefix || !isSupportedPath(path, type)) {
|
|
615
|
+
configurationError(
|
|
616
|
+
fullPath,
|
|
617
|
+
`only root scalar persisted paths are supported (${supportedDerivedFieldTypes.join(", ")})`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (path === versionKey) {
|
|
621
|
+
configurationError(
|
|
622
|
+
fullPath,
|
|
623
|
+
`Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a derived target`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (path === "_id" || (type.$immutable ?? type.options.immutable)) {
|
|
627
|
+
configurationError(fullPath, "immutable targets are not supported");
|
|
628
|
+
}
|
|
629
|
+
fields.push(parseDefinition(fullPath, type.options.derived));
|
|
630
|
+
});
|
|
631
|
+
const virtuals = current.virtuals;
|
|
632
|
+
for (const [path, virtual] of Object.entries(virtuals)) {
|
|
633
|
+
if (hasOwn2(virtual.options, "derived")) {
|
|
634
|
+
configurationError(`${prefix}${path}`, "derived targets must be persisted schema paths, not virtuals");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (ancestors.has(current)) return;
|
|
638
|
+
const nextAncestors = /* @__PURE__ */ new Set([...ancestors, current]);
|
|
639
|
+
const visitEmbedded = (type, path) => {
|
|
640
|
+
if (type.schema) {
|
|
641
|
+
visitSchema(type.schema, `${path}.`, nextAncestors);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
for (const member of type.schemaTypes ?? []) {
|
|
645
|
+
if (hasOwn2(member.options, "derived")) {
|
|
646
|
+
configurationError(path, "derived targets inside union containers are not supported");
|
|
647
|
+
}
|
|
648
|
+
visitEmbedded(member, path);
|
|
649
|
+
}
|
|
650
|
+
const embedded = type.getEmbeddedSchemaType?.() ?? type.caster ?? type.$embeddedSchemaType ?? type.$__schemaType;
|
|
651
|
+
if (!embedded || embedded === type) return;
|
|
652
|
+
const embeddedPath = `${path}.${type.instance === "Map" ? "$*" : "$"}`;
|
|
653
|
+
if (hasOwn2(embedded.options, "derived")) {
|
|
654
|
+
configurationError(embeddedPath, "derived targets inside containers are not supported");
|
|
655
|
+
}
|
|
656
|
+
visitEmbedded(embedded, embeddedPath);
|
|
657
|
+
};
|
|
658
|
+
current.eachPath((path, type) => visitEmbedded(type, `${prefix}${path}`));
|
|
659
|
+
};
|
|
660
|
+
visitSchema(schema, "", /* @__PURE__ */ new Set());
|
|
661
|
+
const targets = new Set(fields.map((field) => field.targetPath));
|
|
662
|
+
for (const field of fields) {
|
|
663
|
+
for (const dependency of field.dependsOn) {
|
|
664
|
+
if (dependency === versionKey) {
|
|
665
|
+
configurationError(
|
|
666
|
+
field.targetPath,
|
|
667
|
+
`Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a dependency`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (targets.has(dependency)) {
|
|
671
|
+
configurationError(
|
|
672
|
+
field.targetPath,
|
|
673
|
+
`dependency path ${JSON.stringify(dependency)} is itself a derived target`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const type = schema.path(dependency);
|
|
677
|
+
if (!type) {
|
|
678
|
+
configurationError(field.targetPath, `unknown or non-persisted dependency path ${JSON.stringify(dependency)}`);
|
|
679
|
+
}
|
|
680
|
+
if (!isSupportedPath(dependency, type)) {
|
|
681
|
+
configurationError(
|
|
682
|
+
field.targetPath,
|
|
683
|
+
`dependency path ${JSON.stringify(dependency)} must be a supported root scalar persisted path`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const inputPaths = [];
|
|
689
|
+
schema.eachPath((path, type) => {
|
|
690
|
+
if (path !== versionKey && !targets.has(path) && isSupportedPath(path, type)) inputPaths.push(path);
|
|
691
|
+
});
|
|
692
|
+
const registration = Object.freeze({ fields: Object.freeze(fields), inputPaths: Object.freeze(inputPaths) });
|
|
693
|
+
Object.defineProperty(markedSchema, registrationKey, { value: registration });
|
|
694
|
+
return registration;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/plugins/derived-fields/snapshot.ts
|
|
698
|
+
import { Types } from "mongoose";
|
|
699
|
+
function cloneDerivedScalar(value) {
|
|
700
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
701
|
+
if (value !== null && typeof value === "object") {
|
|
702
|
+
const bson = value;
|
|
703
|
+
if (bson._bsontype === "ObjectId") return new Types.ObjectId(Buffer.from(bson.id));
|
|
704
|
+
if (bson._bsontype === "Decimal128") return new Types.Decimal128(Buffer.from(bson.bytes));
|
|
705
|
+
}
|
|
706
|
+
return value;
|
|
707
|
+
}
|
|
708
|
+
function readDerivedScalar(document, path) {
|
|
709
|
+
const id = document.populated(path);
|
|
710
|
+
return id === void 0 ? document.get(path, void 0, { getters: false }) : id;
|
|
711
|
+
}
|
|
712
|
+
function snapshotDerivedInputs(document, paths) {
|
|
713
|
+
return copyDerivedSnapshot(Object.fromEntries(paths.map((path) => [path, readDerivedScalar(document, path)])));
|
|
714
|
+
}
|
|
715
|
+
function copyDerivedSnapshot(snapshot, paths = Object.keys(snapshot)) {
|
|
716
|
+
return Object.freeze(Object.fromEntries(paths.map((path) => [path, cloneDerivedScalar(snapshot[path])])));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/plugins/derived-fields/lifecycle.ts
|
|
720
|
+
var watchedPaths = (plan) => [
|
|
721
|
+
...new Set(plan.fields.flatMap((field) => field.mode === "always" ? plan.availablePaths : field.dependsOn))
|
|
722
|
+
];
|
|
723
|
+
function derivedFieldsPlugin(schema) {
|
|
724
|
+
const registration = registerDerivedFields(schema);
|
|
725
|
+
const trackedPaths = [...registration.inputPaths, ...registration.fields.map((field) => field.targetPath)];
|
|
726
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
727
|
+
const stateFor = (document) => {
|
|
728
|
+
let state = states.get(document);
|
|
729
|
+
if (!state) {
|
|
730
|
+
state = { replacedPaths: /* @__PURE__ */ new Set(), assignedPaths: /* @__PURE__ */ new Set(), applyingResults: false };
|
|
731
|
+
states.set(document, state);
|
|
732
|
+
}
|
|
733
|
+
return state;
|
|
734
|
+
};
|
|
735
|
+
for (const path of trackedPaths) {
|
|
736
|
+
schema.path(path).set(function derivedFieldsObserveAssignment(value, _priorValue, _schemaType, options) {
|
|
737
|
+
if (this instanceof Document && !this.isNew && !options?._skipMarkModified && !isFullySelected(this, path)) {
|
|
738
|
+
const state = stateFor(this);
|
|
739
|
+
if (!state.applyingResults) state.assignedPaths.add(path);
|
|
740
|
+
}
|
|
741
|
+
return value;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const planFor = (document, state) => {
|
|
745
|
+
for (const path of state.assignedPaths) {
|
|
746
|
+
if (!document.$__.validationError?.errors[path] && !document.$__.primitiveAtomics?.[path]) {
|
|
747
|
+
document.markModified(path);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
state.assignedPaths.clear();
|
|
751
|
+
const pendingWritePaths = new Set(
|
|
752
|
+
document.isNew ? [] : trackedPaths.filter(
|
|
753
|
+
(path) => path !== "_id" && document.$isDefault(path) && document.$__.selected?.[path] !== 0 && document.get(path, void 0, { getters: false }) != null
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
const plan = planDerivedFields(registration, document, {
|
|
757
|
+
knownReplacedPaths: state.replacedPaths,
|
|
758
|
+
pendingWritePaths
|
|
759
|
+
});
|
|
760
|
+
state.replacedPaths = new Set(document.isNew ? plan.availablePaths : plan.replacedPaths);
|
|
761
|
+
return plan;
|
|
762
|
+
};
|
|
763
|
+
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
|
|
764
|
+
// flag. It was still computed; only newly scheduled fields invalidate reuse.
|
|
765
|
+
plan.fields.every((field) => checkpoint.targets.includes(field.targetPath)) && isDeepStrictEqual(checkpoint.inputs, snapshotDerivedInputs(document, Object.keys(checkpoint.inputs))) && isDeepStrictEqual(checkpoint.results, snapshotDerivedInputs(document, checkpoint.targets));
|
|
766
|
+
const compute = async (document) => {
|
|
767
|
+
const state = stateFor(document);
|
|
768
|
+
state.checkpoint = void 0;
|
|
769
|
+
const plan = planFor(document, state);
|
|
770
|
+
const inputs = snapshotDerivedInputs(document, plan.availablePaths);
|
|
771
|
+
const staged = [];
|
|
772
|
+
for (const { targetPath, get } of plan.fields) {
|
|
773
|
+
const result = await get(copyDerivedSnapshot(inputs));
|
|
774
|
+
staged.push({ path: targetPath, value: cloneDerivedScalar(result) });
|
|
775
|
+
}
|
|
776
|
+
state.applyingResults = true;
|
|
777
|
+
try {
|
|
778
|
+
for (const { path, value } of staged) document.set(path, value);
|
|
779
|
+
} finally {
|
|
780
|
+
state.applyingResults = false;
|
|
781
|
+
}
|
|
782
|
+
const validationError = document.$__.validationError;
|
|
783
|
+
if (staged.some(({ path }) => validationError?.errors[path]?.name === "CastError")) throw validationError;
|
|
784
|
+
const targets = plan.fields.map((field) => field.targetPath);
|
|
785
|
+
state.checkpoint = {
|
|
786
|
+
saveOptions: document.$__.saveOptions,
|
|
787
|
+
isNew: document.isNew,
|
|
788
|
+
targets,
|
|
789
|
+
inputs: copyDerivedSnapshot(inputs, watchedPaths(plan)),
|
|
790
|
+
results: snapshotDerivedInputs(document, targets)
|
|
791
|
+
};
|
|
792
|
+
};
|
|
793
|
+
schema.pre("validate", async function derivedFieldsPreValidate() {
|
|
794
|
+
await compute(this);
|
|
795
|
+
});
|
|
796
|
+
schema.pre("save", async function derivedFieldsPreSave() {
|
|
797
|
+
const state = stateFor(this);
|
|
798
|
+
const plan = planFor(this, state);
|
|
799
|
+
if (this.$__.saveOptions && matches(this, plan, state.checkpoint)) return;
|
|
800
|
+
const options = this.$__.saveOptions;
|
|
801
|
+
const validate = options && "validateBeforeSave" in options ? !!options.validateBeforeSave : schema.options.validateBeforeSave;
|
|
802
|
+
if (validate) {
|
|
803
|
+
await this.validate(
|
|
804
|
+
void 0,
|
|
805
|
+
options && "validateModifiedOnly" in options ? { validateModifiedOnly: options.validateModifiedOnly } : void 0
|
|
806
|
+
);
|
|
807
|
+
} else {
|
|
808
|
+
await compute(this);
|
|
809
|
+
}
|
|
810
|
+
if (!matches(this, planFor(this, state), state.checkpoint)) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
"derivedFieldsPlugin inputs or targets changed after computation; register source-mutating validation hooks before the plugin and keep getters/validators free of document mutations"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
483
818
|
// src/plugins/new-document.ts
|
|
484
819
|
var stateKey = "newDocumentPlugin";
|
|
485
820
|
function newDocumentPlugin(schema, options) {
|
|
@@ -520,8 +855,8 @@ var uniqueEmptiableString = (field, options) => (
|
|
|
520
855
|
);
|
|
521
856
|
|
|
522
857
|
// src/is.ts
|
|
523
|
-
import { Types } from "mongoose";
|
|
524
|
-
var { ObjectId } =
|
|
858
|
+
import { Types as Types2 } from "mongoose";
|
|
859
|
+
var { ObjectId } = Types2;
|
|
525
860
|
var CANONICAL_OBJECT_ID_STRING = /^[0-9a-f]{24}$/;
|
|
526
861
|
function isObjectId(value) {
|
|
527
862
|
try {
|
|
@@ -537,7 +872,10 @@ function isObjectId(value) {
|
|
|
537
872
|
}
|
|
538
873
|
}
|
|
539
874
|
export {
|
|
875
|
+
DerivedFieldsDependencyError,
|
|
540
876
|
cascadeDeletePlugin,
|
|
877
|
+
defineDerivedField,
|
|
878
|
+
derivedFieldsPlugin,
|
|
541
879
|
isObjectId,
|
|
542
880
|
modelFunctionPlugin,
|
|
543
881
|
newDocumentPlugin,
|
package/llms.txt
CHANGED
|
@@ -49,6 +49,44 @@ userSchema.plugin(modelFunctionPlugin, {
|
|
|
49
49
|
// static + instance methods `fullName` and `fullNameById` are now available on the model
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Persisted derived fields (see README "Persisted Derived Fields" for full contract):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { Schema } from 'mongoose';
|
|
56
|
+
import { defineDerivedField, derivedFieldsPlugin } from '@web-ts-toolkit/moo';
|
|
57
|
+
|
|
58
|
+
type User = { firstName: string; lastName: string; fullName: string };
|
|
59
|
+
const userSchema = new Schema<User>({
|
|
60
|
+
firstName: String,
|
|
61
|
+
lastName: String,
|
|
62
|
+
fullName: {
|
|
63
|
+
type: String,
|
|
64
|
+
index: true,
|
|
65
|
+
derived: defineDerivedField<User, 'fullName'>({
|
|
66
|
+
dependsOn: ['firstName', 'lastName'],
|
|
67
|
+
get: (doc) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim(),
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
}, { optimisticConcurrency: true });
|
|
71
|
+
// Register source mutators first, then this plugin, then compile the model.
|
|
72
|
+
userSchema.plugin(derivedFieldsPlugin);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Stored results are searchable/filterable/sortable/indexable with ordinary MongoDB queries; reads/property access never compute or persist.
|
|
76
|
+
- Existing explicit dependencies: no dependency modification/target assignment/pending default write => skip regardless of selection; work requested + all available => recompute; work requested + missing => reject before any getter. An omitted/missing target alone does not request repair.
|
|
77
|
+
- Actual pending non-null source/target defaults count as writes without modification flags (including '', 0, false); null/undefined defaults alone do not write. Pending target fallback defaults recompute. Applying defaults to excluded dependencies can queue writes and require rejection even on an otherwise unrelated save; defaults never prove an excluded input available.
|
|
78
|
+
- Available = selected (defaults usable, genuine absence without a default is known `undefined`, stored `null` is known) or explicitly fully replaced, including excluded null/undefined/default assignments retained on the same document. Unselected defaults/getters and relative `$inc()`/child modifications are not replacements. No automatic fetch.
|
|
79
|
+
- New documents retain their supplied/defaulted/absent input availability on later saves, including with constructor projection metadata.
|
|
80
|
+
- Bare getter shorthand recomputes each async validation/save and requires every supported non-derived input, including hidden fields and `_id`.
|
|
81
|
+
- Sync/async getters receive isolated readonly partial plain inputs, depopulated ids and copied Date/ObjectId/Decimal128 scalars, no virtuals/derived targets/managed version key/containers. Return `undefined` to clear; staged results use Mongoose setters/casting. Getters/validators must not mutate captured hydrated inputs.
|
|
82
|
+
- Root String/Number/Boolean/Date/ObjectId/Decimal128/BigInt only; reject nested/container/Mixed/Union/Binary/UUID/custom kinds, derived chains, immutable/virtual targets, unknown names, malformed options, and duplicate registration. Other ordinary fields coexist but are not getter inputs.
|
|
83
|
+
- Reject Mongoose-managed versionKey (default __v or custom) as a target/explicit dependency and omit it from every input view: Mongoose initializes/increments it after pre-save middleware. With versionKey:false, explicitly declared version-like scalar fields are ordinary supported targets/inputs.
|
|
84
|
+
- `DerivedFieldsDependencyError`: code `MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES`, frozen path-only `issues: [{ targetPath, missingPaths }]`. Getter failures propagate; casting/validation use Mongoose errors. Rejection preserves application assignments in memory.
|
|
85
|
+
- Register source-mutating validation/save middleware before plugin. Normal validation sees results; earlier pre-save changes recompute/revalidate. Later pre-save mutations are outside guarantee; unstable validation-time mutations reject on save. `validateBeforeSave:false` computes/casts but skips required/custom validators.
|
|
86
|
+
- Guaranteed writes: hydrated save/Model.create; async document validation computes without persisting. Query updates/replacements/raw writes bypass. insertMany/bulk inserts may incidentally validate, not all-writer support; validateSync/middleware bypass/pathsToSave excluded.
|
|
87
|
+
- Backfill old rows/formula changes explicitly (load dependencies, mark target modified, save). Ordinary multi-writer Mongoose concurrency applies; optimisticConcurrency recommended for source/result invariants.
|
|
88
|
+
- All helper/error/types also available from `@web-ts-toolkit/moo/plugins` and `@web-ts-toolkit/moo/plugins/derived-fields`.
|
|
89
|
+
|
|
52
90
|
New-document plugin:
|
|
53
91
|
|
|
54
92
|
```ts
|
|
@@ -146,8 +184,9 @@ userSchema.plugin(keycloakUserSyncPlugin, {
|
|
|
146
184
|
- `@web-ts-toolkit/moo/is` — `isObjectId`
|
|
147
185
|
- `@web-ts-toolkit/moo/schema` — `uniqueNullableString`, `uniqueEmptiableString`
|
|
148
186
|
- `@web-ts-toolkit/moo/utils` — `isSchema`, `isObjectIdType`, `isReference`
|
|
149
|
-
- `@web-ts-toolkit/moo/plugins` — non-Keycloak plugins (`modelFunctionPlugin`, `cascadeDeletePlugin`, `newDocumentPlugin`)
|
|
187
|
+
- `@web-ts-toolkit/moo/plugins` — non-Keycloak plugins (`modelFunctionPlugin`, `cascadeDeletePlugin`, `derivedFieldsPlugin`, `newDocumentPlugin`)
|
|
150
188
|
- `@web-ts-toolkit/moo/plugins/cascade-delete` — `cascadeDeletePlugin`
|
|
189
|
+
- `@web-ts-toolkit/moo/plugins/derived-fields` — `derivedFieldsPlugin`, `defineDerivedField`, `DerivedFieldsDependencyError`, authoring types
|
|
151
190
|
- `@web-ts-toolkit/moo/plugins/model-function` — `modelFunctionPlugin`
|
|
152
191
|
- `@web-ts-toolkit/moo/plugins/new-document` — `newDocumentPlugin`
|
|
153
192
|
- `@web-ts-toolkit/moo/plugins/keycloak-user-sync` — `createManagedKeycloakClient`, `keycloakUserSyncPlugin`
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@web-ts-toolkit/moo",
|
|
3
3
|
"description": "Mongoose helpers for schema fields, ObjectId checks, and document plugins",
|
|
4
4
|
"homepage": "https://web-ts-toolkit.pages.dev/docs/packages/moo",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.57.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"mongoose",
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
"require": "./plugins/cascade-delete.js",
|
|
52
52
|
"default": "./plugins/cascade-delete.js"
|
|
53
53
|
},
|
|
54
|
+
"./plugins/derived-fields": {
|
|
55
|
+
"types": "./plugins/derived-fields.d.ts",
|
|
56
|
+
"import": "./plugins/derived-fields.mjs",
|
|
57
|
+
"require": "./plugins/derived-fields.js",
|
|
58
|
+
"default": "./plugins/derived-fields.js"
|
|
59
|
+
},
|
|
54
60
|
"./plugins/model-function": {
|
|
55
61
|
"types": "./plugins/model-function.d.ts",
|
|
56
62
|
"import": "./plugins/model-function.mjs",
|
|
@@ -83,7 +89,7 @@
|
|
|
83
89
|
}
|
|
84
90
|
},
|
|
85
91
|
"dependencies": {
|
|
86
|
-
"@web-ts-toolkit/utils": "0.
|
|
92
|
+
"@web-ts-toolkit/utils": "0.57.0"
|
|
87
93
|
},
|
|
88
94
|
"files": [
|
|
89
95
|
"**/*",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Schema } from 'mongoose';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A partial plain persisted-model view, not a hydrated document. Only available,
|
|
5
|
+
* supported root scalar inputs are supplied; virtuals, populated documents,
|
|
6
|
+
* containers, derived targets, and Mongoose's managed version key are not inputs.
|
|
7
|
+
* Use the raw model type for `TModel`. Selected defaults are available; genuine
|
|
8
|
+
* selected absence is `undefined` and stored `null` is known. Defaults applied to
|
|
9
|
+
* unselected fields never prove availability, even when pending a write.
|
|
10
|
+
* Dates/ObjectIds/Decimal128s are copied per invocation; property getters/
|
|
11
|
+
* transforms are bypassed.
|
|
12
|
+
* Never mutate the hydrated document through a captured reference.
|
|
13
|
+
*/
|
|
14
|
+
type DerivedFieldInput<TModel> = Readonly<Partial<TModel>>;
|
|
15
|
+
/** Sync/async computation with no document `this`; `undefined` clears the target. */
|
|
16
|
+
type DerivedFieldGetter<TModel, TValue> = (this: void, doc: DerivedFieldInput<TModel>) => TValue | undefined | Promise<TValue | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Declare every source read by `get`. Existing documents skip when no dependency
|
|
19
|
+
* modification/default write or target assignment/default write requests work.
|
|
20
|
+
* Pending non-null defaults count even without Mongoose modification flags;
|
|
21
|
+
* null/undefined defaults alone do not write. Every requested computation requires
|
|
22
|
+
* all dependencies selected or fully replaced; excluded defaults are not replacements.
|
|
23
|
+
* The Mongoose-managed version key is not a supported dependency or target.
|
|
24
|
+
*/
|
|
25
|
+
interface DerivedFieldDescriptor<TModel, TValue> {
|
|
26
|
+
readonly dependsOn: readonly Extract<keyof TModel, string>[];
|
|
27
|
+
readonly get: DerivedFieldGetter<TModel, TValue>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `derived` schema-path option. A bare getter recomputes on every operation
|
|
31
|
+
* and requires every supported non-derived schema input to be available.
|
|
32
|
+
*/
|
|
33
|
+
type DerivedFieldDefinition<TModel, TValue> = DerivedFieldDescriptor<TModel, TValue> | DerivedFieldGetter<TModel, TValue>;
|
|
34
|
+
/**
|
|
35
|
+
* Contextually types a `derived` option without augmenting Mongoose globally.
|
|
36
|
+
* The second type argument is the target's key in the raw persisted model, so
|
|
37
|
+
* dependency names, getter input, and the target's result type are checked.
|
|
38
|
+
* Schema registration separately checks runtime path eligibility.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* type User = { firstName: string; lastName: string; fullName: string };
|
|
42
|
+
* const derived = defineDerivedField<User, 'fullName'>({
|
|
43
|
+
* dependsOn: ['firstName', 'lastName'],
|
|
44
|
+
* get: (doc) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim(),
|
|
45
|
+
* });
|
|
46
|
+
*/
|
|
47
|
+
declare function defineDerivedField<TModel, TTargetKey extends Extract<keyof TModel, string>>(definition: DerivedFieldDescriptor<TModel, TModel[TTargetKey]>): DerivedFieldDescriptor<TModel, TModel[TTargetKey]>;
|
|
48
|
+
declare function defineDerivedField<TModel, TTargetKey extends Extract<keyof TModel, string>>(definition: DerivedFieldGetter<TModel, TModel[TTargetKey]>): DerivedFieldGetter<TModel, TModel[TTargetKey]>;
|
|
49
|
+
declare function defineDerivedField<TModel, TTargetKey extends Extract<keyof TModel, string>>(definition: DerivedFieldDefinition<TModel, TModel[TTargetKey]>): DerivedFieldDefinition<TModel, TModel[TTargetKey]>;
|
|
50
|
+
/** One target's unavailable dependency paths; never includes source values. */
|
|
51
|
+
interface DerivedFieldDependencyIssue {
|
|
52
|
+
readonly targetPath: string;
|
|
53
|
+
readonly missingPaths: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Raised by dependency preflight before any getter runs. `issues` lists every
|
|
57
|
+
* scheduled target with missing inputs. No implicit database fetch is attempted.
|
|
58
|
+
*/
|
|
59
|
+
declare class DerivedFieldsDependencyError extends Error {
|
|
60
|
+
/** Stable discriminator, including across separately bundled entrypoints. */
|
|
61
|
+
readonly code: "MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES";
|
|
62
|
+
/** Frozen path-only issues; no source values or hydrated document are attached. */
|
|
63
|
+
readonly issues: readonly DerivedFieldDependencyIssue[];
|
|
64
|
+
constructor(issues: readonly DerivedFieldDependencyIssue[]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Computes opt-in root scalar `derived` fields before asynchronous document
|
|
69
|
+
* validation and hydrated `save()` (including `Model.create()`). Results are
|
|
70
|
+
* staged, then assigned through Mongoose setters/casting; `undefined` clears a
|
|
71
|
+
* target. Getters receive separate frozen plain views with copied scalar values.
|
|
72
|
+
* Supported targets/dependencies: root String, Number, Boolean, Date, ObjectId,
|
|
73
|
+
* Decimal128, BigInt; containers, nested paths, and derived chains are rejected.
|
|
74
|
+
* Mongoose's managed version key (default `__v` or custom) is rejected as a
|
|
75
|
+
* target/dependency and omitted from inputs; with versionKey:false it is ordinary.
|
|
76
|
+
*
|
|
77
|
+
* Register after defining paths and source-mutating middleware, before compiling
|
|
78
|
+
* the model. Source-changing pre-validation hooks must precede this plugin for
|
|
79
|
+
* standalone validation to see their final values. Earlier pre-save changes are
|
|
80
|
+
* recomputed and, unless validation is disabled, revalidated. Changes made by
|
|
81
|
+
* later pre-save hooks are outside this guarantee. Validation hooks/validators
|
|
82
|
+
* must not change inputs after this plugin computes them: a save that still has
|
|
83
|
+
* such changes after revalidation rejects with an ordering error.
|
|
84
|
+
*
|
|
85
|
+
* No inputs are fetched. Explicit descriptors skip unless a dependency modification
|
|
86
|
+
* or target assignment requests work, including actual pending default writes.
|
|
87
|
+
* Mongoose writes non-null defaults (including '', 0, false) without modification
|
|
88
|
+
* flags; null/undefined defaults alone do not write. Pending defaults schedule work
|
|
89
|
+
* but do not prove excluded inputs available: all dependencies must be selected or
|
|
90
|
+
* fully replaced. Missing/projected-out targets without a write request still skip.
|
|
91
|
+
* Bare getters run on every validation/save and require every supported input.
|
|
92
|
+
* New documents retain their supplied/defaulted/absent input availability on
|
|
93
|
+
* later saves, including when constructed with projection metadata.
|
|
94
|
+
* A normal save reuses its own validation computation when inputs/results are
|
|
95
|
+
* unchanged. Separate validations/saves do not share that computation cache.
|
|
96
|
+
* Validation-disabled saves still compute and reject getter/casting failures,
|
|
97
|
+
* but intentionally skip required/custom validation.
|
|
98
|
+
*
|
|
99
|
+
* Query writes, replacements, insertMany/bulkWrite fast paths, raw-driver writes,
|
|
100
|
+
* validateSync(), middleware bypass, and restricted pathsToSave are outside the
|
|
101
|
+
* document-save contract. Some bulk insert paths invoke async document validation
|
|
102
|
+
* and therefore compute incidentally; that is not all-writer consistency.
|
|
103
|
+
* Backfill old rows/formula changes explicitly; ordinary Mongoose concurrent-write
|
|
104
|
+
* semantics apply (consider optimisticConcurrency for source/result invariants).
|
|
105
|
+
*/
|
|
106
|
+
declare function derivedFieldsPlugin<TRawDocType>(schema: Schema<TRawDocType>): void;
|
|
107
|
+
|
|
108
|
+
export { type DerivedFieldDefinition, type DerivedFieldDependencyIssue, type DerivedFieldDescriptor, type DerivedFieldGetter, type DerivedFieldInput, DerivedFieldsDependencyError, defineDerivedField, derivedFieldsPlugin };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Schema } from 'mongoose';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A partial plain persisted-model view, not a hydrated document. Only available,
|
|
5
|
+
* supported root scalar inputs are supplied; virtuals, populated documents,
|
|
6
|
+
* containers, derived targets, and Mongoose's managed version key are not inputs.
|
|
7
|
+
* Use the raw model type for `TModel`. Selected defaults are available; genuine
|
|
8
|
+
* selected absence is `undefined` and stored `null` is known. Defaults applied to
|
|
9
|
+
* unselected fields never prove availability, even when pending a write.
|
|
10
|
+
* Dates/ObjectIds/Decimal128s are copied per invocation; property getters/
|
|
11
|
+
* transforms are bypassed.
|
|
12
|
+
* Never mutate the hydrated document through a captured reference.
|
|
13
|
+
*/
|
|
14
|
+
type DerivedFieldInput<TModel> = Readonly<Partial<TModel>>;
|
|
15
|
+
/** Sync/async computation with no document `this`; `undefined` clears the target. */
|
|
16
|
+
type DerivedFieldGetter<TModel, TValue> = (this: void, doc: DerivedFieldInput<TModel>) => TValue | undefined | Promise<TValue | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Declare every source read by `get`. Existing documents skip when no dependency
|
|
19
|
+
* modification/default write or target assignment/default write requests work.
|
|
20
|
+
* Pending non-null defaults count even without Mongoose modification flags;
|
|
21
|
+
* null/undefined defaults alone do not write. Every requested computation requires
|
|
22
|
+
* all dependencies selected or fully replaced; excluded defaults are not replacements.
|
|
23
|
+
* The Mongoose-managed version key is not a supported dependency or target.
|
|
24
|
+
*/
|
|
25
|
+
interface DerivedFieldDescriptor<TModel, TValue> {
|
|
26
|
+
readonly dependsOn: readonly Extract<keyof TModel, string>[];
|
|
27
|
+
readonly get: DerivedFieldGetter<TModel, TValue>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `derived` schema-path option. A bare getter recomputes on every operation
|
|
31
|
+
* and requires every supported non-derived schema input to be available.
|
|
32
|
+
*/
|
|
33
|
+
type DerivedFieldDefinition<TModel, TValue> = DerivedFieldDescriptor<TModel, TValue> | DerivedFieldGetter<TModel, TValue>;
|
|
34
|
+
/**
|
|
35
|
+
* Contextually types a `derived` option without augmenting Mongoose globally.
|
|
36
|
+
* The second type argument is the target's key in the raw persisted model, so
|
|
37
|
+
* dependency names, getter input, and the target's result type are checked.
|
|
38
|
+
* Schema registration separately checks runtime path eligibility.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* type User = { firstName: string; lastName: string; fullName: string };
|
|
42
|
+
* const derived = defineDerivedField<User, 'fullName'>({
|
|
43
|
+
* dependsOn: ['firstName', 'lastName'],
|
|
44
|
+
* get: (doc) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim(),
|
|
45
|
+
* });
|
|
46
|
+
*/
|
|
47
|
+
declare function defineDerivedField<TModel, TTargetKey extends Extract<keyof TModel, string>>(definition: DerivedFieldDescriptor<TModel, TModel[TTargetKey]>): DerivedFieldDescriptor<TModel, TModel[TTargetKey]>;
|
|
48
|
+
declare function defineDerivedField<TModel, TTargetKey extends Extract<keyof TModel, string>>(definition: DerivedFieldGetter<TModel, TModel[TTargetKey]>): DerivedFieldGetter<TModel, TModel[TTargetKey]>;
|
|
49
|
+
declare function defineDerivedField<TModel, TTargetKey extends Extract<keyof TModel, string>>(definition: DerivedFieldDefinition<TModel, TModel[TTargetKey]>): DerivedFieldDefinition<TModel, TModel[TTargetKey]>;
|
|
50
|
+
/** One target's unavailable dependency paths; never includes source values. */
|
|
51
|
+
interface DerivedFieldDependencyIssue {
|
|
52
|
+
readonly targetPath: string;
|
|
53
|
+
readonly missingPaths: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Raised by dependency preflight before any getter runs. `issues` lists every
|
|
57
|
+
* scheduled target with missing inputs. No implicit database fetch is attempted.
|
|
58
|
+
*/
|
|
59
|
+
declare class DerivedFieldsDependencyError extends Error {
|
|
60
|
+
/** Stable discriminator, including across separately bundled entrypoints. */
|
|
61
|
+
readonly code: "MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES";
|
|
62
|
+
/** Frozen path-only issues; no source values or hydrated document are attached. */
|
|
63
|
+
readonly issues: readonly DerivedFieldDependencyIssue[];
|
|
64
|
+
constructor(issues: readonly DerivedFieldDependencyIssue[]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Computes opt-in root scalar `derived` fields before asynchronous document
|
|
69
|
+
* validation and hydrated `save()` (including `Model.create()`). Results are
|
|
70
|
+
* staged, then assigned through Mongoose setters/casting; `undefined` clears a
|
|
71
|
+
* target. Getters receive separate frozen plain views with copied scalar values.
|
|
72
|
+
* Supported targets/dependencies: root String, Number, Boolean, Date, ObjectId,
|
|
73
|
+
* Decimal128, BigInt; containers, nested paths, and derived chains are rejected.
|
|
74
|
+
* Mongoose's managed version key (default `__v` or custom) is rejected as a
|
|
75
|
+
* target/dependency and omitted from inputs; with versionKey:false it is ordinary.
|
|
76
|
+
*
|
|
77
|
+
* Register after defining paths and source-mutating middleware, before compiling
|
|
78
|
+
* the model. Source-changing pre-validation hooks must precede this plugin for
|
|
79
|
+
* standalone validation to see their final values. Earlier pre-save changes are
|
|
80
|
+
* recomputed and, unless validation is disabled, revalidated. Changes made by
|
|
81
|
+
* later pre-save hooks are outside this guarantee. Validation hooks/validators
|
|
82
|
+
* must not change inputs after this plugin computes them: a save that still has
|
|
83
|
+
* such changes after revalidation rejects with an ordering error.
|
|
84
|
+
*
|
|
85
|
+
* No inputs are fetched. Explicit descriptors skip unless a dependency modification
|
|
86
|
+
* or target assignment requests work, including actual pending default writes.
|
|
87
|
+
* Mongoose writes non-null defaults (including '', 0, false) without modification
|
|
88
|
+
* flags; null/undefined defaults alone do not write. Pending defaults schedule work
|
|
89
|
+
* but do not prove excluded inputs available: all dependencies must be selected or
|
|
90
|
+
* fully replaced. Missing/projected-out targets without a write request still skip.
|
|
91
|
+
* Bare getters run on every validation/save and require every supported input.
|
|
92
|
+
* New documents retain their supplied/defaulted/absent input availability on
|
|
93
|
+
* later saves, including when constructed with projection metadata.
|
|
94
|
+
* A normal save reuses its own validation computation when inputs/results are
|
|
95
|
+
* unchanged. Separate validations/saves do not share that computation cache.
|
|
96
|
+
* Validation-disabled saves still compute and reject getter/casting failures,
|
|
97
|
+
* but intentionally skip required/custom validation.
|
|
98
|
+
*
|
|
99
|
+
* Query writes, replacements, insertMany/bulkWrite fast paths, raw-driver writes,
|
|
100
|
+
* validateSync(), middleware bypass, and restricted pathsToSave are outside the
|
|
101
|
+
* document-save contract. Some bulk insert paths invoke async document validation
|
|
102
|
+
* and therefore compute incidentally; that is not all-writer consistency.
|
|
103
|
+
* Backfill old rows/formula changes explicitly; ordinary Mongoose concurrent-write
|
|
104
|
+
* semantics apply (consider optimisticConcurrency for source/result invariants).
|
|
105
|
+
*/
|
|
106
|
+
declare function derivedFieldsPlugin<TRawDocType>(schema: Schema<TRawDocType>): void;
|
|
107
|
+
|
|
108
|
+
export { type DerivedFieldDefinition, type DerivedFieldDependencyIssue, type DerivedFieldDescriptor, type DerivedFieldGetter, type DerivedFieldInput, DerivedFieldsDependencyError, defineDerivedField, derivedFieldsPlugin };
|