@simtlix/simfinity-js 3.5.0 → 3.5.1
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/README.md +2 -0
- package/package.json +3 -3
- package/src/mongo/models.js +49 -18
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Use a MongoDB replica set or sharded cluster for transactional mutations. The li
|
|
|
22
22
|
|
|
23
23
|
Generated single-reference reads can be batched within a request. Models with any `find` or `findOne` pre/post query hook retain individual `findOne` reads, including hooks shared by both operations, to preserve access restrictions and other middleware behavior. See the [relationship contract](https://simtlix.github.io/simfinity.js/guide/relationships.html).
|
|
24
24
|
|
|
25
|
+
Generated models store enum internal values with their own type (numbers, booleans or strings; other combinations as given) and support embedded fields named `type`. Range filters, sorts and aggregates on numeric and boolean enums follow that type; PostgreSQL uses their text form. Earlier versions stored numeric and boolean enum values as strings; convert existing documents once as described in the [schema guide](https://simtlix.github.io/simfinity.js/guide/schema.html#what-gets-generated).
|
|
26
|
+
|
|
25
27
|
Mongoose 8.24.2 or later within version 8 is required to exclude the update-casting prototype-pollution vulnerability. Existing applications should update Mongoose and their dependency lockfile, then run `npm audit`; upgrading Simfinity does not refresh every transitive dependency already locked by the application.
|
|
26
28
|
|
|
27
29
|
## Optional transactional reference integrity
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-js",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "MongoDB/Mongoose adapter and compatibility facade for Simfinity GraphQL APIs",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"owner": "simtlix"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@simtlix/simfinity-core": "3.5.
|
|
33
|
-
"@simtlix/simfinity-mcp": "3.5.
|
|
32
|
+
"@simtlix/simfinity-core": "3.5.1",
|
|
33
|
+
"@simtlix/simfinity-mcp": "3.5.1"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
package/src/mongo/models.js
CHANGED
|
@@ -53,7 +53,24 @@ const withUnique = (fieldEntry, mongoType) => (fieldEntry.extensions && fieldEnt
|
|
|
53
53
|
? { type: mongoType, unique: true }
|
|
54
54
|
: mongoType);
|
|
55
55
|
|
|
56
|
-
const
|
|
56
|
+
const isPlainObject = (value) => value != null && Object.getPrototypeOf(value) === Object.prototype;
|
|
57
|
+
|
|
58
|
+
// Store enum internal values with their own type, so reads can serialize them. A state machine
|
|
59
|
+
// persists state names in `state`. State machines are not known when models are generated, so every
|
|
60
|
+
// field named `state` keeps accepting names when values are not strings.
|
|
61
|
+
const enumStorageType = (enumType, fieldName) => {
|
|
62
|
+
const values = enumType.getValues().map(({ value }) => value);
|
|
63
|
+
if (values.every((value) => typeof value === 'string')) return String;
|
|
64
|
+
// Mixed keeps values by identity only for primitives; others stay String so the write is rejected.
|
|
65
|
+
const primitive = (value) => value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
66
|
+
if (!values.every(primitive)) return String;
|
|
67
|
+
if (fieldName === 'state') return mongoose.Schema.Types.Mixed;
|
|
68
|
+
if (values.every((value) => typeof value === 'number')) return Number;
|
|
69
|
+
if (values.every((value) => typeof value === 'boolean')) return Boolean;
|
|
70
|
+
return mongoose.Schema.Types.Mixed;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const generateSchemaDefinition = (gqlType, nested = false) => {
|
|
57
74
|
const argTypes = gqlType.getFields();
|
|
58
75
|
const schemaArg = {};
|
|
59
76
|
|
|
@@ -62,10 +79,10 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
62
79
|
|
|
63
80
|
if (matchesScalar(type, GraphQLID)) {
|
|
64
81
|
schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
|
|
65
|
-
} else if (matchesScalar(type, GraphQLString)
|
|
66
|
-
|| type instanceof GraphQLEnumType
|
|
67
|
-
|| isNonNullOfType(type, GraphQLEnumType)) {
|
|
82
|
+
} else if (matchesScalar(type, GraphQLString)) {
|
|
68
83
|
schemaArg[fieldEntryName] = withUnique(fieldEntry, String);
|
|
84
|
+
} else if (type instanceof GraphQLEnumType || isNonNullOfType(type, GraphQLEnumType)) {
|
|
85
|
+
schemaArg[fieldEntryName] = withUnique(fieldEntry, enumStorageType(unwrapNonNull(type), fieldEntryName));
|
|
69
86
|
} else if (matchesScalar(type, GraphQLInt) || matchesScalar(type, GraphQLFloat)) {
|
|
70
87
|
schemaArg[fieldEntryName] = withUnique(fieldEntry, Number);
|
|
71
88
|
} else if (matchesScalar(type, GraphQLBoolean)) {
|
|
@@ -80,7 +97,11 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
80
97
|
if (entryType === gqlType) {
|
|
81
98
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
82
99
|
}
|
|
83
|
-
|
|
100
|
+
const definition = generateSchemaDefinition(entryType, true);
|
|
101
|
+
// A nested object under a `type` key can only be a subdocument; like other embedded objects, it has no _id.
|
|
102
|
+
schemaArg[fieldEntryName] = nested && fieldEntryName === 'type'
|
|
103
|
+
? new mongoose.Schema(definition, { _id: false })
|
|
104
|
+
: definition;
|
|
84
105
|
}
|
|
85
106
|
}
|
|
86
107
|
} else if (getListItemType(type)) {
|
|
@@ -90,12 +111,14 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
90
111
|
if (itemType === gqlType) {
|
|
91
112
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
92
113
|
}
|
|
93
|
-
schemaArg[fieldEntryName] = [generateSchemaDefinition(itemType)];
|
|
114
|
+
schemaArg[fieldEntryName] = [generateSchemaDefinition(itemType, true)];
|
|
94
115
|
}
|
|
95
116
|
} else if (listItemMatchesScalar(type, GraphQLID)) {
|
|
96
117
|
schemaArg[fieldEntryName] = [mongoose.Schema.Types.ObjectId];
|
|
97
|
-
} else if (listItemMatchesScalar(type, GraphQLString)
|
|
118
|
+
} else if (listItemMatchesScalar(type, GraphQLString)) {
|
|
98
119
|
schemaArg[fieldEntryName] = [String];
|
|
120
|
+
} else if (itemType instanceof GraphQLEnumType) {
|
|
121
|
+
schemaArg[fieldEntryName] = [enumStorageType(itemType)];
|
|
99
122
|
} else if (listItemMatchesScalar(type, GraphQLBoolean)) {
|
|
100
123
|
schemaArg[fieldEntryName] = [Boolean];
|
|
101
124
|
} else if (listItemMatchesScalar(type, GraphQLInt) || listItemMatchesScalar(type, GraphQLFloat)) {
|
|
@@ -108,6 +131,12 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
133
|
|
|
134
|
+
// In a nested definition, Mongoose reads a `type` key as the type of the whole embedded path.
|
|
135
|
+
// Declare a field named `type` with an option object; plain objects here already are one.
|
|
136
|
+
if (nested && Object.hasOwn(schemaArg, 'type') && !isPlainObject(schemaArg.type)) {
|
|
137
|
+
schemaArg.type = { type: schemaArg.type };
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
return schemaArg;
|
|
112
141
|
};
|
|
113
142
|
|
|
@@ -116,20 +145,22 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
116
145
|
|
|
117
146
|
for (const [fieldName, fieldDefinition] of Object.entries(schemaDefinition)) {
|
|
118
147
|
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
148
|
+
// Look through option objects, including those that declare an embedded field named `type`.
|
|
149
|
+
let definition = fieldDefinition;
|
|
150
|
+
if (isPlainObject(definition) && definition.type && !isPlainObject(definition.type)) {
|
|
151
|
+
definition = definition.type;
|
|
152
|
+
}
|
|
153
|
+
if (definition instanceof mongoose.Schema) definition = definition.obj;
|
|
119
154
|
|
|
120
|
-
if (
|
|
155
|
+
if (definition === mongoose.Schema.Types.ObjectId) {
|
|
121
156
|
objectIdFields.push(currentPath);
|
|
122
|
-
} else if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
127
|
-
}
|
|
128
|
-
} else if (fieldDefinition.type === mongoose.Schema.Types.ObjectId) {
|
|
129
|
-
objectIdFields.push(currentPath);
|
|
130
|
-
} else if (!fieldDefinition.type) {
|
|
131
|
-
objectIdFields.push(...findObjectIdFields(fieldDefinition, currentPath));
|
|
157
|
+
} else if (Array.isArray(definition)) {
|
|
158
|
+
const arrayElement = definition[0];
|
|
159
|
+
if (typeof arrayElement === 'object' && arrayElement !== null) {
|
|
160
|
+
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
132
161
|
}
|
|
162
|
+
} else if (isPlainObject(definition)) {
|
|
163
|
+
objectIdFields.push(...findObjectIdFields(definition, currentPath));
|
|
133
164
|
}
|
|
134
165
|
}
|
|
135
166
|
|