@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 +1 -1
- package/src/runtime.js +72 -17
- package/types/index.d.ts +5 -2
package/package.json
CHANGED
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,
|
|
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,
|
|
821
|
+
await validator.validate(gqltype.name, fieldEntryName, fieldValue, session);
|
|
798
822
|
}
|
|
799
823
|
}
|
|
800
824
|
|
|
801
|
-
if (
|
|
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] =
|
|
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(
|
|
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(
|
|
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] =
|
|
854
|
+
collectionFields[fieldEntryName] = fieldValue;
|
|
831
855
|
} else if (fieldEntry.extensions.relation.embedded) {
|
|
832
856
|
const collectionEntries = [];
|
|
833
857
|
|
|
834
|
-
for (const element of
|
|
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] =
|
|
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
|
|
964
|
+
if (ownValue(item, storageName) === undefined && memberList) item[storageName] = [];
|
|
935
965
|
if (memberName === 'id' || member.extensions?.readOnly) continue;
|
|
936
|
-
const memberValue = item
|
|
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
|
|
983
|
-
const newObjectData = materializedModel.modelArgs
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|