rads-db 3.2.18 → 3.2.21
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/config.cjs +2 -1
- package/dist/config.mjs +2 -1
- package/features/eventSourcing.cjs +78 -49
- package/features/eventSourcing.d.ts +1 -0
- package/features/eventSourcing.mjs +76 -57
- package/package.json +1 -1
package/dist/config.cjs
CHANGED
|
@@ -459,7 +459,8 @@ function verifyDefaultValueType(field, ctx2) {
|
|
|
459
459
|
throw new TypeError(`Default value type is different from field type: '${type}'`);
|
|
460
460
|
}
|
|
461
461
|
} else {
|
|
462
|
-
|
|
462
|
+
const normalizedType = type.startsWith("Record<") ? "object" : type;
|
|
463
|
+
if (supportedPrimitiveTypes.includes(type) && getPrimitiveTypeFromDefaultValue(defaultValue2) !== normalizedType) {
|
|
463
464
|
throw new Error(`Default value type is different from field type: '${type}'`);
|
|
464
465
|
}
|
|
465
466
|
if (!ctx2.result[type] && ctx2.typeNodesMap[type])
|
package/dist/config.mjs
CHANGED
|
@@ -451,7 +451,8 @@ function verifyDefaultValueType(field, ctx2) {
|
|
|
451
451
|
throw new TypeError(`Default value type is different from field type: '${type}'`);
|
|
452
452
|
}
|
|
453
453
|
} else {
|
|
454
|
-
|
|
454
|
+
const normalizedType = type.startsWith("Record<") ? "object" : type;
|
|
455
|
+
if (supportedPrimitiveTypes.includes(type) && getPrimitiveTypeFromDefaultValue(defaultValue2) !== normalizedType) {
|
|
455
456
|
throw new Error(`Default value type is different from field type: '${type}'`);
|
|
456
457
|
}
|
|
457
458
|
if (!ctx2.result[type] && ctx2.typeNodesMap[type])
|
|
@@ -27,24 +27,74 @@ function verifyEventSourcingSetup(schema, effects, options) {
|
|
|
27
27
|
effects[eventEntityName].push(effect);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
function groupEventsByAggId(docs, aggregateRelationField, eventEntityName) {
|
|
31
|
+
const docsByAggId = {};
|
|
32
|
+
const docsById = {};
|
|
33
|
+
for (const d of docs) {
|
|
34
|
+
if (!d.doc.id || docsById[d.doc.id]) continue;
|
|
35
|
+
docsById[d.doc.id] = d.doc;
|
|
36
|
+
const aggId = d.doc[aggregateRelationField].id;
|
|
37
|
+
if (!aggId) throw new Error(`Missing ${eventEntityName}.${aggregateRelationField}.id`);
|
|
38
|
+
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
39
|
+
throw new Error(`Field ${eventEntityName}.${aggregateRelationField}.id cannot be changed`);
|
|
40
|
+
}
|
|
41
|
+
if (!docsByAggId[aggId]) docsByAggId[aggId] = [];
|
|
42
|
+
docsByAggId[aggId].push(d.doc);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
docsByAggId,
|
|
46
|
+
docsById
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function applyEventToAggregate(ev, aggDoc, aggId, p) {
|
|
50
|
+
const mergedDoc = (0, _radsDb.merge)(aggDoc || {
|
|
51
|
+
id: aggId
|
|
52
|
+
}, ev.change);
|
|
53
|
+
const newAggDoc = p.context.validators[p.entityName](mergedDoc);
|
|
54
|
+
if (!p.options.freezeEvent?.(ev, p.context, p.ctx)) {
|
|
55
|
+
ev.beforeChange = aggDoc;
|
|
56
|
+
const originalChange = ev.change;
|
|
57
|
+
ev.change = (0, _radsDb.diff)(newAggDoc, aggDoc);
|
|
58
|
+
handleKeepHistory(p.schema[p.entityName].keepHistoryFields, originalChange, ev);
|
|
59
|
+
}
|
|
60
|
+
if (p.ctx.skipPrecomputed) {
|
|
61
|
+
for (const field of p.schema[p.entityName].precomputedFields || []) {
|
|
62
|
+
if (p.ctx.skipPrecomputed.includes(field) && mergedDoc[field] !== void 0) {
|
|
63
|
+
newAggDoc[field] = mergedDoc[field];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return newAggDoc;
|
|
68
|
+
}
|
|
69
|
+
function buildAggregateFromEvents(events, aggId, docsById, existingAggregatesById, p) {
|
|
70
|
+
if (events[0].type !== "creation") throw new Error(`First event must have type = "creation". (type: ${events[0].type}, id: ${events[0].id})`);
|
|
71
|
+
if (events.slice(1).some(ev => ev.type === "creation")) {
|
|
72
|
+
throw new Error(`Only first event may have type = "creation"`);
|
|
73
|
+
}
|
|
74
|
+
let aggDoc;
|
|
75
|
+
for (const ev of events) {
|
|
76
|
+
const newAggDoc = applyEventToAggregate(ev, aggDoc, aggId, p);
|
|
77
|
+
if (p.options.applyEventIf && !p.options.applyEventIf(ev, p.context, p.ctx)) continue;
|
|
78
|
+
aggDoc = newAggDoc;
|
|
79
|
+
if (!p.context.options.keepNulls) (0, _radsDb.cleanEntity)(aggDoc);
|
|
80
|
+
}
|
|
81
|
+
if (!aggDoc) return null;
|
|
82
|
+
return {
|
|
83
|
+
aggDoc,
|
|
84
|
+
events,
|
|
85
|
+
updatedEvents: events.filter(ev => docsById[ev.id]),
|
|
86
|
+
oldAggDoc: existingAggregatesById[aggDoc.id]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
30
89
|
function getEffectFor(entityName, aggregateRelationField, eventEntityName, schema, options) {
|
|
31
90
|
return {
|
|
32
91
|
featureName: "eventSourcing",
|
|
33
92
|
async beforePut(context, docs, ctx) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const aggId = d.doc[aggregateRelationField].id;
|
|
40
|
-
if (!aggId) throw new Error(`Missing ${eventEntityName}.${aggregateRelationField}.id`);
|
|
41
|
-
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
42
|
-
throw new Error(`Field ${eventEntityName}.${aggregateRelationField}.id cannot be changed`);
|
|
43
|
-
}
|
|
44
|
-
if (!docsByAggId[aggId]) docsByAggId[aggId] = [];
|
|
45
|
-
docsByAggId[aggId].push(d.doc);
|
|
46
|
-
}
|
|
47
|
-
const existingEvents = await context.drivers[eventEntityName].getAll({
|
|
93
|
+
const {
|
|
94
|
+
docsByAggId,
|
|
95
|
+
docsById
|
|
96
|
+
} = groupEventsByAggId(docs, aggregateRelationField, eventEntityName);
|
|
97
|
+
const [existingEvents, existingAggregates] = await Promise.all([context.drivers[eventEntityName].getAll({
|
|
48
98
|
where: {
|
|
49
99
|
[aggregateRelationField]: {
|
|
50
100
|
id_in: _lodash.default.keys(docsByAggId)
|
|
@@ -53,45 +103,24 @@ function getEffectFor(entityName, aggregateRelationField, eventEntityName, schem
|
|
|
53
103
|
id_in: docs.map(d => d.doc.id)
|
|
54
104
|
}
|
|
55
105
|
}
|
|
56
|
-
}, ctx)
|
|
57
|
-
const existingEventsByAggId = _lodash.default.groupBy(existingEvents, ev => ev[aggregateRelationField].id);
|
|
58
|
-
const existingAggregates = await context.drivers[entityName].getAll({
|
|
106
|
+
}, ctx), context.drivers[entityName].getAll({
|
|
59
107
|
where: {
|
|
60
108
|
id_in: _lodash.default.keys(docsByAggId)
|
|
61
109
|
}
|
|
62
|
-
}, ctx);
|
|
110
|
+
}, ctx)]);
|
|
111
|
+
const existingEventsByAggId = _lodash.default.groupBy(existingEvents, ev => ev[aggregateRelationField].id);
|
|
63
112
|
const existingAggregatesById = _lodash.default.keyBy(existingAggregates, "id");
|
|
64
|
-
const
|
|
65
|
-
|
|
113
|
+
const processor = {
|
|
114
|
+
context,
|
|
115
|
+
entityName,
|
|
116
|
+
schema,
|
|
117
|
+
options,
|
|
118
|
+
ctx
|
|
119
|
+
};
|
|
120
|
+
const result = _lodash.default.compact(_lodash.default.keys(docsByAggId).map(aggId => {
|
|
66
121
|
const events = _lodash.default.orderBy([...docsByAggId[aggId], ...(existingEventsByAggId[aggId] || [])], ["date"], "asc");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
throw new Error(`Only first event may have type = "creation"`);
|
|
70
|
-
}
|
|
71
|
-
let aggDoc;
|
|
72
|
-
for (const ev of events) {
|
|
73
|
-
const newAggDoc = context.validators[entityName]((0, _radsDb.merge)(aggDoc || {
|
|
74
|
-
id: aggId
|
|
75
|
-
}, ev.change));
|
|
76
|
-
ev.beforeChange = aggDoc;
|
|
77
|
-
const originalChange = ev.change;
|
|
78
|
-
ev.change = (0, _radsDb.diff)(newAggDoc, aggDoc);
|
|
79
|
-
handleKeepHistory(schema[entityName].keepHistoryFields, originalChange, ev);
|
|
80
|
-
if (options.applyEventIf && !options.applyEventIf(ev, context, ctx)) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
aggDoc = newAggDoc;
|
|
84
|
-
if (!context.options.keepNulls) (0, _radsDb.cleanEntity)(aggDoc);
|
|
85
|
-
}
|
|
86
|
-
if (aggDoc) {
|
|
87
|
-
result.push({
|
|
88
|
-
aggDoc,
|
|
89
|
-
events,
|
|
90
|
-
updatedEvents: events.filter(ev => docsById[ev.id]),
|
|
91
|
-
oldAggDoc: existingAggregatesById[aggDoc.id]
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
122
|
+
return buildAggregateFromEvents(events, aggId, docsById, existingAggregatesById, processor);
|
|
123
|
+
}));
|
|
95
124
|
await (0, _radsDb.handlePrecomputed)(
|
|
96
125
|
// @ts-expect-error TODO wrong types
|
|
97
126
|
{
|
|
@@ -105,7 +134,7 @@ function getEffectFor(entityName, aggregateRelationField, eventEntityName, schem
|
|
|
105
134
|
})), ctx);
|
|
106
135
|
return result;
|
|
107
136
|
},
|
|
108
|
-
async afterPut(context,
|
|
137
|
+
async afterPut(context, _docs, beforePutResult, ctx) {
|
|
109
138
|
const aggs = beforePutResult.map(d => d.aggDoc);
|
|
110
139
|
const hookItems = beforePutResult.map(v => ({
|
|
111
140
|
doc: v.aggDoc,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComputedContext, RadsFeature, RadsRequestContext } from '@/types';
|
|
2
2
|
export interface EventSourcingFeatureOptions {
|
|
3
3
|
applyEventIf?: (event: any, context: ComputedContext, ctx: RadsRequestContext) => boolean;
|
|
4
|
+
freezeEvent?: (event: any, context: ComputedContext, ctx: RadsRequestContext) => boolean;
|
|
4
5
|
}
|
|
5
6
|
declare const _default: (options?: EventSourcingFeatureOptions) => RadsFeature;
|
|
6
7
|
export default _default;
|
|
@@ -19,68 +19,87 @@ function verifyEventSourcingSetup(schema, effects, options) {
|
|
|
19
19
|
effects[eventEntityName].push(effect);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
function groupEventsByAggId(docs, aggregateRelationField, eventEntityName) {
|
|
23
|
+
const docsByAggId = {};
|
|
24
|
+
const docsById = {};
|
|
25
|
+
for (const d of docs) {
|
|
26
|
+
if (!d.doc.id || docsById[d.doc.id]) continue;
|
|
27
|
+
docsById[d.doc.id] = d.doc;
|
|
28
|
+
const aggId = d.doc[aggregateRelationField].id;
|
|
29
|
+
if (!aggId) throw new Error(`Missing ${eventEntityName}.${aggregateRelationField}.id`);
|
|
30
|
+
if (d.oldDoc && aggId !== d.oldDoc[aggregateRelationField].id) {
|
|
31
|
+
throw new Error(`Field ${eventEntityName}.${aggregateRelationField}.id cannot be changed`);
|
|
32
|
+
}
|
|
33
|
+
if (!docsByAggId[aggId]) docsByAggId[aggId] = [];
|
|
34
|
+
docsByAggId[aggId].push(d.doc);
|
|
35
|
+
}
|
|
36
|
+
return { docsByAggId, docsById };
|
|
37
|
+
}
|
|
38
|
+
function applyEventToAggregate(ev, aggDoc, aggId, p) {
|
|
39
|
+
const mergedDoc = merge(aggDoc || { id: aggId }, ev.change);
|
|
40
|
+
const newAggDoc = p.context.validators[p.entityName](mergedDoc);
|
|
41
|
+
if (!p.options.freezeEvent?.(ev, p.context, p.ctx)) {
|
|
42
|
+
ev.beforeChange = aggDoc;
|
|
43
|
+
const originalChange = ev.change;
|
|
44
|
+
ev.change = diff(newAggDoc, aggDoc);
|
|
45
|
+
handleKeepHistory(p.schema[p.entityName].keepHistoryFields, originalChange, ev);
|
|
46
|
+
}
|
|
47
|
+
if (p.ctx.skipPrecomputed) {
|
|
48
|
+
for (const field of p.schema[p.entityName].precomputedFields || []) {
|
|
49
|
+
if (p.ctx.skipPrecomputed.includes(field) && mergedDoc[field] !== void 0) {
|
|
50
|
+
newAggDoc[field] = mergedDoc[field];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return newAggDoc;
|
|
55
|
+
}
|
|
56
|
+
function buildAggregateFromEvents(events, aggId, docsById, existingAggregatesById, p) {
|
|
57
|
+
if (events[0].type !== "creation")
|
|
58
|
+
throw new Error(`First event must have type = "creation". (type: ${events[0].type}, id: ${events[0].id})`);
|
|
59
|
+
if (events.slice(1).some((ev) => ev.type === "creation")) {
|
|
60
|
+
throw new Error(`Only first event may have type = "creation"`);
|
|
61
|
+
}
|
|
62
|
+
let aggDoc;
|
|
63
|
+
for (const ev of events) {
|
|
64
|
+
const newAggDoc = applyEventToAggregate(ev, aggDoc, aggId, p);
|
|
65
|
+
if (p.options.applyEventIf && !p.options.applyEventIf(ev, p.context, p.ctx)) continue;
|
|
66
|
+
aggDoc = newAggDoc;
|
|
67
|
+
if (!p.context.options.keepNulls) cleanEntity(aggDoc);
|
|
68
|
+
}
|
|
69
|
+
if (!aggDoc) return null;
|
|
70
|
+
return {
|
|
71
|
+
aggDoc,
|
|
72
|
+
events,
|
|
73
|
+
updatedEvents: events.filter((ev) => docsById[ev.id]),
|
|
74
|
+
oldAggDoc: existingAggregatesById[aggDoc.id]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
22
77
|
function getEffectFor(entityName, aggregateRelationField, eventEntityName, schema, options) {
|
|
23
78
|
return {
|
|
24
79
|
featureName: "eventSourcing",
|
|
25
80
|
async beforePut(context, docs, ctx) {
|
|
26
|
-
const docsByAggId =
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const existingEvents = await context.drivers[eventEntityName].getAll(
|
|
40
|
-
{
|
|
41
|
-
where: {
|
|
42
|
-
[aggregateRelationField]: { id_in: _.keys(docsByAggId) },
|
|
43
|
-
_not: { id_in: docs.map((d) => d.doc.id) }
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
ctx
|
|
47
|
-
);
|
|
81
|
+
const { docsByAggId, docsById } = groupEventsByAggId(docs, aggregateRelationField, eventEntityName);
|
|
82
|
+
const [existingEvents, existingAggregates] = await Promise.all([
|
|
83
|
+
context.drivers[eventEntityName].getAll(
|
|
84
|
+
{
|
|
85
|
+
where: {
|
|
86
|
+
[aggregateRelationField]: { id_in: _.keys(docsByAggId) },
|
|
87
|
+
_not: { id_in: docs.map((d) => d.doc.id) }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
ctx
|
|
91
|
+
),
|
|
92
|
+
context.drivers[entityName].getAll({ where: { id_in: _.keys(docsByAggId) } }, ctx)
|
|
93
|
+
]);
|
|
48
94
|
const existingEventsByAggId = _.groupBy(existingEvents, (ev) => ev[aggregateRelationField].id);
|
|
49
|
-
const existingAggregates = await context.drivers[entityName].getAll(
|
|
50
|
-
{ where: { id_in: _.keys(docsByAggId) } },
|
|
51
|
-
ctx
|
|
52
|
-
);
|
|
53
95
|
const existingAggregatesById = _.keyBy(existingAggregates, "id");
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
let aggDoc;
|
|
63
|
-
for (const ev of events) {
|
|
64
|
-
const newAggDoc = context.validators[entityName](merge(aggDoc || { id: aggId }, ev.change));
|
|
65
|
-
ev.beforeChange = aggDoc;
|
|
66
|
-
const originalChange = ev.change;
|
|
67
|
-
ev.change = diff(newAggDoc, aggDoc);
|
|
68
|
-
handleKeepHistory(schema[entityName].keepHistoryFields, originalChange, ev);
|
|
69
|
-
if (options.applyEventIf && !options.applyEventIf(ev, context, ctx)) {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
aggDoc = newAggDoc;
|
|
73
|
-
if (!context.options.keepNulls) cleanEntity(aggDoc);
|
|
74
|
-
}
|
|
75
|
-
if (aggDoc) {
|
|
76
|
-
result.push({
|
|
77
|
-
aggDoc,
|
|
78
|
-
events,
|
|
79
|
-
updatedEvents: events.filter((ev) => docsById[ev.id]),
|
|
80
|
-
oldAggDoc: existingAggregatesById[aggDoc.id]
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
96
|
+
const processor = { context, entityName, schema, options, ctx };
|
|
97
|
+
const result = _.compact(
|
|
98
|
+
_.keys(docsByAggId).map((aggId) => {
|
|
99
|
+
const events = _.orderBy([...docsByAggId[aggId], ...existingEventsByAggId[aggId] || []], ["date"], "asc");
|
|
100
|
+
return buildAggregateFromEvents(events, aggId, docsById, existingAggregatesById, processor);
|
|
101
|
+
})
|
|
102
|
+
);
|
|
84
103
|
await handlePrecomputed(
|
|
85
104
|
// @ts-expect-error TODO wrong types
|
|
86
105
|
{ ...context, typeName: entityName },
|
|
@@ -89,7 +108,7 @@ function getEffectFor(entityName, aggregateRelationField, eventEntityName, schem
|
|
|
89
108
|
);
|
|
90
109
|
return result;
|
|
91
110
|
},
|
|
92
|
-
async afterPut(context,
|
|
111
|
+
async afterPut(context, _docs, beforePutResult, ctx) {
|
|
93
112
|
const aggs = beforePutResult.map((d) => d.aggDoc);
|
|
94
113
|
const hookItems = beforePutResult.map((v) => ({
|
|
95
114
|
doc: v.aggDoc,
|