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 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] == null)
575
+ if (obj[key] === void 0)
706
576
  delete obj[key];
707
577
  if (___default.isPlainObject(obj[key]))
708
- cleanUndefinedAndNull(obj[key]);
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 (mustCleanNulls)
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 (mustCleanNulls)
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 (mustCleanNulls)
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] == null)
567
+ if (obj[key] === void 0)
698
568
  delete obj[key];
699
569
  if (_.isPlainObject(obj[key]))
700
- cleanUndefinedAndNull(obj[key]);
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 (mustCleanNulls)
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 (mustCleanNulls)
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 (mustCleanNulls)
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,2 @@
1
+ declare const _default: () => RadsFeature;
2
+ export default _default;
@@ -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.23",
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 build && pnpm generate-test-schema && pnpm link-rads-db && vitest --ui --test-timeout 300000",
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"