rads-db 3.0.23 → 3.0.25
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/dist/index.cjs +64 -165
- package/dist/index.d.ts +11 -1
- package/dist/index.mjs +61 -166
- package/features/eventSourcing.cjs +153 -0
- package/features/eventSourcing.d.ts +2 -0
- package/features/eventSourcing.mjs +131 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -463,163 +463,6 @@ async function handleEffectsAfterPut(context, docs, beforePutResults, ctx) {
|
|
|
463
463
|
);
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
const mergeFn = createMerge__default({
|
|
467
|
-
mergeArray(options) {
|
|
468
|
-
const clone = options.clone;
|
|
469
|
-
return function(target, source) {
|
|
470
|
-
return clone(source);
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
function merge(target, source) {
|
|
475
|
-
cleanUndefined(source);
|
|
476
|
-
const result = mergeFn(target, source);
|
|
477
|
-
return result;
|
|
478
|
-
}
|
|
479
|
-
function cleanUndefined(obj) {
|
|
480
|
-
if (!obj || !___default.isPlainObject(obj))
|
|
481
|
-
return;
|
|
482
|
-
cleanUndefinedInner(obj);
|
|
483
|
-
}
|
|
484
|
-
function cleanUndefinedInner(obj) {
|
|
485
|
-
for (const key in obj) {
|
|
486
|
-
if (obj[key] === void 0)
|
|
487
|
-
delete obj[key];
|
|
488
|
-
if (___default.isPlainObject(obj[key]))
|
|
489
|
-
cleanUndefinedInner(obj[key]);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function diff(object, base, includeDenormFields = false) {
|
|
494
|
-
if (!base)
|
|
495
|
-
return object;
|
|
496
|
-
return ___default.transform(object, (result, value, key) => {
|
|
497
|
-
const oldValue = base[key];
|
|
498
|
-
if (!___default.isEqual(value, oldValue)) {
|
|
499
|
-
if (mustUseDeepDiff(value, oldValue)) {
|
|
500
|
-
const isRelationField = (value.id || oldValue.id) && key !== "change";
|
|
501
|
-
if (isRelationField && !includeDenormFields) {
|
|
502
|
-
if (value.id !== oldValue.id)
|
|
503
|
-
result[key] = value;
|
|
504
|
-
} else {
|
|
505
|
-
const subDiff = diff(value, oldValue, includeDenormFields);
|
|
506
|
-
if (!___default.isEmpty(subDiff))
|
|
507
|
-
result[key] = subDiff;
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
result[key] = value;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
function mustUseDeepDiff(value, oldValue) {
|
|
516
|
-
return ___default.isObject(value) && ___default.isObject(oldValue) && !___default.isArray(value) && !___default.isArray(oldValue);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function verifyEventSourcingSetup(schema, effects) {
|
|
520
|
-
for (const entityName in schema) {
|
|
521
|
-
if (!schema[entityName].decorators.entity)
|
|
522
|
-
continue;
|
|
523
|
-
if (schema[entityName].decorators.precomputed?.preset !== "eventSourcing")
|
|
524
|
-
continue;
|
|
525
|
-
const eventEntityName = `${entityName}Event`;
|
|
526
|
-
const aggregateRelationField = ___default.lowerFirst(entityName);
|
|
527
|
-
validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField);
|
|
528
|
-
effects[eventEntityName].push({
|
|
529
|
-
async beforePut(context, docs, ctx) {
|
|
530
|
-
const docsByAggId = {};
|
|
531
|
-
const docsById = {};
|
|
532
|
-
for (const d of docs) {
|
|
533
|
-
if (!d.doc.id || docsById[d.doc.id])
|
|
534
|
-
continue;
|
|
535
|
-
docsById[d.doc.id] = d.doc;
|
|
536
|
-
const aggId = d.doc[aggregateRelationField].id;
|
|
537
|
-
if (!aggId)
|
|
538
|
-
throw new Error(`Missing ${entityName}.${aggregateRelationField}.id`);
|
|
539
|
-
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
540
|
-
throw new Error(`Field ${entityName}.${aggregateRelationField}.id cannot be changed`);
|
|
541
|
-
}
|
|
542
|
-
if (!docsByAggId[aggId])
|
|
543
|
-
docsByAggId[aggId] = [];
|
|
544
|
-
docsByAggId[aggId].push(d.doc);
|
|
545
|
-
}
|
|
546
|
-
const existingEvents = await context.drivers[eventEntityName].getAll(
|
|
547
|
-
{
|
|
548
|
-
where: {
|
|
549
|
-
[aggregateRelationField]: { id_in: ___default.keys(docsByAggId) },
|
|
550
|
-
_not: { id_in: docs.map((d) => d.doc.id) }
|
|
551
|
-
}
|
|
552
|
-
},
|
|
553
|
-
ctx
|
|
554
|
-
);
|
|
555
|
-
const existingEventsByAggId = ___default.groupBy(existingEvents, (ev) => ev[aggregateRelationField].id);
|
|
556
|
-
const existingAggregates = await context.drivers[entityName].getAll(
|
|
557
|
-
{ where: { id_in: ___default.keys(docsByAggId) } },
|
|
558
|
-
ctx
|
|
559
|
-
);
|
|
560
|
-
const existingAggregatesById = ___default.keyBy(existingAggregates, "id");
|
|
561
|
-
const result = [];
|
|
562
|
-
for (const aggId in docsByAggId) {
|
|
563
|
-
const events = ___default.orderBy([...docsByAggId[aggId], ...existingEventsByAggId[aggId] || []], ["date"], "asc");
|
|
564
|
-
if (events[0].type !== "creation")
|
|
565
|
-
throw new Error(`First event must have type = "creation". (type: ${events[0].type}, id: ${events[0].id})`);
|
|
566
|
-
if (events.slice(1).some((ev) => ev.type === "creation")) {
|
|
567
|
-
throw new Error(`Only first event may have type = "creation"`);
|
|
568
|
-
}
|
|
569
|
-
let aggDoc = { id: aggId };
|
|
570
|
-
for (const ev of events) {
|
|
571
|
-
const newAggDoc = context.validators[entityName](merge(aggDoc, ev.change));
|
|
572
|
-
ev.change = diff(newAggDoc, aggDoc);
|
|
573
|
-
aggDoc = newAggDoc;
|
|
574
|
-
}
|
|
575
|
-
result.push({ aggDoc, events });
|
|
576
|
-
}
|
|
577
|
-
await handlePrecomputed(
|
|
578
|
-
{ ...context, typeName: entityName },
|
|
579
|
-
result.map((v) => ({ doc: v.aggDoc, events: v.events, oldDoc: existingAggregatesById[v.aggDoc.id] })),
|
|
580
|
-
ctx
|
|
581
|
-
);
|
|
582
|
-
return result;
|
|
583
|
-
},
|
|
584
|
-
async afterPut(context, docs, beforePutResult, ctx) {
|
|
585
|
-
const aggs = beforePutResult.map((d) => d.aggDoc);
|
|
586
|
-
await context.drivers[entityName].putMany(aggs, ctx);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField) {
|
|
592
|
-
const eventEntity = schema[eventEntityName];
|
|
593
|
-
if (!eventEntity)
|
|
594
|
-
throw new Error(
|
|
595
|
-
`You must create entity with name "${eventEntityName}" to use eventSourcing feature with "${entityName}"`
|
|
596
|
-
);
|
|
597
|
-
if (!eventEntity.decorators.entity)
|
|
598
|
-
throw new Error(`"${eventEntityName}" must have "@entity()" decorator.`);
|
|
599
|
-
const expectedFields = {
|
|
600
|
-
id: "string",
|
|
601
|
-
type: "enum",
|
|
602
|
-
change: entityName,
|
|
603
|
-
[aggregateRelationField]: entityName,
|
|
604
|
-
date: "string"
|
|
605
|
-
};
|
|
606
|
-
for (const f in expectedFields) {
|
|
607
|
-
const field = eventEntity.fields?.[f];
|
|
608
|
-
if (!field)
|
|
609
|
-
throw new Error(`"${eventEntityName}" must have field "${f}"`);
|
|
610
|
-
if (expectedFields[f] === "enum") {
|
|
611
|
-
if (!schema[field.type]?.enumValues?.creation)
|
|
612
|
-
throw new Error(`Field "${eventEntityName}.${f}" must be enum that accepts value 'creation'`);
|
|
613
|
-
} else {
|
|
614
|
-
if (field.type !== expectedFields[f])
|
|
615
|
-
throw new Error(`"${eventEntityName}.${f}" must have type "${expectedFields[f]}"`);
|
|
616
|
-
if (f === "change" && !field.isChange) {
|
|
617
|
-
throw new Error(`"${eventEntityName}.${f}" must have type "Change<${expectedFields[f]}>"`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
466
|
function verifyRelationsSetup(schema, effects) {
|
|
624
467
|
for (const entityName in schema) {
|
|
625
468
|
const entity = schema[entityName];
|
|
@@ -700,18 +543,70 @@ async function fillDenormFieldsBeforePut(computedContext, docUpdates, ctx) {
|
|
|
700
543
|
await Promise.all(promises);
|
|
701
544
|
}
|
|
702
545
|
|
|
703
|
-
function cleanUndefinedAndNull(obj) {
|
|
546
|
+
function cleanUndefinedAndNull(obj, isChange = false) {
|
|
547
|
+
for (const key in obj) {
|
|
548
|
+
if (obj[key] == null && !isChange)
|
|
549
|
+
delete obj[key];
|
|
550
|
+
if (___default.isPlainObject(obj[key]))
|
|
551
|
+
cleanUndefinedAndNull(obj[key], key === "change");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const mergeFn = createMerge__default({
|
|
556
|
+
mergeArray(options) {
|
|
557
|
+
const clone = options.clone;
|
|
558
|
+
return function(target, source) {
|
|
559
|
+
return clone(source);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
function merge(target, source) {
|
|
564
|
+
cleanUndefined(source);
|
|
565
|
+
const result = mergeFn(target, source);
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
function cleanUndefined(obj) {
|
|
569
|
+
if (!obj || !___default.isPlainObject(obj))
|
|
570
|
+
return;
|
|
571
|
+
cleanUndefinedInner(obj);
|
|
572
|
+
}
|
|
573
|
+
function cleanUndefinedInner(obj) {
|
|
704
574
|
for (const key in obj) {
|
|
705
|
-
if (obj[key]
|
|
575
|
+
if (obj[key] === void 0)
|
|
706
576
|
delete obj[key];
|
|
707
577
|
if (___default.isPlainObject(obj[key]))
|
|
708
|
-
|
|
578
|
+
cleanUndefinedInner(obj[key]);
|
|
709
579
|
}
|
|
710
580
|
}
|
|
711
581
|
|
|
582
|
+
function diff(object, base, includeDenormFields = false) {
|
|
583
|
+
if (!base)
|
|
584
|
+
return object;
|
|
585
|
+
return ___default.transform(object, (result, value, key) => {
|
|
586
|
+
const oldValue = base[key];
|
|
587
|
+
if (!___default.isEqual(value, oldValue)) {
|
|
588
|
+
if (mustUseDeepDiff(value, oldValue)) {
|
|
589
|
+
const isRelationField = (value.id || oldValue.id) && key !== "change";
|
|
590
|
+
if (isRelationField && !includeDenormFields) {
|
|
591
|
+
if (value.id !== oldValue.id)
|
|
592
|
+
result[key] = value;
|
|
593
|
+
} else {
|
|
594
|
+
const subDiff = diff(value, oldValue, includeDenormFields);
|
|
595
|
+
if (!___default.isEmpty(subDiff))
|
|
596
|
+
result[key] = subDiff;
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
result[key] = value;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function mustUseDeepDiff(value, oldValue) {
|
|
605
|
+
return ___default.isObject(value) && ___default.isObject(oldValue) && !___default.isArray(value) && !___default.isArray(oldValue);
|
|
606
|
+
}
|
|
607
|
+
|
|
712
608
|
function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
713
609
|
const { schema, options, validators } = computedContext;
|
|
714
|
-
const mustCleanNulls = driverInstance.driverName !== "restApi";
|
|
715
610
|
const { precomputedFields } = schema[key];
|
|
716
611
|
async function getMany(args, ctx) {
|
|
717
612
|
args = { ...args, where: { ...args?.where } };
|
|
@@ -743,7 +638,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
|
743
638
|
const docArgsToSave = docs.map((doc) => {
|
|
744
639
|
const oldDoc = oldDocsById[doc.id];
|
|
745
640
|
doc = merge(oldDoc, doc);
|
|
746
|
-
if (
|
|
641
|
+
if (!options.keepNulls)
|
|
747
642
|
cleanUndefinedAndNull(doc);
|
|
748
643
|
return { oldDoc, doc };
|
|
749
644
|
});
|
|
@@ -821,7 +716,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
|
821
716
|
const oldDoc = await driverInstance.get({ where: { id: doc.id } }, ctx);
|
|
822
717
|
if (oldDoc)
|
|
823
718
|
doc = merge(oldDoc, doc);
|
|
824
|
-
if (
|
|
719
|
+
if (!options.keepNulls)
|
|
825
720
|
cleanUndefinedAndNull(doc);
|
|
826
721
|
const docArgsToSave = [{ doc, oldDoc }];
|
|
827
722
|
await handlePrecomputed(computedContext, docArgsToSave, ctx);
|
|
@@ -863,7 +758,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
|
863
758
|
delete doc[p];
|
|
864
759
|
}
|
|
865
760
|
}
|
|
866
|
-
if (
|
|
761
|
+
if (!options.keepNulls)
|
|
867
762
|
cleanUndefinedAndNull(doc);
|
|
868
763
|
return { oldDoc, doc };
|
|
869
764
|
});
|
|
@@ -1007,7 +902,6 @@ function generateMethods(schema, validators, options) {
|
|
|
1007
902
|
if (!opts.noComputed) {
|
|
1008
903
|
verifyComputedPresense(schema, opts.computed, effects);
|
|
1009
904
|
}
|
|
1010
|
-
verifyEventSourcingSetup(schema, effects);
|
|
1011
905
|
verifyRelationsSetup(schema, effects);
|
|
1012
906
|
const computedContextGlobal = {
|
|
1013
907
|
schema,
|
|
@@ -1243,6 +1137,7 @@ function createRadsDbClient(args) {
|
|
|
1243
1137
|
...args,
|
|
1244
1138
|
noComputed: true,
|
|
1245
1139
|
noCustomDrivers: true,
|
|
1140
|
+
keepNulls: true,
|
|
1246
1141
|
driver: restApi(args?.driver)
|
|
1247
1142
|
};
|
|
1248
1143
|
const s = radsDbArgs.schema || _radsDb.schema;
|
|
@@ -1250,12 +1145,16 @@ function createRadsDbClient(args) {
|
|
|
1250
1145
|
return generateMethods(s, validators, radsDbArgs);
|
|
1251
1146
|
}
|
|
1252
1147
|
|
|
1148
|
+
exports.cleanUndefinedAndNull = cleanUndefinedAndNull;
|
|
1253
1149
|
exports.computed = computed;
|
|
1254
1150
|
exports.createRadsDb = createRadsDb;
|
|
1255
1151
|
exports.createRadsDbClient = createRadsDbClient;
|
|
1152
|
+
exports.diff = diff;
|
|
1256
1153
|
exports.entity = entity;
|
|
1257
1154
|
exports.field = field;
|
|
1258
1155
|
exports.getDriverInstance = getDriverInstance;
|
|
1156
|
+
exports.handlePrecomputed = handlePrecomputed;
|
|
1157
|
+
exports.merge = merge;
|
|
1259
1158
|
exports.precomputed = precomputed;
|
|
1260
1159
|
exports.ui = ui;
|
|
1261
1160
|
exports.validate = validate;
|
package/dist/index.d.ts
CHANGED
|
@@ -186,6 +186,7 @@ interface CreateRadsDbArgs {
|
|
|
186
186
|
beforeGet?: (args: GetArgsAny, ctx: RadsRequestContext, context: ComputedContext) => MaybePromise<void>;
|
|
187
187
|
context?: Record<string, any> & Omit<RadsRequestContext, 'dryRun' | 'silent'>;
|
|
188
188
|
features?: RadsFeature[];
|
|
189
|
+
keepNulls?: boolean;
|
|
189
190
|
}
|
|
190
191
|
type CreateRadsDbArgsNormalized = RequiredFields<CreateRadsDbArgs, 'computed' | 'features'> & {
|
|
191
192
|
driver: CreateRadsArgsDrivers;
|
|
@@ -386,6 +387,15 @@ declare function computed(meta?: ComputedDecoratorArgs): (a: any, b?: ClassField
|
|
|
386
387
|
|
|
387
388
|
declare function getDriverInstance(schema: Schema, key: string, driverConstructor: DriverConstructor, driverInstances: Record<string, Driver>): Driver;
|
|
388
389
|
|
|
390
|
+
declare function handlePrecomputed(context: ComputedContext, docs: any[], ctx: RadsRequestContext): Promise<void>;
|
|
391
|
+
|
|
392
|
+
/** Performs immutable deep merge */
|
|
393
|
+
declare function merge<T extends Record<string, any>>(target: T, source: any): T;
|
|
394
|
+
|
|
395
|
+
declare function diff(object: any, base: any, includeDenormFields?: boolean): any;
|
|
396
|
+
|
|
397
|
+
declare function cleanUndefinedAndNull(obj: Record<string, any>, isChange?: boolean): void;
|
|
398
|
+
|
|
389
399
|
/**
|
|
390
400
|
* Creates instance of rads db - object that provides access to all entities.
|
|
391
401
|
* Intended to be used on the backend. On the client you may want to opt for "createRadsDbClient" instead.
|
|
@@ -398,4 +408,4 @@ declare function createRadsDb(args?: CreateRadsDbArgs): RadsDb;
|
|
|
398
408
|
*/
|
|
399
409
|
declare function createRadsDbClient(args?: CreateRadsDbClientArgs): RadsDb;
|
|
400
410
|
|
|
401
|
-
export { Change, ComputedContext, ComputedContextGlobal, ComputedDecoratorArgs, CreateRadsArgsDrivers, CreateRadsDbArgs, CreateRadsDbArgsNormalized, CreateRadsDbClientArgs, DeepPartial, Driver, DriverConstructor, EntityDecoratorArgs, EntityMethods, EnumDefinition, FieldDecoratorArgs, FieldDefinition, FileSystemNode, FileUploadArgs, FileUploadDriver, FileUploadResult, GenerateClientNormalizedOptions, GenerateClientOptions, GetAggArgs, GetAggArgsAgg, GetAggArgsAny, GetAggResponse, GetArgs, GetArgsAny, GetArgsInclude, GetManyArgs, GetManyArgsAny, GetManyResponse, GetResponse, GetResponseInclude, GetResponseIncludeSelect, GetResponseNoInclude, GetRestRoutesArgs, GetRestRoutesOptions, GetRestRoutesResponse, MinimalDriver, PutArgs, PutEffect, RadsFeature, RadsRequestContext, RadsUiSlotDefinition, RadsUiSlotName, RadsVitePluginOptions, Relation, RequiredFields, RestDriverOptions, RestFileUploadDriverOptions, Schema, SchemaValidators, TypeDefinition, UiDecoratorArgs, UiFieldDecoratorArgs, ValidateEntityDecoratorArgs, ValidateFieldDecoratorArgs, ValidateStringDecoratorArgs, VerifyManyArgs, VerifyManyArgsAny, VerifyManyResponse, computed, createRadsDb, createRadsDbClient, entity, field, getDriverInstance, precomputed, ui, validate };
|
|
411
|
+
export { Change, ComputedContext, ComputedContextGlobal, ComputedDecoratorArgs, CreateRadsArgsDrivers, CreateRadsDbArgs, CreateRadsDbArgsNormalized, CreateRadsDbClientArgs, DeepPartial, Driver, DriverConstructor, EntityDecoratorArgs, EntityMethods, EnumDefinition, FieldDecoratorArgs, FieldDefinition, FileSystemNode, FileUploadArgs, FileUploadDriver, FileUploadResult, GenerateClientNormalizedOptions, GenerateClientOptions, GetAggArgs, GetAggArgsAgg, GetAggArgsAny, GetAggResponse, GetArgs, GetArgsAny, GetArgsInclude, GetManyArgs, GetManyArgsAny, GetManyResponse, GetResponse, GetResponseInclude, GetResponseIncludeSelect, GetResponseNoInclude, GetRestRoutesArgs, GetRestRoutesOptions, GetRestRoutesResponse, MinimalDriver, PutArgs, PutEffect, RadsFeature, RadsRequestContext, RadsUiSlotDefinition, RadsUiSlotName, RadsVitePluginOptions, Relation, RequiredFields, RestDriverOptions, RestFileUploadDriverOptions, Schema, SchemaValidators, TypeDefinition, UiDecoratorArgs, UiFieldDecoratorArgs, ValidateEntityDecoratorArgs, ValidateFieldDecoratorArgs, ValidateStringDecoratorArgs, VerifyManyArgs, VerifyManyArgsAny, VerifyManyResponse, cleanUndefinedAndNull, computed, createRadsDb, createRadsDbClient, diff, entity, field, getDriverInstance, handlePrecomputed, merge, precomputed, ui, validate };
|
package/dist/index.mjs
CHANGED
|
@@ -455,163 +455,6 @@ async function handleEffectsAfterPut(context, docs, beforePutResults, ctx) {
|
|
|
455
455
|
);
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
const mergeFn = createMerge({
|
|
459
|
-
mergeArray(options) {
|
|
460
|
-
const clone = options.clone;
|
|
461
|
-
return function(target, source) {
|
|
462
|
-
return clone(source);
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
function merge(target, source) {
|
|
467
|
-
cleanUndefined(source);
|
|
468
|
-
const result = mergeFn(target, source);
|
|
469
|
-
return result;
|
|
470
|
-
}
|
|
471
|
-
function cleanUndefined(obj) {
|
|
472
|
-
if (!obj || !_.isPlainObject(obj))
|
|
473
|
-
return;
|
|
474
|
-
cleanUndefinedInner(obj);
|
|
475
|
-
}
|
|
476
|
-
function cleanUndefinedInner(obj) {
|
|
477
|
-
for (const key in obj) {
|
|
478
|
-
if (obj[key] === void 0)
|
|
479
|
-
delete obj[key];
|
|
480
|
-
if (_.isPlainObject(obj[key]))
|
|
481
|
-
cleanUndefinedInner(obj[key]);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function diff(object, base, includeDenormFields = false) {
|
|
486
|
-
if (!base)
|
|
487
|
-
return object;
|
|
488
|
-
return _.transform(object, (result, value, key) => {
|
|
489
|
-
const oldValue = base[key];
|
|
490
|
-
if (!_.isEqual(value, oldValue)) {
|
|
491
|
-
if (mustUseDeepDiff(value, oldValue)) {
|
|
492
|
-
const isRelationField = (value.id || oldValue.id) && key !== "change";
|
|
493
|
-
if (isRelationField && !includeDenormFields) {
|
|
494
|
-
if (value.id !== oldValue.id)
|
|
495
|
-
result[key] = value;
|
|
496
|
-
} else {
|
|
497
|
-
const subDiff = diff(value, oldValue, includeDenormFields);
|
|
498
|
-
if (!_.isEmpty(subDiff))
|
|
499
|
-
result[key] = subDiff;
|
|
500
|
-
}
|
|
501
|
-
} else {
|
|
502
|
-
result[key] = value;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
function mustUseDeepDiff(value, oldValue) {
|
|
508
|
-
return _.isObject(value) && _.isObject(oldValue) && !_.isArray(value) && !_.isArray(oldValue);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function verifyEventSourcingSetup(schema, effects) {
|
|
512
|
-
for (const entityName in schema) {
|
|
513
|
-
if (!schema[entityName].decorators.entity)
|
|
514
|
-
continue;
|
|
515
|
-
if (schema[entityName].decorators.precomputed?.preset !== "eventSourcing")
|
|
516
|
-
continue;
|
|
517
|
-
const eventEntityName = `${entityName}Event`;
|
|
518
|
-
const aggregateRelationField = _.lowerFirst(entityName);
|
|
519
|
-
validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField);
|
|
520
|
-
effects[eventEntityName].push({
|
|
521
|
-
async beforePut(context, docs, ctx) {
|
|
522
|
-
const docsByAggId = {};
|
|
523
|
-
const docsById = {};
|
|
524
|
-
for (const d of docs) {
|
|
525
|
-
if (!d.doc.id || docsById[d.doc.id])
|
|
526
|
-
continue;
|
|
527
|
-
docsById[d.doc.id] = d.doc;
|
|
528
|
-
const aggId = d.doc[aggregateRelationField].id;
|
|
529
|
-
if (!aggId)
|
|
530
|
-
throw new Error(`Missing ${entityName}.${aggregateRelationField}.id`);
|
|
531
|
-
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
532
|
-
throw new Error(`Field ${entityName}.${aggregateRelationField}.id cannot be changed`);
|
|
533
|
-
}
|
|
534
|
-
if (!docsByAggId[aggId])
|
|
535
|
-
docsByAggId[aggId] = [];
|
|
536
|
-
docsByAggId[aggId].push(d.doc);
|
|
537
|
-
}
|
|
538
|
-
const existingEvents = await context.drivers[eventEntityName].getAll(
|
|
539
|
-
{
|
|
540
|
-
where: {
|
|
541
|
-
[aggregateRelationField]: { id_in: _.keys(docsByAggId) },
|
|
542
|
-
_not: { id_in: docs.map((d) => d.doc.id) }
|
|
543
|
-
}
|
|
544
|
-
},
|
|
545
|
-
ctx
|
|
546
|
-
);
|
|
547
|
-
const existingEventsByAggId = _.groupBy(existingEvents, (ev) => ev[aggregateRelationField].id);
|
|
548
|
-
const existingAggregates = await context.drivers[entityName].getAll(
|
|
549
|
-
{ where: { id_in: _.keys(docsByAggId) } },
|
|
550
|
-
ctx
|
|
551
|
-
);
|
|
552
|
-
const existingAggregatesById = _.keyBy(existingAggregates, "id");
|
|
553
|
-
const result = [];
|
|
554
|
-
for (const aggId in docsByAggId) {
|
|
555
|
-
const events = _.orderBy([...docsByAggId[aggId], ...existingEventsByAggId[aggId] || []], ["date"], "asc");
|
|
556
|
-
if (events[0].type !== "creation")
|
|
557
|
-
throw new Error(`First event must have type = "creation". (type: ${events[0].type}, id: ${events[0].id})`);
|
|
558
|
-
if (events.slice(1).some((ev) => ev.type === "creation")) {
|
|
559
|
-
throw new Error(`Only first event may have type = "creation"`);
|
|
560
|
-
}
|
|
561
|
-
let aggDoc = { id: aggId };
|
|
562
|
-
for (const ev of events) {
|
|
563
|
-
const newAggDoc = context.validators[entityName](merge(aggDoc, ev.change));
|
|
564
|
-
ev.change = diff(newAggDoc, aggDoc);
|
|
565
|
-
aggDoc = newAggDoc;
|
|
566
|
-
}
|
|
567
|
-
result.push({ aggDoc, events });
|
|
568
|
-
}
|
|
569
|
-
await handlePrecomputed(
|
|
570
|
-
{ ...context, typeName: entityName },
|
|
571
|
-
result.map((v) => ({ doc: v.aggDoc, events: v.events, oldDoc: existingAggregatesById[v.aggDoc.id] })),
|
|
572
|
-
ctx
|
|
573
|
-
);
|
|
574
|
-
return result;
|
|
575
|
-
},
|
|
576
|
-
async afterPut(context, docs, beforePutResult, ctx) {
|
|
577
|
-
const aggs = beforePutResult.map((d) => d.aggDoc);
|
|
578
|
-
await context.drivers[entityName].putMany(aggs, ctx);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
function validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField) {
|
|
584
|
-
const eventEntity = schema[eventEntityName];
|
|
585
|
-
if (!eventEntity)
|
|
586
|
-
throw new Error(
|
|
587
|
-
`You must create entity with name "${eventEntityName}" to use eventSourcing feature with "${entityName}"`
|
|
588
|
-
);
|
|
589
|
-
if (!eventEntity.decorators.entity)
|
|
590
|
-
throw new Error(`"${eventEntityName}" must have "@entity()" decorator.`);
|
|
591
|
-
const expectedFields = {
|
|
592
|
-
id: "string",
|
|
593
|
-
type: "enum",
|
|
594
|
-
change: entityName,
|
|
595
|
-
[aggregateRelationField]: entityName,
|
|
596
|
-
date: "string"
|
|
597
|
-
};
|
|
598
|
-
for (const f in expectedFields) {
|
|
599
|
-
const field = eventEntity.fields?.[f];
|
|
600
|
-
if (!field)
|
|
601
|
-
throw new Error(`"${eventEntityName}" must have field "${f}"`);
|
|
602
|
-
if (expectedFields[f] === "enum") {
|
|
603
|
-
if (!schema[field.type]?.enumValues?.creation)
|
|
604
|
-
throw new Error(`Field "${eventEntityName}.${f}" must be enum that accepts value 'creation'`);
|
|
605
|
-
} else {
|
|
606
|
-
if (field.type !== expectedFields[f])
|
|
607
|
-
throw new Error(`"${eventEntityName}.${f}" must have type "${expectedFields[f]}"`);
|
|
608
|
-
if (f === "change" && !field.isChange) {
|
|
609
|
-
throw new Error(`"${eventEntityName}.${f}" must have type "Change<${expectedFields[f]}>"`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
458
|
function verifyRelationsSetup(schema, effects) {
|
|
616
459
|
for (const entityName in schema) {
|
|
617
460
|
const entity = schema[entityName];
|
|
@@ -692,18 +535,70 @@ async function fillDenormFieldsBeforePut(computedContext, docUpdates, ctx) {
|
|
|
692
535
|
await Promise.all(promises);
|
|
693
536
|
}
|
|
694
537
|
|
|
695
|
-
function cleanUndefinedAndNull(obj) {
|
|
538
|
+
function cleanUndefinedAndNull(obj, isChange = false) {
|
|
539
|
+
for (const key in obj) {
|
|
540
|
+
if (obj[key] == null && !isChange)
|
|
541
|
+
delete obj[key];
|
|
542
|
+
if (_.isPlainObject(obj[key]))
|
|
543
|
+
cleanUndefinedAndNull(obj[key], key === "change");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const mergeFn = createMerge({
|
|
548
|
+
mergeArray(options) {
|
|
549
|
+
const clone = options.clone;
|
|
550
|
+
return function(target, source) {
|
|
551
|
+
return clone(source);
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
function merge(target, source) {
|
|
556
|
+
cleanUndefined(source);
|
|
557
|
+
const result = mergeFn(target, source);
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
function cleanUndefined(obj) {
|
|
561
|
+
if (!obj || !_.isPlainObject(obj))
|
|
562
|
+
return;
|
|
563
|
+
cleanUndefinedInner(obj);
|
|
564
|
+
}
|
|
565
|
+
function cleanUndefinedInner(obj) {
|
|
696
566
|
for (const key in obj) {
|
|
697
|
-
if (obj[key]
|
|
567
|
+
if (obj[key] === void 0)
|
|
698
568
|
delete obj[key];
|
|
699
569
|
if (_.isPlainObject(obj[key]))
|
|
700
|
-
|
|
570
|
+
cleanUndefinedInner(obj[key]);
|
|
701
571
|
}
|
|
702
572
|
}
|
|
703
573
|
|
|
574
|
+
function diff(object, base, includeDenormFields = false) {
|
|
575
|
+
if (!base)
|
|
576
|
+
return object;
|
|
577
|
+
return _.transform(object, (result, value, key) => {
|
|
578
|
+
const oldValue = base[key];
|
|
579
|
+
if (!_.isEqual(value, oldValue)) {
|
|
580
|
+
if (mustUseDeepDiff(value, oldValue)) {
|
|
581
|
+
const isRelationField = (value.id || oldValue.id) && key !== "change";
|
|
582
|
+
if (isRelationField && !includeDenormFields) {
|
|
583
|
+
if (value.id !== oldValue.id)
|
|
584
|
+
result[key] = value;
|
|
585
|
+
} else {
|
|
586
|
+
const subDiff = diff(value, oldValue, includeDenormFields);
|
|
587
|
+
if (!_.isEmpty(subDiff))
|
|
588
|
+
result[key] = subDiff;
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
result[key] = value;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function mustUseDeepDiff(value, oldValue) {
|
|
597
|
+
return _.isObject(value) && _.isObject(oldValue) && !_.isArray(value) && !_.isArray(oldValue);
|
|
598
|
+
}
|
|
599
|
+
|
|
704
600
|
function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
705
601
|
const { schema, options, validators } = computedContext;
|
|
706
|
-
const mustCleanNulls = driverInstance.driverName !== "restApi";
|
|
707
602
|
const { precomputedFields } = schema[key];
|
|
708
603
|
async function getMany(args, ctx) {
|
|
709
604
|
args = { ...args, where: { ...args?.where } };
|
|
@@ -735,7 +630,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
|
735
630
|
const docArgsToSave = docs.map((doc) => {
|
|
736
631
|
const oldDoc = oldDocsById[doc.id];
|
|
737
632
|
doc = merge(oldDoc, doc);
|
|
738
|
-
if (
|
|
633
|
+
if (!options.keepNulls)
|
|
739
634
|
cleanUndefinedAndNull(doc);
|
|
740
635
|
return { oldDoc, doc };
|
|
741
636
|
});
|
|
@@ -813,7 +708,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
|
813
708
|
const oldDoc = await driverInstance.get({ where: { id: doc.id } }, ctx);
|
|
814
709
|
if (oldDoc)
|
|
815
710
|
doc = merge(oldDoc, doc);
|
|
816
|
-
if (
|
|
711
|
+
if (!options.keepNulls)
|
|
817
712
|
cleanUndefinedAndNull(doc);
|
|
818
713
|
const docArgsToSave = [{ doc, oldDoc }];
|
|
819
714
|
await handlePrecomputed(computedContext, docArgsToSave, ctx);
|
|
@@ -855,7 +750,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
|
|
|
855
750
|
delete doc[p];
|
|
856
751
|
}
|
|
857
752
|
}
|
|
858
|
-
if (
|
|
753
|
+
if (!options.keepNulls)
|
|
859
754
|
cleanUndefinedAndNull(doc);
|
|
860
755
|
return { oldDoc, doc };
|
|
861
756
|
});
|
|
@@ -999,7 +894,6 @@ function generateMethods(schema, validators, options) {
|
|
|
999
894
|
if (!opts.noComputed) {
|
|
1000
895
|
verifyComputedPresense(schema, opts.computed, effects);
|
|
1001
896
|
}
|
|
1002
|
-
verifyEventSourcingSetup(schema, effects);
|
|
1003
897
|
verifyRelationsSetup(schema, effects);
|
|
1004
898
|
const computedContextGlobal = {
|
|
1005
899
|
schema,
|
|
@@ -1235,6 +1129,7 @@ function createRadsDbClient(args) {
|
|
|
1235
1129
|
...args,
|
|
1236
1130
|
noComputed: true,
|
|
1237
1131
|
noCustomDrivers: true,
|
|
1132
|
+
keepNulls: true,
|
|
1238
1133
|
driver: restApi(args?.driver)
|
|
1239
1134
|
};
|
|
1240
1135
|
const s = radsDbArgs.schema || schema;
|
|
@@ -1242,4 +1137,4 @@ function createRadsDbClient(args) {
|
|
|
1242
1137
|
return generateMethods(s, validators, radsDbArgs);
|
|
1243
1138
|
}
|
|
1244
1139
|
|
|
1245
|
-
export { computed, createRadsDb, createRadsDbClient, entity, field, getDriverInstance, precomputed, ui, validate };
|
|
1140
|
+
export { cleanUndefinedAndNull, computed, createRadsDb, createRadsDbClient, diff, entity, field, getDriverInstance, handlePrecomputed, merge, precomputed, ui, validate };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
module.exports = void 0;
|
|
7
|
+
var _lodash = _interopRequireDefault(require("lodash"));
|
|
8
|
+
var _radsDb = require("rads-db");
|
|
9
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
10
|
+
var _default = () => {
|
|
11
|
+
return {
|
|
12
|
+
name: "eventSourcing",
|
|
13
|
+
init(db, context) {
|
|
14
|
+
verifyEventSourcingSetup(context.schema, context.effects);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
module.exports = _default;
|
|
19
|
+
function verifyEventSourcingSetup(schema, effects) {
|
|
20
|
+
for (const entityName in schema) {
|
|
21
|
+
if (!schema[entityName].decorators.entity) continue;
|
|
22
|
+
if (schema[entityName].decorators.precomputed?.preset !== "eventSourcing") continue;
|
|
23
|
+
const eventEntityName = `${entityName}Event`;
|
|
24
|
+
const aggregateRelationField = _lodash.default.lowerFirst(entityName);
|
|
25
|
+
validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField);
|
|
26
|
+
effects[eventEntityName].push({
|
|
27
|
+
async beforePut(context, docs, ctx) {
|
|
28
|
+
const docsByAggId = {};
|
|
29
|
+
const docsById = {};
|
|
30
|
+
for (const d of docs) {
|
|
31
|
+
if (!d.doc.id || docsById[d.doc.id]) continue;
|
|
32
|
+
docsById[d.doc.id] = d.doc;
|
|
33
|
+
const aggId = d.doc[aggregateRelationField].id;
|
|
34
|
+
if (!aggId) throw new Error(`Missing ${entityName}.${aggregateRelationField}.id`);
|
|
35
|
+
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
36
|
+
throw new Error(`Field ${entityName}.${aggregateRelationField}.id cannot be changed`);
|
|
37
|
+
}
|
|
38
|
+
if (!docsByAggId[aggId]) docsByAggId[aggId] = [];
|
|
39
|
+
docsByAggId[aggId].push(d.doc);
|
|
40
|
+
}
|
|
41
|
+
const existingEvents = await context.drivers[eventEntityName].getAll({
|
|
42
|
+
where: {
|
|
43
|
+
[aggregateRelationField]: {
|
|
44
|
+
id_in: _lodash.default.keys(docsByAggId)
|
|
45
|
+
},
|
|
46
|
+
_not: {
|
|
47
|
+
id_in: docs.map(d => d.doc.id)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, ctx);
|
|
51
|
+
const existingEventsByAggId = _lodash.default.groupBy(existingEvents, ev => ev[aggregateRelationField].id);
|
|
52
|
+
const existingAggregates = await context.drivers[entityName].getAll({
|
|
53
|
+
where: {
|
|
54
|
+
id_in: _lodash.default.keys(docsByAggId)
|
|
55
|
+
}
|
|
56
|
+
}, ctx);
|
|
57
|
+
const existingAggregatesById = _lodash.default.keyBy(existingAggregates, "id");
|
|
58
|
+
const result = [];
|
|
59
|
+
for (const aggId in docsByAggId) {
|
|
60
|
+
const events = _lodash.default.orderBy([...docsByAggId[aggId], ...(existingEventsByAggId[aggId] || [])], ["date"], "asc");
|
|
61
|
+
if (events[0].type !== "creation") throw new Error(`First event must have type = "creation". (type: ${events[0].type}, id: ${events[0].id})`);
|
|
62
|
+
if (events.slice(1).some(ev => ev.type === "creation")) {
|
|
63
|
+
throw new Error(`Only first event may have type = "creation"`);
|
|
64
|
+
}
|
|
65
|
+
let aggDoc;
|
|
66
|
+
for (const ev of events) {
|
|
67
|
+
const newAggDoc = context.validators[entityName]((0, _radsDb.merge)(aggDoc || {
|
|
68
|
+
id: aggId
|
|
69
|
+
}, ev.change));
|
|
70
|
+
ev.beforeChange = aggDoc;
|
|
71
|
+
ev.change = (0, _radsDb.diff)(newAggDoc, aggDoc);
|
|
72
|
+
aggDoc = newAggDoc;
|
|
73
|
+
if (!context.options.keepNulls) (0, _radsDb.cleanUndefinedAndNull)(aggDoc);
|
|
74
|
+
}
|
|
75
|
+
result.push({
|
|
76
|
+
aggDoc,
|
|
77
|
+
events
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
await (0, _radsDb.handlePrecomputed)({
|
|
81
|
+
...context,
|
|
82
|
+
typeName: entityName
|
|
83
|
+
}, result.map(v => ({
|
|
84
|
+
doc: v.aggDoc,
|
|
85
|
+
events: v.events,
|
|
86
|
+
oldDoc: existingAggregatesById[v.aggDoc.id]
|
|
87
|
+
})), ctx);
|
|
88
|
+
return result;
|
|
89
|
+
},
|
|
90
|
+
async afterPut(context, docs, beforePutResult, ctx) {
|
|
91
|
+
const aggs = beforePutResult.map(d => d.aggDoc);
|
|
92
|
+
await context.drivers[entityName].putMany(aggs, ctx);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField) {
|
|
98
|
+
const eventEntity = schema[eventEntityName];
|
|
99
|
+
if (!eventEntity) throw new Error(`You must create entity with name "${eventEntityName}" to use eventSourcing feature with "${entityName}"`);
|
|
100
|
+
if (!eventEntity.decorators.entity) throw new Error(`"${eventEntityName}" must have "@entity()" decorator.`);
|
|
101
|
+
const expectedFields = {
|
|
102
|
+
id: {
|
|
103
|
+
type: "string"
|
|
104
|
+
},
|
|
105
|
+
change: {
|
|
106
|
+
type: entityName,
|
|
107
|
+
isChange: true
|
|
108
|
+
},
|
|
109
|
+
type: {
|
|
110
|
+
isEnum: true
|
|
111
|
+
},
|
|
112
|
+
beforeChange: {
|
|
113
|
+
type: entityName,
|
|
114
|
+
isChange: false,
|
|
115
|
+
isRelation: false,
|
|
116
|
+
isRequired: false
|
|
117
|
+
},
|
|
118
|
+
[aggregateRelationField]: {
|
|
119
|
+
type: entityName,
|
|
120
|
+
isRelation: true
|
|
121
|
+
},
|
|
122
|
+
date: {
|
|
123
|
+
type: "string"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
for (const f in expectedFields) {
|
|
127
|
+
validateSchemaField(f, eventEntityName, expectedFields, schema);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function validateSchemaField(fieldName, eventEntityName, expectedFields, schema) {
|
|
131
|
+
const eventEntity = schema[eventEntityName];
|
|
132
|
+
const field = eventEntity.fields?.[fieldName];
|
|
133
|
+
if (!field) throw new Error(`"${eventEntityName}" must have field "${fieldName}"`);
|
|
134
|
+
const expectedField = expectedFields[fieldName];
|
|
135
|
+
if (expectedField.type && expectedField.type !== field.type) {
|
|
136
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must have type "${expectedField.type}"`);
|
|
137
|
+
}
|
|
138
|
+
if (expectedField.isEnum) {
|
|
139
|
+
if (!schema[field.type]?.enumValues?.creation) throw new Error(`Field "${eventEntityName}.${fieldName}" must be enum that accepts value 'creation'`);
|
|
140
|
+
}
|
|
141
|
+
if (expectedField.isChange != null && expectedField.isChange !== (field.isChange || false)) {
|
|
142
|
+
const expectedType = expectedField.isChange ? `Change<${expectedField.type}>` : expectedField.type;
|
|
143
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must have type "${expectedType}"`);
|
|
144
|
+
}
|
|
145
|
+
if (expectedField.isRelation != null && expectedField.isRelation !== (field.isRelation || false)) {
|
|
146
|
+
const expectedType = expectedField.isRelation ? `Relation<${expectedField.type}>` : expectedField.type;
|
|
147
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must have type "${expectedType}"`);
|
|
148
|
+
}
|
|
149
|
+
if (expectedField.isRequired != null && expectedField.isRequired !== (field.isRequired || false)) {
|
|
150
|
+
const expectedRequired = expectedField.isRequired ? `required` : "optional";
|
|
151
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must be ${expectedRequired}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import { cleanUndefinedAndNull, diff, handlePrecomputed, merge } from "rads-db";
|
|
3
|
+
export default () => {
|
|
4
|
+
return {
|
|
5
|
+
name: "eventSourcing",
|
|
6
|
+
init(db, context) {
|
|
7
|
+
verifyEventSourcingSetup(context.schema, context.effects);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
function verifyEventSourcingSetup(schema, effects) {
|
|
12
|
+
for (const entityName in schema) {
|
|
13
|
+
if (!schema[entityName].decorators.entity)
|
|
14
|
+
continue;
|
|
15
|
+
if (schema[entityName].decorators.precomputed?.preset !== "eventSourcing")
|
|
16
|
+
continue;
|
|
17
|
+
const eventEntityName = `${entityName}Event`;
|
|
18
|
+
const aggregateRelationField = _.lowerFirst(entityName);
|
|
19
|
+
validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField);
|
|
20
|
+
effects[eventEntityName].push({
|
|
21
|
+
async beforePut(context, docs, ctx) {
|
|
22
|
+
const docsByAggId = {};
|
|
23
|
+
const docsById = {};
|
|
24
|
+
for (const d of docs) {
|
|
25
|
+
if (!d.doc.id || docsById[d.doc.id])
|
|
26
|
+
continue;
|
|
27
|
+
docsById[d.doc.id] = d.doc;
|
|
28
|
+
const aggId = d.doc[aggregateRelationField].id;
|
|
29
|
+
if (!aggId)
|
|
30
|
+
throw new Error(`Missing ${entityName}.${aggregateRelationField}.id`);
|
|
31
|
+
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
32
|
+
throw new Error(`Field ${entityName}.${aggregateRelationField}.id cannot be changed`);
|
|
33
|
+
}
|
|
34
|
+
if (!docsByAggId[aggId])
|
|
35
|
+
docsByAggId[aggId] = [];
|
|
36
|
+
docsByAggId[aggId].push(d.doc);
|
|
37
|
+
}
|
|
38
|
+
const existingEvents = await context.drivers[eventEntityName].getAll(
|
|
39
|
+
{
|
|
40
|
+
where: {
|
|
41
|
+
[aggregateRelationField]: { id_in: _.keys(docsByAggId) },
|
|
42
|
+
_not: { id_in: docs.map((d) => d.doc.id) }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
ctx
|
|
46
|
+
);
|
|
47
|
+
const existingEventsByAggId = _.groupBy(existingEvents, (ev) => ev[aggregateRelationField].id);
|
|
48
|
+
const existingAggregates = await context.drivers[entityName].getAll(
|
|
49
|
+
{ where: { id_in: _.keys(docsByAggId) } },
|
|
50
|
+
ctx
|
|
51
|
+
);
|
|
52
|
+
const existingAggregatesById = _.keyBy(existingAggregates, "id");
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const aggId in docsByAggId) {
|
|
55
|
+
const events = _.orderBy([...docsByAggId[aggId], ...existingEventsByAggId[aggId] || []], ["date"], "asc");
|
|
56
|
+
if (events[0].type !== "creation")
|
|
57
|
+
throw new Error(`First event must have type = "creation". (type: ${events[0].type}, id: ${events[0].id})`);
|
|
58
|
+
if (events.slice(1).some((ev) => ev.type === "creation")) {
|
|
59
|
+
throw new Error(`Only first event may have type = "creation"`);
|
|
60
|
+
}
|
|
61
|
+
let aggDoc;
|
|
62
|
+
for (const ev of events) {
|
|
63
|
+
const newAggDoc = context.validators[entityName](merge(aggDoc || { id: aggId }, ev.change));
|
|
64
|
+
ev.beforeChange = aggDoc;
|
|
65
|
+
ev.change = diff(newAggDoc, aggDoc);
|
|
66
|
+
aggDoc = newAggDoc;
|
|
67
|
+
if (!context.options.keepNulls)
|
|
68
|
+
cleanUndefinedAndNull(aggDoc);
|
|
69
|
+
}
|
|
70
|
+
result.push({ aggDoc, events });
|
|
71
|
+
}
|
|
72
|
+
await handlePrecomputed(
|
|
73
|
+
{ ...context, typeName: entityName },
|
|
74
|
+
result.map((v) => ({ doc: v.aggDoc, events: v.events, oldDoc: existingAggregatesById[v.aggDoc.id] })),
|
|
75
|
+
ctx
|
|
76
|
+
);
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
async afterPut(context, docs, beforePutResult, ctx) {
|
|
80
|
+
const aggs = beforePutResult.map((d) => d.aggDoc);
|
|
81
|
+
await context.drivers[entityName].putMany(aggs, ctx);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function validateEventSourcingSetup(schema, entityName, eventEntityName, aggregateRelationField) {
|
|
87
|
+
const eventEntity = schema[eventEntityName];
|
|
88
|
+
if (!eventEntity)
|
|
89
|
+
throw new Error(
|
|
90
|
+
`You must create entity with name "${eventEntityName}" to use eventSourcing feature with "${entityName}"`
|
|
91
|
+
);
|
|
92
|
+
if (!eventEntity.decorators.entity)
|
|
93
|
+
throw new Error(`"${eventEntityName}" must have "@entity()" decorator.`);
|
|
94
|
+
const expectedFields = {
|
|
95
|
+
id: { type: "string" },
|
|
96
|
+
change: { type: entityName, isChange: true },
|
|
97
|
+
type: { isEnum: true },
|
|
98
|
+
beforeChange: { type: entityName, isChange: false, isRelation: false, isRequired: false },
|
|
99
|
+
[aggregateRelationField]: { type: entityName, isRelation: true },
|
|
100
|
+
date: { type: "string" }
|
|
101
|
+
};
|
|
102
|
+
for (const f in expectedFields) {
|
|
103
|
+
validateSchemaField(f, eventEntityName, expectedFields, schema);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function validateSchemaField(fieldName, eventEntityName, expectedFields, schema) {
|
|
107
|
+
const eventEntity = schema[eventEntityName];
|
|
108
|
+
const field = eventEntity.fields?.[fieldName];
|
|
109
|
+
if (!field)
|
|
110
|
+
throw new Error(`"${eventEntityName}" must have field "${fieldName}"`);
|
|
111
|
+
const expectedField = expectedFields[fieldName];
|
|
112
|
+
if (expectedField.type && expectedField.type !== field.type) {
|
|
113
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must have type "${expectedField.type}"`);
|
|
114
|
+
}
|
|
115
|
+
if (expectedField.isEnum) {
|
|
116
|
+
if (!schema[field.type]?.enumValues?.creation)
|
|
117
|
+
throw new Error(`Field "${eventEntityName}.${fieldName}" must be enum that accepts value 'creation'`);
|
|
118
|
+
}
|
|
119
|
+
if (expectedField.isChange != null && expectedField.isChange !== (field.isChange || false)) {
|
|
120
|
+
const expectedType = expectedField.isChange ? `Change<${expectedField.type}>` : expectedField.type;
|
|
121
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must have type "${expectedType}"`);
|
|
122
|
+
}
|
|
123
|
+
if (expectedField.isRelation != null && expectedField.isRelation !== (field.isRelation || false)) {
|
|
124
|
+
const expectedType = expectedField.isRelation ? `Relation<${expectedField.type}>` : expectedField.type;
|
|
125
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must have type "${expectedType}"`);
|
|
126
|
+
}
|
|
127
|
+
if (expectedField.isRequired != null && expectedField.isRequired !== (field.isRequired || false)) {
|
|
128
|
+
const expectedRequired = expectedField.isRequired ? `required` : "optional";
|
|
129
|
+
throw new Error(`"${eventEntityName}.${fieldName}" must be ${expectedRequired}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rads-db",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.25",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"drivers",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"test": "vitest --run && vitest typecheck --run",
|
|
108
108
|
"link-rads-db": "symlink-dir ./test/_rads-db ./node_modules/_rads-db",
|
|
109
109
|
"generate-test-schema": "rads-db",
|
|
110
|
-
"dev": "pnpm install && pnpm
|
|
110
|
+
"dev": "pnpm install && pnpm link-rads-db && pnpm build && pnpm generate-test-schema && vitest --ui --test-timeout 300000",
|
|
111
111
|
"lint": "cross-env NODE_ENV=production eslint src/**/*.* test/**/*.*",
|
|
112
112
|
"dev-typecheck": "pnpm install && pnpm build && pnpm generate-test-schema && pnpm link-rads-db && vitest typecheck --ui",
|
|
113
113
|
"build": "unbuild"
|