@simtlix/simfinity-core 3.5.5 → 3.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-core",
3
- "version": "3.5.5",
3
+ "version": "3.5.6",
4
4
  "description": "Driver-free GraphQL runtime and model metadata for Simfinity",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/runtime.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  GraphQLObjectType, GraphQLString, GraphQLID, GraphQLSchema, GraphQLList,
5
5
  GraphQLNonNull, GraphQLInputObjectType, GraphQLScalarType,
6
6
  GraphQLInt, GraphQLEnumType, GraphQLBoolean, GraphQLError, GraphQLInterfaceType, GraphQLUnionType,
7
- Kind, defaultFieldResolver, getNamedType,
7
+ Kind, getNamedType,
8
8
  } from 'graphql';
9
9
 
10
10
  import SimfinityError from './errors/simfinity.error.js';
@@ -264,6 +264,29 @@ export const buildErrorFormatter = (callback) => (error) => {
264
264
 
265
265
  export { SimfinityError, InternalServerError };
266
266
 
267
+ // A field named like an Object.prototype member (`constructor`, `toString`, ...) that reads as that
268
+ // very member holds no value: graphql input objects and stored records without the field inherit
269
+ // it, and MongoDB nested paths return it from their own accessors. Other values, overriding members
270
+ // such as class methods and getters, and every other name read as usual. The property is read once,
271
+ // so a getter runs once, as with a plain read.
272
+ const isObjectMember = (key, read) => key in Object.prototype && read === Object.prototype[key];
273
+ const ownValue = (value, key) => {
274
+ if (value == null) return undefined;
275
+ const read = value[key];
276
+ return isObjectMember(key, read) ? undefined : read;
277
+ };
278
+
279
+ // Output fields with such names read through this resolver instead of graphql's default one, which
280
+ // would call the inherited member. Otherwise it resolves as the default one does, calling a method
281
+ // with the parent as receiver, but reads the property only once. It reads no data, so installing it
282
+ // binds no type to a runtime.
283
+ const readOwnField = markGenerated((parent, args, context, info) => {
284
+ if (parent === null || (typeof parent !== 'object' && typeof parent !== 'function')) return undefined;
285
+ const property = parent[info.fieldName];
286
+ if (isObjectMember(info.fieldName, property)) return null;
287
+ return typeof property === 'function' ? property.call(parent, args, context, info) : property;
288
+ });
289
+
267
290
  const cloneInput = (value, type) => {
268
291
  if (value === null || value === undefined || !type) return value;
269
292
  if (type instanceof GraphQLNonNull) return cloneInput(value, type.ofType);
@@ -791,28 +814,29 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
791
814
  const collectionFields = {};
792
815
 
793
816
  for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
817
+ const fieldValue = ownValue(args, fieldEntryName);
794
818
  if (fieldEntry.extensions && fieldEntry.extensions.validations
795
819
  && fieldEntry.extensions.validations[operation]) {
796
820
  for (const validator of fieldEntry.extensions.validations[operation]) {
797
- await validator.validate(gqltype.name, fieldEntryName, args[fieldEntryName], session);
821
+ await validator.validate(gqltype.name, fieldEntryName, fieldValue, session);
798
822
  }
799
823
  }
800
824
 
801
- if (args[fieldEntryName] !== undefined && args[fieldEntryName] !== null) {
825
+ if (fieldValue !== undefined && fieldValue !== null) {
802
826
  if (fieldEntry.type instanceof GraphQLScalarType
803
827
  || fieldEntry.type instanceof GraphQLEnumType
804
828
  || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
805
829
  || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
806
- modelArgs[fieldEntryName] = args[fieldEntryName];
830
+ modelArgs[fieldEntryName] = fieldValue;
807
831
  } else if (fieldEntry.type instanceof GraphQLObjectType
808
832
  || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
809
833
  if (fieldEntry.extensions && fieldEntry.extensions.relation) {
810
834
  if (!fieldEntry.extensions.relation.embedded) {
811
- modelArgs[getFieldStorageName(fieldEntryName, fieldEntry)] = adapter.castId(args[fieldEntryName].id);
835
+ modelArgs[getFieldStorageName(fieldEntryName, fieldEntry)] = adapter.castId(fieldValue.id);
812
836
  } else {
813
837
  const fieldType = fieldEntry.type instanceof GraphQLNonNull
814
838
  ? fieldEntry.type.ofType : fieldEntry.type;
815
- modelArgs[fieldEntryName] = (await materializeModel(args[fieldEntryName], fieldType,
839
+ modelArgs[fieldEntryName] = (await materializeModel(fieldValue, fieldType,
816
840
  null, operation, session)).modelArgs;
817
841
  }
818
842
  } else {
@@ -827,11 +851,11 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
827
851
  if (itemType instanceof GraphQLObjectType && fieldEntry.extensions
828
852
  && fieldEntry.extensions.relation) {
829
853
  if (!fieldEntry.extensions.relation.embedded) {
830
- collectionFields[fieldEntryName] = args[fieldEntryName];
854
+ collectionFields[fieldEntryName] = fieldValue;
831
855
  } else if (fieldEntry.extensions.relation.embedded) {
832
856
  const collectionEntries = [];
833
857
 
834
- for (const element of args[fieldEntryName]) {
858
+ for (const element of fieldValue) {
835
859
  if (element === null) {
836
860
  collectionEntries.push(null);
837
861
  } else {
@@ -843,9 +867,15 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
843
867
  modelArgs[fieldEntryName] = collectionEntries;
844
868
  }
845
869
  } else if (itemType instanceof GraphQLScalarType || itemType instanceof GraphQLEnumType) {
846
- modelArgs[fieldEntryName] = args[fieldEntryName];
870
+ modelArgs[fieldEntryName] = fieldValue;
847
871
  }
848
872
  }
873
+ } else if (operation === 'CREATE' && fieldEntryName in Object.prototype && getListShape(fieldEntry.type)
874
+ && !(fieldEntry.extensions?.relation && !fieldEntry.extensions.relation.embedded)) {
875
+ // A create without a value stores [] for a list: SQL backends store it for every list, and
876
+ // Mongoose defaults to it, except for a list named like an Object.prototype member, where it
877
+ // finds the inherited member. Collections are not stored on the record.
878
+ modelArgs[fieldEntryName] = [];
849
879
  }
850
880
  }
851
881
 
@@ -931,9 +961,9 @@ const completeEmbeddedValue = (fieldName, fieldEntry, value, patch = null) => {
931
961
  const memberList = getListShape(member.type);
932
962
  if (relation && !relation.embedded && memberList) continue;
933
963
  const storageName = getFieldStorageName(memberName, member);
934
- if (item[storageName] === undefined && memberList) item[storageName] = [];
964
+ if (ownValue(item, storageName) === undefined && memberList) item[storageName] = [];
935
965
  if (memberName === 'id' || member.extensions?.readOnly) continue;
936
- const memberValue = item[storageName];
966
+ const memberValue = ownValue(item, storageName);
937
967
  if (memberValue === null || memberValue === undefined) {
938
968
  if (member.type instanceof GraphQLNonNull) {
939
969
  throw new SimfinityError(`Required value ${memberName} is missing`, 'REQUIRED_VALUE', 400);
@@ -979,8 +1009,8 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
979
1009
  });
980
1010
  if (currentObject) {
981
1011
  for (const fieldEntryName of embeddedFieldNames) {
982
- const oldObjectData = currentObject[fieldEntryName];
983
- const newObjectData = materializedModel.modelArgs[fieldEntryName];
1012
+ const oldObjectData = ownValue(currentObject, fieldEntryName);
1013
+ const newObjectData = ownValue(materializedModel.modelArgs, fieldEntryName);
984
1014
  if (newObjectData) {
985
1015
  if (Array.isArray(newObjectData)) {
986
1016
  materializedModel.modelArgs[fieldEntryName] = newObjectData;
@@ -988,7 +1018,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
988
1018
  } else {
989
1019
  materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
990
1020
  clearNullEmbeddedMembers(materializedModel.modelArgs[fieldEntryName],
991
- argTypes[fieldEntryName], args[fieldEntryName]);
1021
+ argTypes[fieldEntryName], ownValue(args, fieldEntryName));
992
1022
  completeEmbeddedValue(fieldEntryName, argTypes[fieldEntryName],
993
1023
  materializedModel.modelArgs[fieldEntryName], newObjectData);
994
1024
  }
@@ -1724,8 +1754,9 @@ const installIdResolver = (gqltype) => {
1724
1754
  // this runtime: another runtime that reaches a shared type reads the same value through it.
1725
1755
  // The value is read as graphql's default resolver reads it, so a method on the parent, such as one a
1726
1756
  // custom mutation result supplies, is still called; a promise it returns is read once it settles.
1757
+ // Only a field named like an Object.prototype member that reads as that member reads as null.
1727
1758
  const readEmbeddedField = markGenerated((parent, args, context, info) => {
1728
- const value = defaultFieldResolver(parent, args, context, info);
1759
+ const value = readOwnField(parent, args, context, info);
1729
1760
  return isThenable(value)
1730
1761
  ? value.then((resolved) => adapter.readEmbeddedValue(resolved))
1731
1762
  : adapter.readEmbeddedValue(value);
@@ -1744,6 +1775,20 @@ const installEmbeddedValueResolvers = (gqltype, visited) => {
1744
1775
  }
1745
1776
  };
1746
1777
 
1778
+ // Fields named like Object.prototype members, of registered types and the embedded types they reach,
1779
+ // read through readOwnField when they have no resolver yet; references and collections keep the ones
1780
+ // autoGenerateResolvers gives them.
1781
+ const installObjectMemberResolvers = (gqltype, visited) => {
1782
+ if (!(gqltype instanceof GraphQLObjectType) || visited.has(gqltype)) return;
1783
+ visited.add(gqltype);
1784
+ for (const [fieldName, fieldEntry] of Object.entries(gqltype.getFields())) {
1785
+ const relation = fieldEntry.extensions?.relation;
1786
+ if (relation && !relation.embedded) continue;
1787
+ if (fieldName in Object.prototype && !fieldEntry.resolve) fieldEntry.resolve = readOwnField;
1788
+ if (relation) installObjectMemberResolvers(unwrapListAndNonNull(fieldEntry.type), visited);
1789
+ }
1790
+ };
1791
+
1747
1792
  // Configuration issues this runtime already reported, per type, so a repeated createSchema call or a
1748
1793
  // type reached from several places logs each one once.
1749
1794
  const reportedIssues = new WeakMap();
@@ -1827,6 +1872,9 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
1827
1872
  }
1828
1873
  }
1829
1874
  });
1875
+ // After every type has its relation and embedded value resolvers, which these fields keep.
1876
+ const objectMemberTypes = new Set();
1877
+ Object.values(typesDict.types).forEach(({ gqltype }) => installObjectMemberResolvers(gqltype, objectMemberTypes));
1830
1878
 
1831
1879
  const query = buildRootQuery('RootQueryType', includedQueryTypes);
1832
1880
  Object.values(typesDict.types).forEach(({ gqltype }) => {
@@ -1939,8 +1987,15 @@ const autoGenerateResolvers = (gqltype) => {
1939
1987
  if (!relatedTypeInfo || !relatedTypeInfo.model) {
1940
1988
  throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
1941
1989
  }
1942
- // Any stored identifier except null or an empty string, so numeric IDs such as 0 resolve.
1943
- const relatedId = parent[connectionField] ?? parent[fieldName];
1990
+ // Any stored identifier except null or an empty string, so numeric IDs such as 0 resolve. A
1991
+ // function under the field name is a member, such as the class a MongoDB document inherits
1992
+ // `constructor` from, not an identifier.
1993
+ // The field name is read only when the stored identifier is missing.
1994
+ let relatedId = ownValue(parent, connectionField);
1995
+ if (relatedId == null) {
1996
+ const linked = ownValue(parent, fieldName);
1997
+ relatedId = typeof linked === 'function' ? undefined : linked;
1998
+ }
1944
1999
  const id = relatedId?._id ?? relatedId;
1945
2000
  if (id == null || id === '') return null;
1946
2001
  try {
package/types/index.d.ts CHANGED
@@ -66,9 +66,12 @@ export class SimfinityError extends Error {
66
66
  getCode(): string | undefined;
67
67
  getStatus(): number | undefined;
68
68
  getTimestamp(): string;
69
- /** Set on errors that wrap a driver error, such as MongoDB's TRANSACTION_RETRY_EXCEEDED. */
69
+ /**
70
+ * Set on errors that wrap a driver error, such as TRANSACTION_RETRY_EXCEEDED, and on the PostgreSQL database errors
71
+ * that a SQL runtime maps or reports as DATABASE_ERROR; on those PostgreSQL errors it is not enumerable.
72
+ */
70
73
  cause?: unknown;
71
- /** Set on errors that wrap a driver error, such as MongoDB's TRANSACTION_RETRY_EXCEEDED; returns `cause`. */
74
+ /** Set on errors that wrap a driver error, such as TRANSACTION_RETRY_EXCEEDED, and on PostgreSQL database errors; returns `cause`. */
72
75
  getCause?(): unknown;
73
76
  }
74
77