rads-db 3.0.58 → 3.0.62

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
@@ -5,13 +5,11 @@ const _ = require('lodash');
5
5
  const uuid = require('uuid');
6
6
  const createMerge = require('@fastify/deepmerge');
7
7
  const _radsDb = require('_rads-db');
8
- const pluralize = require('pluralize');
9
8
 
10
9
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
11
10
 
12
11
  const ___default = /*#__PURE__*/_interopDefaultCompat(_);
13
12
  const createMerge__default = /*#__PURE__*/_interopDefaultCompat(createMerge);
14
- const pluralize__default = /*#__PURE__*/_interopDefaultCompat(pluralize);
15
13
 
16
14
  function generateValidators(schema) {
17
15
  const zodSchemas = {};
@@ -58,7 +56,7 @@ function getZodSchema(zodSchemas, schema, key) {
58
56
  const objectSchema = {};
59
57
  for (const fieldName in type.fields) {
60
58
  const f = type.fields[fieldName];
61
- if (f.decorators?.computed || f.decorators?.precomputed) {
59
+ if (f.decorators?.computed || f.decorators?.precomputed || f.isInverseRelation) {
62
60
  continue;
63
61
  }
64
62
  objectSchema[fieldName] = getFieldZodSchema(zodSchemas, schema, f);
@@ -82,7 +80,7 @@ function getFieldZodSchemaBase(zodSchemas, schema, field) {
82
80
  const zodSchema = getZodSchema(zodSchemas, schema, field.type);
83
81
  if (!zodSchema)
84
82
  throw new Error(`Cannot find zod schema for type: ${field.type}`);
85
- return zodSchema.deepPartial().omit({ id: true });
83
+ return deepPartial(zodSchema.omit({ id: true }));
86
84
  }
87
85
  if (field.type === "string")
88
86
  return zod.z.string();
@@ -106,6 +104,25 @@ function getFieldZodSchemaBase(zodSchemas, schema, field) {
106
104
  }
107
105
  throw new Error(`Unknown type: ${field.type}`);
108
106
  }
107
+ function deepPartial(zodSchema) {
108
+ const shape = zodSchema.shape;
109
+ const newShape = {};
110
+ for (const key in shape) {
111
+ let field = shape[key];
112
+ while (field instanceof zod.ZodNullable || field instanceof zod.ZodOptional) {
113
+ field = field.unwrap();
114
+ }
115
+ if (field instanceof zod.ZodLazy) {
116
+ field = field.schema;
117
+ }
118
+ if (field instanceof zod.ZodObject) {
119
+ newShape[key] = deepPartial(field).optional().nullable();
120
+ } else {
121
+ newShape[key] = field.optional().nullable();
122
+ }
123
+ }
124
+ return zod.z.object(newShape);
125
+ }
109
126
  class ValidationError extends Error {
110
127
  constructor(error) {
111
128
  const firstError = error.errors[0];
@@ -491,6 +508,73 @@ async function handleEffectsAfterPut(context, docs, beforePutResults, ctx) {
491
508
  );
492
509
  }
493
510
 
511
+ function cleanUndefinedAndNull(obj, isChange = false) {
512
+ for (const key in obj) {
513
+ if (obj[key] == null && !isChange)
514
+ delete obj[key];
515
+ if (___default.isPlainObject(obj[key]))
516
+ cleanUndefinedAndNull(obj[key], isChange || key === "change");
517
+ }
518
+ }
519
+
520
+ const mergeFn = createMerge__default({
521
+ mergeArray(options) {
522
+ const clone = options.clone;
523
+ return function(target, source) {
524
+ return clone(source);
525
+ };
526
+ }
527
+ });
528
+ function merge(target, source) {
529
+ cleanUndefined(source);
530
+ const result = mergeFn(target, source);
531
+ return result;
532
+ }
533
+ function cleanUndefined(obj) {
534
+ if (!obj || !___default.isPlainObject(obj))
535
+ return;
536
+ cleanUndefinedInner(obj);
537
+ }
538
+ function cleanUndefinedInner(obj) {
539
+ for (const key in obj) {
540
+ if (obj[key] === void 0)
541
+ delete obj[key];
542
+ if (___default.isPlainObject(obj[key]))
543
+ cleanUndefinedInner(obj[key]);
544
+ }
545
+ }
546
+
547
+ function diff(object, base, includeDenormFields = false) {
548
+ if (!base)
549
+ return object;
550
+ return ___default.transform(object, (result, value, key) => {
551
+ const oldValue = base[key];
552
+ if (!___default.isEqual(value, oldValue)) {
553
+ if (mustUseDeepDiff(value, oldValue)) {
554
+ const isRelationField = (value.id || oldValue.id) && key !== "change";
555
+ if (isRelationField && !includeDenormFields) {
556
+ if (value.id !== oldValue.id)
557
+ result[key] = value;
558
+ } else {
559
+ const subDiff = diff(value, oldValue, includeDenormFields);
560
+ if (!___default.isEmpty(subDiff))
561
+ result[key] = subDiff;
562
+ }
563
+ } else {
564
+ result[key] = value;
565
+ }
566
+ }
567
+ });
568
+ }
569
+ function mustUseDeepDiff(value, oldValue) {
570
+ return ___default.isObject(value) && ___default.isObject(oldValue) && !___default.isArray(value) && !___default.isArray(oldValue);
571
+ }
572
+
573
+ const radsDbRelations = {
574
+ verifyRelationsSetup,
575
+ fillDenormFieldsBeforePut,
576
+ handleInclude
577
+ };
494
578
  function verifyRelationsSetup(schema, effects) {
495
579
  for (const entityName in schema) {
496
580
  const entity = schema[entityName];
@@ -570,67 +654,96 @@ async function fillDenormFieldsBeforePut(computedContext, docUpdates, ctx) {
570
654
  });
571
655
  await Promise.all(promises);
572
656
  }
573
-
574
- function cleanUndefinedAndNull(obj, isChange = false) {
575
- for (const key in obj) {
576
- if (obj[key] == null && !isChange)
577
- delete obj[key];
578
- if (___default.isPlainObject(obj[key]))
579
- cleanUndefinedAndNull(obj[key], key === "change");
580
- }
581
- }
582
-
583
- const mergeFn = createMerge__default({
584
- mergeArray(options) {
585
- const clone = options.clone;
586
- return function(target, source) {
587
- return clone(source);
588
- };
589
- }
590
- });
591
- function merge(target, source) {
592
- cleanUndefined(source);
593
- const result = mergeFn(target, source);
594
- return result;
595
- }
596
- function cleanUndefined(obj) {
597
- if (!obj || !___default.isPlainObject(obj))
657
+ async function handleInclude(computedContext, include, result, ctx) {
658
+ if (!result || !result.length || !include)
598
659
  return;
599
- cleanUndefinedInner(obj);
660
+ const { schema, typeName } = computedContext;
661
+ const fields = schema[typeName].fields || {};
662
+ const fieldsToInclude = ___default.keys(include).filter((key) => include[key]);
663
+ const relationsToInclude = fieldsToInclude.filter((key) => fields[key].isRelation);
664
+ const args = {
665
+ entityTypeName: typeName,
666
+ computedContext,
667
+ ctx,
668
+ result,
669
+ include
670
+ };
671
+ const inverseRelationsToInclude = fieldsToInclude.filter((key) => fields[key].isInverseRelation);
672
+ const downloadRelationsPromises = relationsToInclude.map(
673
+ (fieldName) => downloadRelatedDocuments({ ...args, fieldName })
674
+ );
675
+ const downloadInverseRelationsPromises = inverseRelationsToInclude.map(
676
+ (fieldName) => downloadInverseRelationDocuments({ ...args, fieldName })
677
+ );
678
+ await Promise.all([...downloadRelationsPromises, ...downloadInverseRelationsPromises]);
600
679
  }
601
- function cleanUndefinedInner(obj) {
602
- for (const key in obj) {
603
- if (obj[key] === void 0)
604
- delete obj[key];
605
- if (___default.isPlainObject(obj[key]))
606
- cleanUndefinedInner(obj[key]);
680
+ async function downloadInverseRelationDocuments(args) {
681
+ const { computedContext, ctx, entityTypeName, fieldName, result, include } = args;
682
+ const { schema, db } = computedContext;
683
+ const field = schema[entityTypeName].fields?.[fieldName];
684
+ if (!field?.inverseRelationField)
685
+ throw new Error(`Cannot find field ${fieldName}`);
686
+ const typeName = field.type;
687
+ const type = schema[typeName];
688
+ if (!type)
689
+ throw new Error(`Cannot find entity ${typeName}`);
690
+ for (const item of result) {
691
+ if (!item?.id)
692
+ continue;
693
+ const entityHandle = schema[typeName].handle;
694
+ const documentsRelatedToThisDoc = await db[entityHandle].getMany(
695
+ { where: { [field.inverseRelationField]: { id: item.id } }, include: include?.[fieldName], maxItemCount: 100 },
696
+ ctx
697
+ );
698
+ item[fieldName] = documentsRelatedToThisDoc.nodes;
607
699
  }
608
700
  }
609
-
610
- function diff(object, base, includeDenormFields = false) {
611
- if (!base)
612
- return object;
613
- return ___default.transform(object, (result, value, key) => {
614
- const oldValue = base[key];
615
- if (!___default.isEqual(value, oldValue)) {
616
- if (mustUseDeepDiff(value, oldValue)) {
617
- const isRelationField = (value.id || oldValue.id) && key !== "change";
618
- if (isRelationField && !includeDenormFields) {
619
- if (value.id !== oldValue.id)
620
- result[key] = value;
621
- } else {
622
- const subDiff = diff(value, oldValue, includeDenormFields);
623
- if (!___default.isEmpty(subDiff))
624
- result[key] = subDiff;
625
- }
626
- } else {
627
- result[key] = value;
701
+ async function downloadRelatedDocuments(args) {
702
+ const { computedContext, ctx, entityTypeName, fieldName, result, include } = args;
703
+ const { schema, db } = computedContext;
704
+ const typeName = schema[entityTypeName].fields?.[fieldName].type || "";
705
+ const type = schema[typeName];
706
+ if (!type)
707
+ throw new Error(`Cannot find entity ${typeName}`);
708
+ const idsToGet = /* @__PURE__ */ new Set();
709
+ for (const item of result) {
710
+ if (!item)
711
+ continue;
712
+ const fieldValue = item[fieldName];
713
+ if (!fieldValue)
714
+ continue;
715
+ const fieldValues = ___default.isArray(fieldValue) ? fieldValue : [fieldValue];
716
+ for (const fv of fieldValues) {
717
+ const id = fv?.id;
718
+ if (!id)
719
+ continue;
720
+ idsToGet.add(id);
721
+ }
722
+ }
723
+ const entityHandle = schema[typeName].handle;
724
+ const relatedEntities = await db[entityHandle].getAll(
725
+ { where: { id_in: [...idsToGet] }, include: include?.[fieldName] },
726
+ ctx
727
+ );
728
+ const relatedEntitiesById = ___default.keyBy(relatedEntities, "id");
729
+ for (const item of result) {
730
+ if (!item)
731
+ continue;
732
+ const fieldValue = item[fieldName];
733
+ if (!fieldValue)
734
+ continue;
735
+ const fieldValues = ___default.isArray(fieldValue) ? fieldValue : [fieldValue];
736
+ for (const fv of fieldValues) {
737
+ const id = fv?.id;
738
+ if (!id)
739
+ continue;
740
+ const relatedEntity = relatedEntitiesById[id];
741
+ if (!relatedEntity) {
742
+ console.warn(`Cannot find ${typeName} with id "${id}" (for ${typeName}.${fieldName} with id "${item.id}")`);
628
743
  }
744
+ Object.assign(fv, relatedEntity);
629
745
  }
630
- });
631
- }
632
- function mustUseDeepDiff(value, oldValue) {
633
- return ___default.isObject(value) && ___default.isObject(oldValue) && !___default.isArray(value) && !___default.isArray(oldValue);
746
+ }
634
747
  }
635
748
 
636
749
  function getRadsDbMethods(key, computedContext, driverInstance) {
@@ -643,7 +756,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
643
756
  if (!result2)
644
757
  result2 = await driverInstance.getMany(args, ctx);
645
758
  if (args.include)
646
- await handleInclude(computedContext, args.include, result2.nodes, ctx);
759
+ await radsDbRelations.handleInclude(computedContext, args.include, result2.nodes, ctx);
647
760
  await handleComputed(computedContext, result2.nodes, ctx);
648
761
  await afterGet(result2.nodes, args, ctx, computedContext);
649
762
  return result2;
@@ -680,7 +793,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
680
793
  }
681
794
  d.doc = validatedDoc;
682
795
  }
683
- await fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
796
+ await radsDbRelations.fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
684
797
  const docsToSave = docArgsToSave.map((x) => x.doc);
685
798
  await driverInstance.putMany(docsToSave, ctx);
686
799
  await handleEffectsAfterPut(computedContext, docArgsToSave, beforePutResults, ctx);
@@ -723,7 +836,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
723
836
  }
724
837
  d.doc = validatedDoc;
725
838
  }
726
- await fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
839
+ await radsDbRelations.fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
727
840
  for (const { oldDoc, doc } of docArgsToSave) {
728
841
  const d = diff(doc, oldDoc);
729
842
  if (___default.isEmpty(d))
@@ -782,7 +895,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
782
895
  result2 = await driverInstance.get(args, ctx);
783
896
  const resultArray = result2 ? [result2] : [];
784
897
  if (result2 && args.include)
785
- await handleInclude(computedContext, args.include, resultArray, ctx);
898
+ await radsDbRelations.handleInclude(computedContext, args.include, resultArray, ctx);
786
899
  if (result2)
787
900
  await handleComputed(computedContext, resultArray, ctx);
788
901
  await afterGet(resultArray, args, ctx, computedContext);
@@ -795,7 +908,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
795
908
  if (!result2)
796
909
  result2 = await driverInstance.getAll(args, ctx);
797
910
  if (args.include)
798
- await handleInclude(computedContext, args.include, result2, ctx);
911
+ await radsDbRelations.handleInclude(computedContext, args.include, result2, ctx);
799
912
  await handleComputed(computedContext, result2, ctx);
800
913
  await afterGet(result2, args, ctx, computedContext);
801
914
  return result2;
@@ -838,7 +951,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
838
951
  }
839
952
  }
840
953
  doc = validatedDoc;
841
- await fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
954
+ await radsDbRelations.fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
842
955
  await driverInstance.put(doc, ctx);
843
956
  await handleEffectsAfterPut(computedContext, docArgsToSave, beforePutResults, ctx);
844
957
  await afterPut(docArgsToSave, ctx, computedContext);
@@ -873,61 +986,6 @@ async function afterPut(items, ctx, computedContext) {
873
986
  await f.afterPut?.(items, ctx, computedContext);
874
987
  }
875
988
  }
876
- async function handleInclude(computedContext, include, result, ctx) {
877
- if (!result || !result.length || !include)
878
- return;
879
- const { schema, typeName, drivers } = computedContext;
880
- const fields = schema[typeName].fields || {};
881
- const relationsToInclude = ___default.keys(include).filter((key) => include[key] && fields[key].isRelation);
882
- const downloadRelationsPromises = relationsToInclude.map(async (fieldName) => {
883
- const typeName2 = fields[fieldName].type;
884
- const type = schema[typeName2];
885
- if (!type)
886
- throw new Error(`Cannot find entity ${typeName2}`);
887
- const idsToGet = /* @__PURE__ */ new Set();
888
- for (const item of result) {
889
- if (!item)
890
- continue;
891
- const fieldValue = item[fieldName];
892
- if (!fieldValue)
893
- continue;
894
- const fieldValues = ___default.isArray(fieldValue) ? fieldValue : [fieldValue];
895
- for (const fv of fieldValues) {
896
- const id = fv?.id;
897
- if (!id)
898
- continue;
899
- idsToGet.add(id);
900
- }
901
- }
902
- const driverInstance = getExistingDriverInstance(typeName2, drivers);
903
- const relatedEntities = await driverInstance.getAll({ where: { id_in: [...idsToGet] } }, ctx);
904
- const relatedEntitiesById = ___default.keyBy(relatedEntities, "id");
905
- for (const item of result) {
906
- if (!item)
907
- continue;
908
- const fieldValue = item[fieldName];
909
- if (!fieldValue)
910
- continue;
911
- const fieldValues = ___default.isArray(fieldValue) ? fieldValue : [fieldValue];
912
- for (const fv of fieldValues) {
913
- const id = fv?.id;
914
- if (!id)
915
- continue;
916
- const relatedEntity = relatedEntitiesById[id];
917
- if (!relatedEntity)
918
- console.warn(`Cannot find ${typeName2} with id "${id}" (for ${typeName2}.${fieldName} with id "${item.id}")`);
919
- Object.assign(fv, relatedEntity);
920
- }
921
- }
922
- });
923
- await Promise.all(downloadRelationsPromises);
924
- }
925
- function getExistingDriverInstance(entityName, drivers) {
926
- if (!drivers[entityName]) {
927
- throw new Error(`Driver for entity ${entityName} was not found!`);
928
- }
929
- return drivers[entityName];
930
- }
931
989
 
932
990
  function generateMethods(schema, validators, options) {
933
991
  const drivers = {};
@@ -957,7 +1015,7 @@ function generateMethods(schema, validators, options) {
957
1015
  if (!opts.noComputed) {
958
1016
  verifyComputedPresense(schema, opts.computed, effects);
959
1017
  }
960
- verifyRelationsSetup(schema, effects);
1018
+ radsDbRelations.verifyRelationsSetup(schema, effects);
961
1019
  const computedContextGlobal = {
962
1020
  schema,
963
1021
  validators,
@@ -1125,12 +1183,14 @@ const restApi = (options) => (schema, entity) => {
1125
1183
  }
1126
1184
  return responseJson;
1127
1185
  }
1128
- const pluralEntityName = ___default.lowerFirst(pluralize__default(entity));
1186
+ const { handlePlural } = schema[entity] || {};
1187
+ if (!handlePlural)
1188
+ throw new Error(`Entity ${entity} was not found in schema`);
1129
1189
  const instance = {
1130
1190
  driverName: "restApi",
1131
1191
  async getMany(args) {
1132
1192
  args = args || {};
1133
- const url = `${opts.baseUrl}/${pluralEntityName}`;
1193
+ const url = `${opts.baseUrl}/${handlePlural}`;
1134
1194
  const fetchOptions = {
1135
1195
  method: "POST",
1136
1196
  body: JSON.stringify(args),
@@ -1140,7 +1200,7 @@ const restApi = (options) => (schema, entity) => {
1140
1200
  return await fetchInner(url, fetchOptions);
1141
1201
  },
1142
1202
  async putMany(item) {
1143
- const url = `${opts.baseUrl}/${pluralEntityName}`;
1203
+ const url = `${opts.baseUrl}/${handlePlural}`;
1144
1204
  const fetchOptions = {
1145
1205
  method: "PUT",
1146
1206
  body: JSON.stringify({ data: item }),
package/dist/index.d.ts CHANGED
@@ -89,9 +89,17 @@ type DeepPartialWithNullsItem<T> = T extends {
89
89
  type DeepPartial<T> = {
90
90
  [K in keyof T]?: NonNullable<T[K]> extends any[] ? DeepPartial<NonNullable<T[K]>[number]>[] : NonNullable<T[K]> extends Record<string, any> ? DeepPartial<T[K]> : T[K];
91
91
  };
92
+ /** Indicates that this field is a relation to another entity in the database.
93
+ * Only id will be stored in the database.
94
+ * If you want to store additional fields, please, pass them as the second type argument */
92
95
  type Relation<T extends {
93
96
  id: any;
94
97
  }, K extends Exclude<keyof T, 'id'> = never> = Pick<T, K | 'id'>;
98
+ /** Indicates that this is computed field - all documents that point to this document via Relation<>.
99
+ * Note: this field is not stored in the database at all. Returns up to 100 items and doesn't support pagination
100
+ * If you need more control, please, use separate request instead.
101
+ */
102
+ type InverseRelation<EN extends keyof EntityMeta, _Field extends keyof EntityMeta[EN]['relations']> = EntityMeta[EN]['type'];
95
103
  type PutArgs<T> = {
96
104
  id: string;
97
105
  } & DeepPartialWithNulls<T>;
@@ -298,7 +306,9 @@ interface FieldDefinition {
298
306
  isRequired?: boolean;
299
307
  isArray?: boolean;
300
308
  isRelation?: boolean;
309
+ isInverseRelation?: boolean;
301
310
  isChange?: boolean;
311
+ inverseRelationField?: string;
302
312
  relationDenormFields?: string[];
303
313
  comment?: string;
304
314
  decorators?: Record<string, Record<string, any>>;
@@ -448,4 +458,4 @@ declare function createRadsDb(args?: CreateRadsDbArgs): RadsDb;
448
458
  */
449
459
  declare function createRadsDbClient(args?: CreateRadsDbClientArgs): RadsDb;
450
460
 
451
- export { Change, ComputedContext, ComputedContextGlobal, ComputedDecoratorArgs, CreateRadsArgsDrivers, CreateRadsDbArgs, CreateRadsDbArgsNormalized, CreateRadsDbClientArgs, DeepKeys, DeepPartial, DeepPartialWithNulls, DeepPartialWithNullsItem, Driver, DriverConstructor, EntityDecoratorArgs, EntityMethods, EnumDefinition, FieldDecoratorArgs, FieldDefinition, FileSystemNode, FileUploadArgs, FileUploadDriver, FileUploadResult, GenerateClientNormalizedOptions, GenerateClientOptions, Get, GetAggArgs, GetAggArgsAgg, GetAggArgsAny, GetAggResponse, GetArgs, GetArgsAny, GetArgsInclude, GetManyArgs, GetManyArgsAny, GetManyResponse, GetResponse, GetResponseInclude, GetResponseIncludeSelect, GetResponseNoInclude, GetRestRoutesArgs, GetRestRoutesOptions, GetRestRoutesResponse, MinimalDriver, Put, PutArgs, PutEffect, RadsFeature, RadsHookDoc, RadsRequestContext, RadsUiSlotDefinition, RadsUiSlotName, RadsVitePluginOptions, Relation, RequiredFields, RestDriverOptions, RestFileUploadDriverOptions, Schema, SchemaValidators, TypeDefinition, UiDecoratorArgs, UiFieldDecoratorArgs, ValidateEntityDecoratorArgs, ValidateFieldDecoratorArgs, ValidateStringDecoratorArgs, VerifyManyArgs, VerifyManyArgsAny, VerifyManyResponse, Where, WhereJsonContains, cleanUndefinedAndNull, computed, createRadsDb, createRadsDbClient, diff, entity, field, getDriverInstance, handlePrecomputed, keepHistory, merge, precomputed, ui, validate };
461
+ export { Change, ComputedContext, ComputedContextGlobal, ComputedDecoratorArgs, CreateRadsArgsDrivers, CreateRadsDbArgs, CreateRadsDbArgsNormalized, CreateRadsDbClientArgs, DeepKeys, DeepPartial, DeepPartialWithNulls, DeepPartialWithNullsItem, Driver, DriverConstructor, EntityDecoratorArgs, EntityMethods, EnumDefinition, FieldDecoratorArgs, FieldDefinition, FileSystemNode, FileUploadArgs, FileUploadDriver, FileUploadResult, GenerateClientNormalizedOptions, GenerateClientOptions, Get, GetAggArgs, GetAggArgsAgg, GetAggArgsAny, GetAggResponse, GetArgs, GetArgsAny, GetArgsInclude, GetManyArgs, GetManyArgsAny, GetManyResponse, GetResponse, GetResponseInclude, GetResponseIncludeSelect, GetResponseNoInclude, GetRestRoutesArgs, GetRestRoutesOptions, GetRestRoutesResponse, InverseRelation, MinimalDriver, Put, PutArgs, PutEffect, RadsFeature, RadsHookDoc, RadsRequestContext, RadsUiSlotDefinition, RadsUiSlotName, RadsVitePluginOptions, Relation, RequiredFields, RestDriverOptions, RestFileUploadDriverOptions, Schema, SchemaValidators, TypeDefinition, UiDecoratorArgs, UiFieldDecoratorArgs, ValidateEntityDecoratorArgs, ValidateFieldDecoratorArgs, ValidateStringDecoratorArgs, VerifyManyArgs, VerifyManyArgsAny, VerifyManyResponse, Where, WhereJsonContains, cleanUndefinedAndNull, computed, createRadsDb, createRadsDbClient, diff, entity, field, getDriverInstance, handlePrecomputed, keepHistory, merge, precomputed, ui, validate };
package/dist/index.mjs CHANGED
@@ -1,9 +1,8 @@
1
- import { z, ZodError } from 'zod';
1
+ import { z, ZodNullable, ZodOptional, ZodLazy, ZodObject, ZodError } from 'zod';
2
2
  import _ from 'lodash';
3
3
  import { v4 } from 'uuid';
4
4
  import createMerge from '@fastify/deepmerge';
5
5
  import { schema } from '_rads-db';
6
- import pluralize from 'pluralize';
7
6
 
8
7
  function generateValidators(schema) {
9
8
  const zodSchemas = {};
@@ -50,7 +49,7 @@ function getZodSchema(zodSchemas, schema, key) {
50
49
  const objectSchema = {};
51
50
  for (const fieldName in type.fields) {
52
51
  const f = type.fields[fieldName];
53
- if (f.decorators?.computed || f.decorators?.precomputed) {
52
+ if (f.decorators?.computed || f.decorators?.precomputed || f.isInverseRelation) {
54
53
  continue;
55
54
  }
56
55
  objectSchema[fieldName] = getFieldZodSchema(zodSchemas, schema, f);
@@ -74,7 +73,7 @@ function getFieldZodSchemaBase(zodSchemas, schema, field) {
74
73
  const zodSchema = getZodSchema(zodSchemas, schema, field.type);
75
74
  if (!zodSchema)
76
75
  throw new Error(`Cannot find zod schema for type: ${field.type}`);
77
- return zodSchema.deepPartial().omit({ id: true });
76
+ return deepPartial(zodSchema.omit({ id: true }));
78
77
  }
79
78
  if (field.type === "string")
80
79
  return z.string();
@@ -98,6 +97,25 @@ function getFieldZodSchemaBase(zodSchemas, schema, field) {
98
97
  }
99
98
  throw new Error(`Unknown type: ${field.type}`);
100
99
  }
100
+ function deepPartial(zodSchema) {
101
+ const shape = zodSchema.shape;
102
+ const newShape = {};
103
+ for (const key in shape) {
104
+ let field = shape[key];
105
+ while (field instanceof ZodNullable || field instanceof ZodOptional) {
106
+ field = field.unwrap();
107
+ }
108
+ if (field instanceof ZodLazy) {
109
+ field = field.schema;
110
+ }
111
+ if (field instanceof ZodObject) {
112
+ newShape[key] = deepPartial(field).optional().nullable();
113
+ } else {
114
+ newShape[key] = field.optional().nullable();
115
+ }
116
+ }
117
+ return z.object(newShape);
118
+ }
101
119
  class ValidationError extends Error {
102
120
  constructor(error) {
103
121
  const firstError = error.errors[0];
@@ -483,6 +501,73 @@ async function handleEffectsAfterPut(context, docs, beforePutResults, ctx) {
483
501
  );
484
502
  }
485
503
 
504
+ function cleanUndefinedAndNull(obj, isChange = false) {
505
+ for (const key in obj) {
506
+ if (obj[key] == null && !isChange)
507
+ delete obj[key];
508
+ if (_.isPlainObject(obj[key]))
509
+ cleanUndefinedAndNull(obj[key], isChange || key === "change");
510
+ }
511
+ }
512
+
513
+ const mergeFn = createMerge({
514
+ mergeArray(options) {
515
+ const clone = options.clone;
516
+ return function(target, source) {
517
+ return clone(source);
518
+ };
519
+ }
520
+ });
521
+ function merge(target, source) {
522
+ cleanUndefined(source);
523
+ const result = mergeFn(target, source);
524
+ return result;
525
+ }
526
+ function cleanUndefined(obj) {
527
+ if (!obj || !_.isPlainObject(obj))
528
+ return;
529
+ cleanUndefinedInner(obj);
530
+ }
531
+ function cleanUndefinedInner(obj) {
532
+ for (const key in obj) {
533
+ if (obj[key] === void 0)
534
+ delete obj[key];
535
+ if (_.isPlainObject(obj[key]))
536
+ cleanUndefinedInner(obj[key]);
537
+ }
538
+ }
539
+
540
+ function diff(object, base, includeDenormFields = false) {
541
+ if (!base)
542
+ return object;
543
+ return _.transform(object, (result, value, key) => {
544
+ const oldValue = base[key];
545
+ if (!_.isEqual(value, oldValue)) {
546
+ if (mustUseDeepDiff(value, oldValue)) {
547
+ const isRelationField = (value.id || oldValue.id) && key !== "change";
548
+ if (isRelationField && !includeDenormFields) {
549
+ if (value.id !== oldValue.id)
550
+ result[key] = value;
551
+ } else {
552
+ const subDiff = diff(value, oldValue, includeDenormFields);
553
+ if (!_.isEmpty(subDiff))
554
+ result[key] = subDiff;
555
+ }
556
+ } else {
557
+ result[key] = value;
558
+ }
559
+ }
560
+ });
561
+ }
562
+ function mustUseDeepDiff(value, oldValue) {
563
+ return _.isObject(value) && _.isObject(oldValue) && !_.isArray(value) && !_.isArray(oldValue);
564
+ }
565
+
566
+ const radsDbRelations = {
567
+ verifyRelationsSetup,
568
+ fillDenormFieldsBeforePut,
569
+ handleInclude
570
+ };
486
571
  function verifyRelationsSetup(schema, effects) {
487
572
  for (const entityName in schema) {
488
573
  const entity = schema[entityName];
@@ -562,67 +647,96 @@ async function fillDenormFieldsBeforePut(computedContext, docUpdates, ctx) {
562
647
  });
563
648
  await Promise.all(promises);
564
649
  }
565
-
566
- function cleanUndefinedAndNull(obj, isChange = false) {
567
- for (const key in obj) {
568
- if (obj[key] == null && !isChange)
569
- delete obj[key];
570
- if (_.isPlainObject(obj[key]))
571
- cleanUndefinedAndNull(obj[key], key === "change");
572
- }
573
- }
574
-
575
- const mergeFn = createMerge({
576
- mergeArray(options) {
577
- const clone = options.clone;
578
- return function(target, source) {
579
- return clone(source);
580
- };
581
- }
582
- });
583
- function merge(target, source) {
584
- cleanUndefined(source);
585
- const result = mergeFn(target, source);
586
- return result;
587
- }
588
- function cleanUndefined(obj) {
589
- if (!obj || !_.isPlainObject(obj))
650
+ async function handleInclude(computedContext, include, result, ctx) {
651
+ if (!result || !result.length || !include)
590
652
  return;
591
- cleanUndefinedInner(obj);
653
+ const { schema, typeName } = computedContext;
654
+ const fields = schema[typeName].fields || {};
655
+ const fieldsToInclude = _.keys(include).filter((key) => include[key]);
656
+ const relationsToInclude = fieldsToInclude.filter((key) => fields[key].isRelation);
657
+ const args = {
658
+ entityTypeName: typeName,
659
+ computedContext,
660
+ ctx,
661
+ result,
662
+ include
663
+ };
664
+ const inverseRelationsToInclude = fieldsToInclude.filter((key) => fields[key].isInverseRelation);
665
+ const downloadRelationsPromises = relationsToInclude.map(
666
+ (fieldName) => downloadRelatedDocuments({ ...args, fieldName })
667
+ );
668
+ const downloadInverseRelationsPromises = inverseRelationsToInclude.map(
669
+ (fieldName) => downloadInverseRelationDocuments({ ...args, fieldName })
670
+ );
671
+ await Promise.all([...downloadRelationsPromises, ...downloadInverseRelationsPromises]);
592
672
  }
593
- function cleanUndefinedInner(obj) {
594
- for (const key in obj) {
595
- if (obj[key] === void 0)
596
- delete obj[key];
597
- if (_.isPlainObject(obj[key]))
598
- cleanUndefinedInner(obj[key]);
673
+ async function downloadInverseRelationDocuments(args) {
674
+ const { computedContext, ctx, entityTypeName, fieldName, result, include } = args;
675
+ const { schema, db } = computedContext;
676
+ const field = schema[entityTypeName].fields?.[fieldName];
677
+ if (!field?.inverseRelationField)
678
+ throw new Error(`Cannot find field ${fieldName}`);
679
+ const typeName = field.type;
680
+ const type = schema[typeName];
681
+ if (!type)
682
+ throw new Error(`Cannot find entity ${typeName}`);
683
+ for (const item of result) {
684
+ if (!item?.id)
685
+ continue;
686
+ const entityHandle = schema[typeName].handle;
687
+ const documentsRelatedToThisDoc = await db[entityHandle].getMany(
688
+ { where: { [field.inverseRelationField]: { id: item.id } }, include: include?.[fieldName], maxItemCount: 100 },
689
+ ctx
690
+ );
691
+ item[fieldName] = documentsRelatedToThisDoc.nodes;
599
692
  }
600
693
  }
601
-
602
- function diff(object, base, includeDenormFields = false) {
603
- if (!base)
604
- return object;
605
- return _.transform(object, (result, value, key) => {
606
- const oldValue = base[key];
607
- if (!_.isEqual(value, oldValue)) {
608
- if (mustUseDeepDiff(value, oldValue)) {
609
- const isRelationField = (value.id || oldValue.id) && key !== "change";
610
- if (isRelationField && !includeDenormFields) {
611
- if (value.id !== oldValue.id)
612
- result[key] = value;
613
- } else {
614
- const subDiff = diff(value, oldValue, includeDenormFields);
615
- if (!_.isEmpty(subDiff))
616
- result[key] = subDiff;
617
- }
618
- } else {
619
- result[key] = value;
694
+ async function downloadRelatedDocuments(args) {
695
+ const { computedContext, ctx, entityTypeName, fieldName, result, include } = args;
696
+ const { schema, db } = computedContext;
697
+ const typeName = schema[entityTypeName].fields?.[fieldName].type || "";
698
+ const type = schema[typeName];
699
+ if (!type)
700
+ throw new Error(`Cannot find entity ${typeName}`);
701
+ const idsToGet = /* @__PURE__ */ new Set();
702
+ for (const item of result) {
703
+ if (!item)
704
+ continue;
705
+ const fieldValue = item[fieldName];
706
+ if (!fieldValue)
707
+ continue;
708
+ const fieldValues = _.isArray(fieldValue) ? fieldValue : [fieldValue];
709
+ for (const fv of fieldValues) {
710
+ const id = fv?.id;
711
+ if (!id)
712
+ continue;
713
+ idsToGet.add(id);
714
+ }
715
+ }
716
+ const entityHandle = schema[typeName].handle;
717
+ const relatedEntities = await db[entityHandle].getAll(
718
+ { where: { id_in: [...idsToGet] }, include: include?.[fieldName] },
719
+ ctx
720
+ );
721
+ const relatedEntitiesById = _.keyBy(relatedEntities, "id");
722
+ for (const item of result) {
723
+ if (!item)
724
+ continue;
725
+ const fieldValue = item[fieldName];
726
+ if (!fieldValue)
727
+ continue;
728
+ const fieldValues = _.isArray(fieldValue) ? fieldValue : [fieldValue];
729
+ for (const fv of fieldValues) {
730
+ const id = fv?.id;
731
+ if (!id)
732
+ continue;
733
+ const relatedEntity = relatedEntitiesById[id];
734
+ if (!relatedEntity) {
735
+ console.warn(`Cannot find ${typeName} with id "${id}" (for ${typeName}.${fieldName} with id "${item.id}")`);
620
736
  }
737
+ Object.assign(fv, relatedEntity);
621
738
  }
622
- });
623
- }
624
- function mustUseDeepDiff(value, oldValue) {
625
- return _.isObject(value) && _.isObject(oldValue) && !_.isArray(value) && !_.isArray(oldValue);
739
+ }
626
740
  }
627
741
 
628
742
  function getRadsDbMethods(key, computedContext, driverInstance) {
@@ -635,7 +749,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
635
749
  if (!result2)
636
750
  result2 = await driverInstance.getMany(args, ctx);
637
751
  if (args.include)
638
- await handleInclude(computedContext, args.include, result2.nodes, ctx);
752
+ await radsDbRelations.handleInclude(computedContext, args.include, result2.nodes, ctx);
639
753
  await handleComputed(computedContext, result2.nodes, ctx);
640
754
  await afterGet(result2.nodes, args, ctx, computedContext);
641
755
  return result2;
@@ -672,7 +786,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
672
786
  }
673
787
  d.doc = validatedDoc;
674
788
  }
675
- await fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
789
+ await radsDbRelations.fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
676
790
  const docsToSave = docArgsToSave.map((x) => x.doc);
677
791
  await driverInstance.putMany(docsToSave, ctx);
678
792
  await handleEffectsAfterPut(computedContext, docArgsToSave, beforePutResults, ctx);
@@ -715,7 +829,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
715
829
  }
716
830
  d.doc = validatedDoc;
717
831
  }
718
- await fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
832
+ await radsDbRelations.fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
719
833
  for (const { oldDoc, doc } of docArgsToSave) {
720
834
  const d = diff(doc, oldDoc);
721
835
  if (_.isEmpty(d))
@@ -774,7 +888,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
774
888
  result2 = await driverInstance.get(args, ctx);
775
889
  const resultArray = result2 ? [result2] : [];
776
890
  if (result2 && args.include)
777
- await handleInclude(computedContext, args.include, resultArray, ctx);
891
+ await radsDbRelations.handleInclude(computedContext, args.include, resultArray, ctx);
778
892
  if (result2)
779
893
  await handleComputed(computedContext, resultArray, ctx);
780
894
  await afterGet(resultArray, args, ctx, computedContext);
@@ -787,7 +901,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
787
901
  if (!result2)
788
902
  result2 = await driverInstance.getAll(args, ctx);
789
903
  if (args.include)
790
- await handleInclude(computedContext, args.include, result2, ctx);
904
+ await radsDbRelations.handleInclude(computedContext, args.include, result2, ctx);
791
905
  await handleComputed(computedContext, result2, ctx);
792
906
  await afterGet(result2, args, ctx, computedContext);
793
907
  return result2;
@@ -830,7 +944,7 @@ function getRadsDbMethods(key, computedContext, driverInstance) {
830
944
  }
831
945
  }
832
946
  doc = validatedDoc;
833
- await fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
947
+ await radsDbRelations.fillDenormFieldsBeforePut(computedContext, docArgsToSave, ctx);
834
948
  await driverInstance.put(doc, ctx);
835
949
  await handleEffectsAfterPut(computedContext, docArgsToSave, beforePutResults, ctx);
836
950
  await afterPut(docArgsToSave, ctx, computedContext);
@@ -865,61 +979,6 @@ async function afterPut(items, ctx, computedContext) {
865
979
  await f.afterPut?.(items, ctx, computedContext);
866
980
  }
867
981
  }
868
- async function handleInclude(computedContext, include, result, ctx) {
869
- if (!result || !result.length || !include)
870
- return;
871
- const { schema, typeName, drivers } = computedContext;
872
- const fields = schema[typeName].fields || {};
873
- const relationsToInclude = _.keys(include).filter((key) => include[key] && fields[key].isRelation);
874
- const downloadRelationsPromises = relationsToInclude.map(async (fieldName) => {
875
- const typeName2 = fields[fieldName].type;
876
- const type = schema[typeName2];
877
- if (!type)
878
- throw new Error(`Cannot find entity ${typeName2}`);
879
- const idsToGet = /* @__PURE__ */ new Set();
880
- for (const item of result) {
881
- if (!item)
882
- continue;
883
- const fieldValue = item[fieldName];
884
- if (!fieldValue)
885
- continue;
886
- const fieldValues = _.isArray(fieldValue) ? fieldValue : [fieldValue];
887
- for (const fv of fieldValues) {
888
- const id = fv?.id;
889
- if (!id)
890
- continue;
891
- idsToGet.add(id);
892
- }
893
- }
894
- const driverInstance = getExistingDriverInstance(typeName2, drivers);
895
- const relatedEntities = await driverInstance.getAll({ where: { id_in: [...idsToGet] } }, ctx);
896
- const relatedEntitiesById = _.keyBy(relatedEntities, "id");
897
- for (const item of result) {
898
- if (!item)
899
- continue;
900
- const fieldValue = item[fieldName];
901
- if (!fieldValue)
902
- continue;
903
- const fieldValues = _.isArray(fieldValue) ? fieldValue : [fieldValue];
904
- for (const fv of fieldValues) {
905
- const id = fv?.id;
906
- if (!id)
907
- continue;
908
- const relatedEntity = relatedEntitiesById[id];
909
- if (!relatedEntity)
910
- console.warn(`Cannot find ${typeName2} with id "${id}" (for ${typeName2}.${fieldName} with id "${item.id}")`);
911
- Object.assign(fv, relatedEntity);
912
- }
913
- }
914
- });
915
- await Promise.all(downloadRelationsPromises);
916
- }
917
- function getExistingDriverInstance(entityName, drivers) {
918
- if (!drivers[entityName]) {
919
- throw new Error(`Driver for entity ${entityName} was not found!`);
920
- }
921
- return drivers[entityName];
922
- }
923
982
 
924
983
  function generateMethods(schema, validators, options) {
925
984
  const drivers = {};
@@ -949,7 +1008,7 @@ function generateMethods(schema, validators, options) {
949
1008
  if (!opts.noComputed) {
950
1009
  verifyComputedPresense(schema, opts.computed, effects);
951
1010
  }
952
- verifyRelationsSetup(schema, effects);
1011
+ radsDbRelations.verifyRelationsSetup(schema, effects);
953
1012
  const computedContextGlobal = {
954
1013
  schema,
955
1014
  validators,
@@ -1117,12 +1176,14 @@ const restApi = (options) => (schema, entity) => {
1117
1176
  }
1118
1177
  return responseJson;
1119
1178
  }
1120
- const pluralEntityName = _.lowerFirst(pluralize(entity));
1179
+ const { handlePlural } = schema[entity] || {};
1180
+ if (!handlePlural)
1181
+ throw new Error(`Entity ${entity} was not found in schema`);
1121
1182
  const instance = {
1122
1183
  driverName: "restApi",
1123
1184
  async getMany(args) {
1124
1185
  args = args || {};
1125
- const url = `${opts.baseUrl}/${pluralEntityName}`;
1186
+ const url = `${opts.baseUrl}/${handlePlural}`;
1126
1187
  const fetchOptions = {
1127
1188
  method: "POST",
1128
1189
  body: JSON.stringify(args),
@@ -1132,7 +1193,7 @@ const restApi = (options) => (schema, entity) => {
1132
1193
  return await fetchInner(url, fetchOptions);
1133
1194
  },
1134
1195
  async putMany(item) {
1135
- const url = `${opts.baseUrl}/${pluralEntityName}`;
1196
+ const url = `${opts.baseUrl}/${handlePlural}`;
1136
1197
  const fetchOptions = {
1137
1198
  method: "PUT",
1138
1199
  body: JSON.stringify({ data: item }),
@@ -4,9 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  module.exports = void 0;
7
- var _lodash = _interopRequireDefault(require("lodash"));
8
- var _pluralize = _interopRequireDefault(require("pluralize"));
9
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
7
  var _default = options => (schema, entity) => {
11
8
  const opts = {
12
9
  baseUrl: "/api",
@@ -30,12 +27,15 @@ var _default = options => (schema, entity) => {
30
27
  }
31
28
  return responseJson;
32
29
  }
33
- const pluralEntityName = _lodash.default.lowerFirst((0, _pluralize.default)(entity));
30
+ const {
31
+ handlePlural
32
+ } = schema[entity] || {};
33
+ if (!handlePlural) throw new Error(`Entity ${entity} was not found in schema`);
34
34
  const instance = {
35
35
  driverName: "restApi",
36
36
  async getMany(args) {
37
37
  args = args || {};
38
- const url = `${opts.baseUrl}/${pluralEntityName}`;
38
+ const url = `${opts.baseUrl}/${handlePlural}`;
39
39
  const fetchOptions = {
40
40
  method: "POST",
41
41
  body: JSON.stringify(args),
@@ -50,7 +50,7 @@ var _default = options => (schema, entity) => {
50
50
  return await fetchInner(url, fetchOptions);
51
51
  },
52
52
  async putMany(item) {
53
- const url = `${opts.baseUrl}/${pluralEntityName}`;
53
+ const url = `${opts.baseUrl}/${handlePlural}`;
54
54
  const fetchOptions = {
55
55
  method: "PUT",
56
56
  body: JSON.stringify({
@@ -1,5 +1,3 @@
1
- import _ from "lodash";
2
- import pluralize from "pluralize";
3
1
  export default (options) => (schema, entity) => {
4
2
  const opts = {
5
3
  baseUrl: "/api",
@@ -24,12 +22,14 @@ export default (options) => (schema, entity) => {
24
22
  }
25
23
  return responseJson;
26
24
  }
27
- const pluralEntityName = _.lowerFirst(pluralize(entity));
25
+ const { handlePlural } = schema[entity] || {};
26
+ if (!handlePlural)
27
+ throw new Error(`Entity ${entity} was not found in schema`);
28
28
  const instance = {
29
29
  driverName: "restApi",
30
30
  async getMany(args) {
31
31
  args = args || {};
32
- const url = `${opts.baseUrl}/${pluralEntityName}`;
32
+ const url = `${opts.baseUrl}/${handlePlural}`;
33
33
  const fetchOptions = {
34
34
  method: "POST",
35
35
  body: JSON.stringify(args),
@@ -39,7 +39,7 @@ export default (options) => (schema, entity) => {
39
39
  return await fetchInner(url, fetchOptions);
40
40
  },
41
41
  async putMany(item) {
42
- const url = `${opts.baseUrl}/${pluralEntityName}`;
42
+ const url = `${opts.baseUrl}/${handlePlural}`;
43
43
  const fetchOptions = {
44
44
  method: "PUT",
45
45
  body: JSON.stringify({ data: item }),
@@ -22,8 +22,7 @@ var _default = options => {
22
22
  async function preloadEntities(entitiesToPreload) {
23
23
  for (const typeName in entitiesToPreload) {
24
24
  const {
25
- where,
26
- revalidateInterval
25
+ where
27
26
  } = entitiesToPreload[typeName];
28
27
  const handle = schema[typeName]?.handle;
29
28
  if (!handle) throw new Error(`Entity ${typeName} was not found`);
@@ -54,7 +53,7 @@ var _default = options => {
54
53
  ...schema[typeName],
55
54
  name: cacheEntityName,
56
55
  handle: cacheEntityName,
57
- handlePlural: `${cacheEntityName}s`
56
+ handlePlural: `${cacheEntityName}/list`
58
57
  };
59
58
  cacheDrivers[handle] = (0, _radsDb.getDriverInstance)(schema, cacheEntityName, normalizedOptions[typeName].driver, drivers);
60
59
  cacheStatus[handle] = {
@@ -11,7 +11,7 @@ export default (options) => {
11
11
  const { schema, drivers } = context;
12
12
  async function preloadEntities(entitiesToPreload) {
13
13
  for (const typeName in entitiesToPreload) {
14
- const { where, revalidateInterval } = entitiesToPreload[typeName];
14
+ const { where } = entitiesToPreload[typeName];
15
15
  const handle = schema[typeName]?.handle;
16
16
  if (!handle)
17
17
  throw new Error(`Entity ${typeName} was not found`);
@@ -36,7 +36,7 @@ export default (options) => {
36
36
  ...schema[typeName],
37
37
  name: cacheEntityName,
38
38
  handle: cacheEntityName,
39
- handlePlural: `${cacheEntityName}s`
39
+ handlePlural: `${cacheEntityName}/list`
40
40
  };
41
41
  cacheDrivers[handle] = getDriverInstance(schema, cacheEntityName, normalizedOptions[typeName].driver, drivers);
42
42
  cacheStatus[handle] = { preloadStatus: normalizedOptions[typeName].preload ? "neverLoaded" : void 0 };
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.parseSchema = parseSchema;
7
7
  var _typescript = require("typescript");
8
8
  var _lodash = _interopRequireDefault(require("lodash"));
9
- var _pluralize = _interopRequireDefault(require("pluralize"));
10
9
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
10
  const supportedPrimitiveTypes = ["string", "number", "boolean", "Record<string, string>", "Record<string, any>"];
12
11
  function parseSchema(typescriptFiles) {
@@ -186,7 +185,7 @@ function parseClassDeclaration(typeDeclaration, typeName, ctx) {
186
185
  fields[field.name] = field;
187
186
  }
188
187
  const handle = _lodash.default.lowerFirst(name);
189
- const handlePlural = (0, _pluralize.default)(handle);
188
+ const handlePlural = `${handle}/list`;
190
189
  const result = {
191
190
  name,
192
191
  handle,
@@ -268,23 +267,13 @@ function parseClassMember(node, parentFields, parentName, ctx) {
268
267
  if (!parentField) throw new Error(`Cannot find field ${parentName}.${defaultValueCopyFrom}"`);
269
268
  defaultValueType = parentField.type;
270
269
  }
271
- const {
272
- isArray,
273
- isRelation,
274
- isChange,
275
- relationDenormFields,
276
- type
277
- } = parseFieldType(ctx, parentName, name, node, defaultValueType);
270
+ const parsedType = parseFieldType(ctx, parentName, name, node, defaultValueType);
278
271
  const result = {
279
- type,
272
+ ...parsedType,
280
273
  defaultValue,
281
274
  defaultValueCopyFrom,
282
275
  name,
283
276
  isRequired,
284
- isArray,
285
- isRelation,
286
- isChange,
287
- relationDenormFields,
288
277
  comment
289
278
  };
290
279
  if (!_lodash.default.isEmpty(decorators)) result.decorators = decorators;
@@ -311,6 +300,7 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueType) {
311
300
  };
312
301
  parseFieldTypeArray(parsedType, parentName);
313
302
  parseFieldTypeRelation(parsedType, parentName, ctx);
303
+ parseFieldTypeInverseRelation(parsedType, parentName, ctx);
314
304
  parseFieldTypeInlineEnum(parsedType, parentName, fieldName, ctx);
315
305
  parseFieldTypeKeyofEnum(parsedType, parentName, fieldName, ctx);
316
306
  parseFieldTypeRecordEnum(parsedType, parentName, fieldName, ctx);
@@ -328,8 +318,10 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueType) {
328
318
  return {
329
319
  isArray: parsedType.isArray || void 0,
330
320
  isRelation: parsedType.isRelation || void 0,
321
+ isInverseRelation: parsedType.isInverseRelation || void 0,
331
322
  isChange: parsedType.isChange || void 0,
332
323
  relationDenormFields: parsedType.relationDenormFields,
324
+ inverseRelationField: parsedType.inverseRelationField,
333
325
  type: parsedType.type
334
326
  };
335
327
  }
@@ -382,6 +374,28 @@ function parseFieldTypeRelation(parsedType, parentName, ctx) {
382
374
  }
383
375
  }
384
376
  }
377
+ function parseFieldTypeInverseRelation(parsedType, parentName, ctx) {
378
+ if (parsedType.nodeType?.kind !== _typescript.SyntaxKind.TypeReference) return;
379
+ const nt = parsedType.nodeType;
380
+ if (nt.typeName.getText(ctx.sourceFile) === "InverseRelation") {
381
+ if (!nt.typeArguments?.length) throw new Error(`Missing type argument for Relation<>: '${parentName}'`);
382
+ parsedType.type = parseStringField(ctx, nt.typeArguments[0]);
383
+ parsedType.isInverseRelation = true;
384
+ if (nt.typeArguments[1]) {
385
+ const taNode = nt.typeArguments[1];
386
+ parsedType.inverseRelationField = parseStringField(ctx, taNode);
387
+ }
388
+ }
389
+ }
390
+ function parseStringField(ctx, node) {
391
+ if (node.kind === _typescript.SyntaxKind.LiteralType) {
392
+ const literal = node.literal;
393
+ if (literal.kind === _typescript.SyntaxKind.StringLiteral) {
394
+ return literal.text;
395
+ }
396
+ }
397
+ throw new Error(`Unexpected type - ${node.getText(ctx.sourceFile)}`);
398
+ }
385
399
  function parseFieldTypeInlineEnum(parsedType, parentName, fieldName, ctx) {
386
400
  if (parsedType.nodeType?.kind !== _typescript.SyntaxKind.UnionType) return;
387
401
  const nt = parsedType.nodeType;
@@ -564,6 +578,14 @@ function verifyRelationFields(result) {
564
578
  if (field.isRelation && !result[field.type]?.decorators?.entity) {
565
579
  throw new Error(`${key}.${fieldName}: Relation must point to an entity. Got "${field.type}" instead.`);
566
580
  }
581
+ if (field.isInverseRelation) {
582
+ if (!result[field.type]?.decorators?.entity) throw new Error(`${key}.${fieldName}: InverseRelation must point to an entity. Got "${field.type}" instead.`);
583
+ if (field.isRequired) throw new Error(`${key}.${fieldName}: InverseRelation cannot be required. Please, add "?" to the field name.`);
584
+ if (!field.isArray) throw new Error(`${key}.${fieldName}: InverseRelation must be an array.`);
585
+ if (!field.inverseRelationField) throw new Error(`${key}.${fieldName}: InverseRelation must have a field name.`);
586
+ if (!result[field.type]?.fields?.[field.inverseRelationField]?.isRelation) throw new Error(`${key}.${fieldName}: InverseRelation field must point to relation field. Got "${field.inverseRelationField}" instead.`);
587
+ if (result[field.type]?.fields?.[field.inverseRelationField]?.type !== key) throw new Error(`${key}.${fieldName}: InverseRelation field must point to the current entity. Got "${field.inverseRelationField}" instead.`);
588
+ }
567
589
  }
568
590
  }
569
591
  }
@@ -1,6 +1,5 @@
1
1
  import { ScriptTarget, SyntaxKind, createSourceFile } from "typescript";
2
2
  import _ from "lodash";
3
- import pluralize from "pluralize";
4
3
  const supportedPrimitiveTypes = ["string", "number", "boolean", "Record<string, string>", "Record<string, any>"];
5
4
  export function parseSchema(typescriptFiles) {
6
5
  const typeNodesMap = getTypeNodesMap(typescriptFiles);
@@ -163,7 +162,7 @@ function parseClassDeclaration(typeDeclaration, typeName, ctx) {
163
162
  fields[field.name] = field;
164
163
  }
165
164
  const handle = _.lowerFirst(name);
166
- const handlePlural = pluralize(handle);
165
+ const handlePlural = `${handle}/list`;
167
166
  const result = {
168
167
  name,
169
168
  handle,
@@ -232,23 +231,13 @@ function parseClassMember(node, parentFields, parentName, ctx) {
232
231
  throw new Error(`Cannot find field ${parentName}.${defaultValueCopyFrom}"`);
233
232
  defaultValueType = parentField.type;
234
233
  }
235
- const { isArray, isRelation, isChange, relationDenormFields, type } = parseFieldType(
236
- ctx,
237
- parentName,
238
- name,
239
- node,
240
- defaultValueType
241
- );
234
+ const parsedType = parseFieldType(ctx, parentName, name, node, defaultValueType);
242
235
  const result = {
243
- type,
236
+ ...parsedType,
244
237
  defaultValue,
245
238
  defaultValueCopyFrom,
246
239
  name,
247
240
  isRequired,
248
- isArray,
249
- isRelation,
250
- isChange,
251
- relationDenormFields,
252
241
  comment
253
242
  };
254
243
  if (!_.isEmpty(decorators))
@@ -282,6 +271,7 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueType) {
282
271
  };
283
272
  parseFieldTypeArray(parsedType, parentName);
284
273
  parseFieldTypeRelation(parsedType, parentName, ctx);
274
+ parseFieldTypeInverseRelation(parsedType, parentName, ctx);
285
275
  parseFieldTypeInlineEnum(parsedType, parentName, fieldName, ctx);
286
276
  parseFieldTypeKeyofEnum(parsedType, parentName, fieldName, ctx);
287
277
  parseFieldTypeRecordEnum(parsedType, parentName, fieldName, ctx);
@@ -301,8 +291,10 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueType) {
301
291
  return {
302
292
  isArray: parsedType.isArray || void 0,
303
293
  isRelation: parsedType.isRelation || void 0,
294
+ isInverseRelation: parsedType.isInverseRelation || void 0,
304
295
  isChange: parsedType.isChange || void 0,
305
296
  relationDenormFields: parsedType.relationDenormFields,
297
+ inverseRelationField: parsedType.inverseRelationField,
306
298
  type: parsedType.type
307
299
  };
308
300
  }
@@ -360,6 +352,30 @@ function parseFieldTypeRelation(parsedType, parentName, ctx) {
360
352
  }
361
353
  }
362
354
  }
355
+ function parseFieldTypeInverseRelation(parsedType, parentName, ctx) {
356
+ if (parsedType.nodeType?.kind !== SyntaxKind.TypeReference)
357
+ return;
358
+ const nt = parsedType.nodeType;
359
+ if (nt.typeName.getText(ctx.sourceFile) === "InverseRelation") {
360
+ if (!nt.typeArguments?.length)
361
+ throw new Error(`Missing type argument for Relation<>: '${parentName}'`);
362
+ parsedType.type = parseStringField(ctx, nt.typeArguments[0]);
363
+ parsedType.isInverseRelation = true;
364
+ if (nt.typeArguments[1]) {
365
+ const taNode = nt.typeArguments[1];
366
+ parsedType.inverseRelationField = parseStringField(ctx, taNode);
367
+ }
368
+ }
369
+ }
370
+ function parseStringField(ctx, node) {
371
+ if (node.kind === SyntaxKind.LiteralType) {
372
+ const literal = node.literal;
373
+ if (literal.kind === SyntaxKind.StringLiteral) {
374
+ return literal.text;
375
+ }
376
+ }
377
+ throw new Error(`Unexpected type - ${node.getText(ctx.sourceFile)}`);
378
+ }
363
379
  function parseFieldTypeInlineEnum(parsedType, parentName, fieldName, ctx) {
364
380
  if (parsedType.nodeType?.kind !== SyntaxKind.UnionType)
365
381
  return;
@@ -537,6 +553,24 @@ function verifyRelationFields(result) {
537
553
  if (field.isRelation && !result[field.type]?.decorators?.entity) {
538
554
  throw new Error(`${key}.${fieldName}: Relation must point to an entity. Got "${field.type}" instead.`);
539
555
  }
556
+ if (field.isInverseRelation) {
557
+ if (!result[field.type]?.decorators?.entity)
558
+ throw new Error(`${key}.${fieldName}: InverseRelation must point to an entity. Got "${field.type}" instead.`);
559
+ if (field.isRequired)
560
+ throw new Error(`${key}.${fieldName}: InverseRelation cannot be required. Please, add "?" to the field name.`);
561
+ if (!field.isArray)
562
+ throw new Error(`${key}.${fieldName}: InverseRelation must be an array.`);
563
+ if (!field.inverseRelationField)
564
+ throw new Error(`${key}.${fieldName}: InverseRelation must have a field name.`);
565
+ if (!result[field.type]?.fields?.[field.inverseRelationField]?.isRelation)
566
+ throw new Error(
567
+ `${key}.${fieldName}: InverseRelation field must point to relation field. Got "${field.inverseRelationField}" instead.`
568
+ );
569
+ if (result[field.type]?.fields?.[field.inverseRelationField]?.type !== key)
570
+ throw new Error(
571
+ `${key}.${fieldName}: InverseRelation field must point to the current entity. Got "${field.inverseRelationField}" instead.`
572
+ );
573
+ }
540
574
  }
541
575
  }
542
576
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rads-db",
3
- "version": "3.0.58",
3
+ "version": "3.0.62",
4
4
  "packageManager": "pnpm@8.6.1",
5
5
  "description": "Say goodbye to boilerplate code and hello to efficient and elegant syntax.",
6
6
  "author": "",
@@ -77,7 +77,6 @@
77
77
  "ignore": "^5.2.4",
78
78
  "klaw": "^4.1.0",
79
79
  "lodash": "^4.17.21",
80
- "pluralize": "^8.0.0",
81
80
  "uuid": ">=8",
82
81
  "zod": "^3.21.4"
83
82
  },
@@ -87,7 +86,6 @@
87
86
  "@types/eslint": "^8.44.8",
88
87
  "@types/klaw": "^3.0.3",
89
88
  "@types/lodash": "4.14.191",
90
- "@types/pluralize": "^0.0.29",
91
89
  "@types/uuid": "^9.0.2",
92
90
  "@vitest/ui": "^1.6.0",
93
91
  "cross-env": "^7.0.3",