@simtlix/simfinity-js 3.5.7 → 3.5.9
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 +4 -2
- package/package.json +3 -3
- package/src/auth/errors.d.ts +2 -0
- package/src/auth/expressions.d.ts +2 -0
- package/src/auth/index.d.ts +2 -0
- package/src/auth/rules.d.ts +2 -0
- package/src/const/QLOperator.d.ts +2 -0
- package/src/const/QLSort.d.ts +2 -0
- package/src/const/QLValue.d.ts +2 -0
- package/src/errors/internal-server.error.d.ts +2 -0
- package/src/errors/simfinity.error.d.ts +2 -0
- package/src/index.d.ts +2 -0
- package/src/mcp.d.ts +3 -0
- package/src/mongo/adapter.js +133 -2
- package/src/mongo/models.js +17 -9
- package/src/mongo/queries.js +4 -28
- package/src/plugins.d.ts +2 -0
- package/src/scalars.d.ts +2 -0
- package/src/validators.d.ts +2 -0
- package/types/index.d.ts +33 -1
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ 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).
|
|
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`. Embedded types that contain each other are rejected with `INVALID_MODEL` (`Embedded cycle at …`) when a generated model reaches them, and always in transactional mode; in the default mode, a cycle that only supplied models reach is not checked. 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
26
|
|
|
27
|
-
The facade registers generated models on your application's default Mongoose instance and does not change global Mongoose options such as `strictQuery`. `createMongoAdapter().castId` accepts only an ObjectId or its 24-character hexadecimal string and throws `NOT_VALID_ID` (400) otherwise; create new identifiers with `new mongoose.Types.ObjectId()`. Owned transactions retry transient failures after a short random delay, and a write conflict that outlasts the retries fails with `TRANSACTION_RETRY_EXCEEDED` (409).
|
|
27
|
+
The facade registers generated models on your application's default Mongoose instance and does not change global Mongoose options such as `strictQuery`. Supplied models may use another connection, such as one from `mongoose.createConnection()`: generated mutations and `saveObject()` use the model's connection, and, in the default `referentialIntegrity: 'off'` mode, custom mutations use the default connection unless it is unused, never opened and without compiled models, also on its `useDb()` descendants, and the registered models share one MongoDB client; with transactional reference integrity they use the first protected model's connection. `createMongoAdapter().castId` accepts only an ObjectId or its 24-character hexadecimal string and throws `NOT_VALID_ID` (400) otherwise; create new identifiers with `new mongoose.Types.ObjectId()`. Owned transactions retry transient failures after a short random delay, and a write conflict that outlasts the retries fails with `TRANSACTION_RETRY_EXCEEDED` (409).
|
|
28
28
|
|
|
29
29
|
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.
|
|
30
30
|
|
|
@@ -50,6 +50,8 @@ Owned transactions use snapshot reads and majority commits. Supplied sessions ne
|
|
|
50
50
|
|
|
51
51
|
The source now lives in `packages/mongodb` in the monorepo. Its public npm name remains `@simtlix/simfinity-js`; existing imports, including paths under `@simtlix/simfinity-js/src/`, retain their layout. Shared helpers, error identities and MCP compatibility exports are preserved. Internal Simfinity dependencies use the same exact release version.
|
|
52
52
|
|
|
53
|
+
Its legacy `src/` modules that re-export public API (`src/index.js`, `src/auth/index.js`, `src/auth/errors.js`, `src/auth/expressions.js`, `src/auth/rules.js`, `src/plugins.js`, `src/scalars.js`, `src/validators.js`, `src/mcp.js`, `src/const/*.js`, `src/errors/*.js`, and their extensionless aliases) ship TypeScript declarations with the types of the package root and the matching `@simtlix/simfinity-core` subpaths; `src/mongo/*` is internal and undeclared. New code should still import `auth`, `plugins`, `scalars` and `validators` from the package root. Importing the typed `@simtlix/simfinity-core` subpaths, such as `@simtlix/simfinity-core/auth`, instead needs `@simtlix/simfinity-core` as a direct dependency pinned to this package's exact version.
|
|
54
|
+
|
|
53
55
|
For tooling that locates the package, use `require.resolve('@simtlix/simfinity-js/package.json')` or the package root without a trailing slash. Directory specifiers ending in `/` were not valid ESM imports and are not supported by the export map, including in `require.resolve`; use explicit filenames such as `src/auth/index.js`.
|
|
54
56
|
|
|
55
57
|
- [MongoDB quick start](https://simtlix.github.io/simfinity.js/guide/getting-started.html)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-js",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.9",
|
|
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.9",
|
|
33
|
+
"@simtlix/simfinity-mcp": "3.5.9"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
package/src/index.d.ts
ADDED
package/src/mcp.d.ts
ADDED
package/src/mongo/adapter.js
CHANGED
|
@@ -50,11 +50,116 @@ const hasNoFindHooks = (hooks) => {
|
|
|
50
50
|
const batchesFindOne = (Model) => hasNoFindHooks(Model.schema?.s?.hooks)
|
|
51
51
|
&& (Model.Query?.prototype?._queryMiddleware == null || hasNoFindHooks(Model.Query.prototype._queryMiddleware));
|
|
52
52
|
|
|
53
|
+
// `useDb()` connections share their parent's MongoClient, and a session belongs to a client.
|
|
54
|
+
const clientOf = (connection) => connection.getClient?.() || connection;
|
|
55
|
+
|
|
56
|
+
// Whether a schema lets Mongoose render a member inside an embedded object from something other
|
|
57
|
+
// than its stored value: a getter on a path, a virtual, an alias or, on a subdocument, a method
|
|
58
|
+
// (GraphQL's default resolver calls one named like a member), at any depth (nested paths,
|
|
59
|
+
// subdocuments, document arrays, map values). Core cannot tell such an object's data without
|
|
60
|
+
// running them, so rawEmbeddedValue leaves it unconverted and core keeps it. Read from schema
|
|
61
|
+
// metadata only. The `id` virtual that Mongoose adds to a schema with an `_id` renders that `_id`,
|
|
62
|
+
// which core reads itself, so it does not count; one the application declares does: it has another
|
|
63
|
+
// getter, or the schema disables the automatic one.
|
|
64
|
+
const isAutomaticIdVirtual = (schema, name, virtual) => name === 'id' && virtual?.getters?.length === 1
|
|
65
|
+
&& schema.options?.id !== false && schema.paths._id !== undefined && schema.paths.id === undefined;
|
|
66
|
+
const schemaTypeComputes = (schemaType, seen) => {
|
|
67
|
+
if (!(schemaType instanceof mongoose.SchemaType) || seen.has(schemaType)) return false;
|
|
68
|
+
seen.add(schemaType);
|
|
69
|
+
if (schemaType.getters?.length > 0) return true;
|
|
70
|
+
// schemaComputes is defined below: schemas and their paths contain each other.
|
|
71
|
+
if (schemaType.schema && schemaComputes(schemaType.schema, '', seen)) return true;
|
|
72
|
+
// Map values, array items and document array elements.
|
|
73
|
+
return [schemaType.$__schemaType, schemaType.$embeddedSchemaType, schemaType.caster]
|
|
74
|
+
.some((inner) => schemaTypeComputes(inner, seen));
|
|
75
|
+
};
|
|
76
|
+
// `prefix` is a nested path of the schema; '' is the whole schema, as a subdocument's own.
|
|
77
|
+
const schemaComputes = (schema, prefix, seen) => {
|
|
78
|
+
if (prefix === '') {
|
|
79
|
+
if (seen.has(schema)) return false;
|
|
80
|
+
seen.add(schema);
|
|
81
|
+
// Nested paths get no schema methods; a subdocument's own schema can.
|
|
82
|
+
if (Object.keys(schema.methods ?? {}).length > 0) return true;
|
|
83
|
+
}
|
|
84
|
+
const inside = (path) => prefix === '' || path.startsWith(`${prefix}.`);
|
|
85
|
+
return Object.entries(schema.paths ?? {}).some(([path, schemaType]) => inside(path) && schemaTypeComputes(schemaType, seen))
|
|
86
|
+
|| Object.entries(schema.virtuals ?? {}).some(([name, virtual]) => inside(name)
|
|
87
|
+
&& !(prefix === '' && isAutomaticIdVirtual(schema, name, virtual)))
|
|
88
|
+
// An alias stores its value under another path, and may be declared outside a nested path.
|
|
89
|
+
|| Object.entries(schema.aliases ?? {}).some(([alias, path]) => inside(alias) || inside(path));
|
|
90
|
+
};
|
|
91
|
+
// Schemas do not change once their models are compiled, so each answer is computed once.
|
|
92
|
+
const computesCache = new WeakMap();
|
|
93
|
+
const cachedComputes = (key, prefix, compute) => {
|
|
94
|
+
let byPrefix = computesCache.get(key);
|
|
95
|
+
if (!byPrefix) {
|
|
96
|
+
byPrefix = new Map();
|
|
97
|
+
computesCache.set(key, byPrefix);
|
|
98
|
+
}
|
|
99
|
+
if (!byPrefix.has(prefix)) byPrefix.set(prefix, compute());
|
|
100
|
+
return byPrefix.get(prefix);
|
|
101
|
+
};
|
|
102
|
+
const declaresComputedMembers = (schema, prefix) => cachedComputes(schema, prefix, () => schemaComputes(schema, prefix, new Set()));
|
|
103
|
+
const declaresComputedValues = (schemaType) => cachedComputes(schemaType, '', () => schemaTypeComputes(schemaType, new Set()));
|
|
104
|
+
// A hydrated nested path is cached by its full path in its document's internal getters cache, which
|
|
105
|
+
// it shares; '' (the whole schema) if it is not found there.
|
|
106
|
+
const nestedPathOf = (nested) => {
|
|
107
|
+
const cache = nested.$__?.getters;
|
|
108
|
+
return (cache && Object.keys(cache).find((path) => cache[path] === nested)) ?? '';
|
|
109
|
+
};
|
|
110
|
+
|
|
53
111
|
export const createMongoAdapter = (options) => {
|
|
54
112
|
const integrity = createMongoIntegrity(options);
|
|
55
113
|
let queries;
|
|
114
|
+
let getRegistrations = () => [];
|
|
56
115
|
const privateConnectionFields = new Map();
|
|
57
116
|
|
|
117
|
+
// A registered custom mutation passes no model, so its owned session stays on the default
|
|
118
|
+
// mongoose.connection, as before. While that connection has never been opened (no client), a
|
|
119
|
+
// session there can only wait and time out. Use the one MongoDB client of the registered models
|
|
120
|
+
// instead, but only when nothing can need the default connection:
|
|
121
|
+
// - any model compiled on it, generated or the application's, registered or not, means the
|
|
122
|
+
// application opens it, maybe after the request arrived, and the callback may write that
|
|
123
|
+
// model with the session, which must then come from the default connection's client. The
|
|
124
|
+
// same holds for a model compiled on any useDb() descendant of it, such as
|
|
125
|
+
// mongoose.connection.useDb('tenant').useDb('audit'), which shares its client. A connection's
|
|
126
|
+
// otherDbs lists its direct useDb() children, not theirs, so the walk follows otherDbs at any
|
|
127
|
+
// depth.
|
|
128
|
+
// useDb(name, { noListener: true }) leaves a connection out of its parent's otherDbs, so neither
|
|
129
|
+
// it nor its descendants are seen, and native collection writes
|
|
130
|
+
// (mongoose.connection.collection()) compile no model;
|
|
131
|
+
// - registered models on several clients have no single client to choose.
|
|
132
|
+
// Once the default connection has a client, nothing changes.
|
|
133
|
+
const hasModels = (connection) => Object.keys(connection.models).length > 0;
|
|
134
|
+
// A child's otherDbs also lists its parent, so the walk visits each connection once, cycles
|
|
135
|
+
// included: a Set iteration also visits the entries added while it runs.
|
|
136
|
+
const defaultClientHasModels = () => {
|
|
137
|
+
const seen = new Set([mongoose.connection]);
|
|
138
|
+
for (const connection of seen) {
|
|
139
|
+
if (hasModels(connection)) return true;
|
|
140
|
+
for (const other of connection.otherDbs) seen.add(other);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
const sessionModel = () => {
|
|
145
|
+
if (mongoose.connection.getClient() || defaultClientHasModels()) return undefined;
|
|
146
|
+
let found;
|
|
147
|
+
for (const { model } of getRegistrations()) {
|
|
148
|
+
const connection = model?.db;
|
|
149
|
+
if (!connection) continue;
|
|
150
|
+
if (connection === mongoose.connection) return undefined;
|
|
151
|
+
if (!found) found = model;
|
|
152
|
+
else if (clientOf(found.db) !== clientOf(connection)) return undefined;
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// A supplied session or model is used as is; only an owned session without a model looks up
|
|
158
|
+
// the registrations.
|
|
159
|
+
const withOwnedTransaction = (session, body, Model, ...rest) => withMongoTransaction(
|
|
160
|
+
session, body, (Model || session) ? Model : sessionModel(), ...rest,
|
|
161
|
+
);
|
|
162
|
+
|
|
58
163
|
const addPrivateConnectionField = (typeName, fieldName) => {
|
|
59
164
|
if (!privateConnectionFields.has(typeName)) privateConnectionFields.set(typeName, new Set());
|
|
60
165
|
privateConnectionFields.get(typeName).add(fieldName);
|
|
@@ -63,6 +168,7 @@ export const createMongoAdapter = (options) => {
|
|
|
63
168
|
const adapter = {
|
|
64
169
|
bind(binding) {
|
|
65
170
|
integrity.bind(binding);
|
|
171
|
+
getRegistrations = binding.getRegistrations;
|
|
66
172
|
queries = createMongoQueries(binding);
|
|
67
173
|
adapter.buildQuery = queries.buildQuery;
|
|
68
174
|
adapter.buildFilterGroupMatch = queries.buildFilterGroupMatch;
|
|
@@ -103,7 +209,7 @@ export const createMongoAdapter = (options) => {
|
|
|
103
209
|
// Relation inputs and batch keys: only an ObjectId or its hex form, never a newly minted one.
|
|
104
210
|
castId: castObjectId,
|
|
105
211
|
initialize: integrity.initialize,
|
|
106
|
-
withTransaction: integrity.enabled ? integrity.withTransaction :
|
|
212
|
+
withTransaction: integrity.enabled ? integrity.withTransaction : withOwnedTransaction,
|
|
107
213
|
newRecord(Model, data, session) {
|
|
108
214
|
const record = new Model(data);
|
|
109
215
|
record.$session(session);
|
|
@@ -118,7 +224,8 @@ export const createMongoAdapter = (options) => {
|
|
|
118
224
|
},
|
|
119
225
|
// A hydrated document renders a singular embedded path as an object even when the stored value
|
|
120
226
|
// is an explicit null. Only that stored null reads as null; an absent value keeps Mongoose's
|
|
121
|
-
// materialized object
|
|
227
|
+
// materialized object; core then reads it as null when its stored members (rawEmbeddedValue)
|
|
228
|
+
// hold no data and miss a required member (runtime.js readEmbeddedObject).
|
|
122
229
|
readEmbeddedValue(value) {
|
|
123
230
|
// The nested accessor's toJSON is called without a receiver on purpose. It reads the schema's
|
|
124
231
|
// toJSON virtuals option from `this` (guarded by `this &&`), and with that option set a stored
|
|
@@ -126,6 +233,30 @@ export const createMongoAdapter = (options) => {
|
|
|
126
233
|
return value?.$__isNested === true && typeof value.toJSON === 'function' && value.toJSON.call(null) === null
|
|
127
234
|
? null : value;
|
|
128
235
|
},
|
|
236
|
+
// The stored data of a hydrated embedded value, which core reads to tell whether it holds data
|
|
237
|
+
// without running schema getters or virtuals: a nested path's raw value (its toJSON without a
|
|
238
|
+
// receiver, as above), a subdocument's _doc or a Mongoose map's entries. The first two hold an
|
|
239
|
+
// absent nested path as {}, as Mongoose renders it, and keep subdocuments and maps hydrated,
|
|
240
|
+
// which core passes back here. A subdocument's automatic `id` virtual is not in _doc; core reads
|
|
241
|
+
// `_id` for `id`. A value whose schema declares a getter, a virtual, an alias or a subdocument
|
|
242
|
+
// method on a member at any depth inside it (declaresComputedMembers) is returned unchanged, so core counts it as data
|
|
243
|
+
// and keeps it, as 3.5.8 rendered it; no getter runs. Generated models declare none. Any other
|
|
244
|
+
// value is returned unchanged too.
|
|
245
|
+
rawEmbeddedValue(value) {
|
|
246
|
+
if (value?.$__isNested === true) {
|
|
247
|
+
// A schema from another Mongoose copy cannot be inspected; keep the value as 3.5.8 did.
|
|
248
|
+
if (typeof value.toJSON !== 'function' || !(value.$__schema instanceof mongoose.Schema)
|
|
249
|
+
|| declaresComputedMembers(value.$__schema, nestedPathOf(value))) return value;
|
|
250
|
+
return value.toJSON.call(null);
|
|
251
|
+
}
|
|
252
|
+
// Iterating a map reads its stored values; its get() would run the value getters.
|
|
253
|
+
if (value instanceof mongoose.Types.Map) {
|
|
254
|
+
return declaresComputedValues(value.$__schemaType) ? value : Object.fromEntries(value);
|
|
255
|
+
}
|
|
256
|
+
if (!(value instanceof mongoose.Document) || !value._doc) return value;
|
|
257
|
+
const schema = value.$__schema ?? value.schema;
|
|
258
|
+
return !schema || declaresComputedMembers(schema, '') ? value : value._doc;
|
|
259
|
+
},
|
|
129
260
|
getById(Model, id, session, { projection, plain, requiredId } = {}) {
|
|
130
261
|
integrity.assertReady();
|
|
131
262
|
let query = withSession(requiredId == null ? Model.findById(id, projection)
|
package/src/mongo/models.js
CHANGED
|
@@ -87,7 +87,17 @@ const assertEmbeddedFieldName = (gqlType, fieldEntryName) => {
|
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// An embedded field whose type is the type being generated, or a type that embeds it on this path,
|
|
91
|
+
// would nest without end. The path starts at the type whose model is generated. Sibling and nested
|
|
92
|
+
// fields may still embed the same type.
|
|
93
|
+
const assertNoEmbeddedCycle = (gqlType, entryType, ancestors, fieldPath) => {
|
|
94
|
+
if (entryType === gqlType) {
|
|
95
|
+
throw new SimfinityError('A type cannot have a field of its same type and embedded', 'INVALID_MODEL', 400);
|
|
96
|
+
}
|
|
97
|
+
if (ancestors.includes(entryType)) throw new SimfinityError(`Embedded cycle at ${fieldPath}`, 'INVALID_MODEL', 400);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const generateSchemaDefinition = (gqlType, nested = false, ancestors = [], path = gqlType.name) => {
|
|
91
101
|
const argTypes = gqlType.getFields();
|
|
92
102
|
const schemaArg = {};
|
|
93
103
|
|
|
@@ -111,10 +121,9 @@ const generateSchemaDefinition = (gqlType, nested = false) => {
|
|
|
111
121
|
} else {
|
|
112
122
|
assertEmbeddedFieldName(gqlType, fieldEntryName);
|
|
113
123
|
const entryType = unwrapNonNull(type);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const definition = generateSchemaDefinition(entryType, true);
|
|
124
|
+
const fieldPath = `${path}.${fieldEntryName}`;
|
|
125
|
+
assertNoEmbeddedCycle(gqlType, entryType, ancestors, fieldPath);
|
|
126
|
+
const definition = generateSchemaDefinition(entryType, true, [...ancestors, gqlType], fieldPath);
|
|
118
127
|
// A nested object under a `type` key can only be a subdocument; like other embedded objects, it has no _id.
|
|
119
128
|
schemaArg[fieldEntryName] = nested && fieldEntryName === 'type'
|
|
120
129
|
? new mongoose.Schema(definition, { _id: false })
|
|
@@ -126,10 +135,9 @@ const generateSchemaDefinition = (gqlType, nested = false) => {
|
|
|
126
135
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
127
136
|
if (fieldEntry.extensions.relation.embedded) {
|
|
128
137
|
assertEmbeddedFieldName(gqlType, fieldEntryName);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
schemaArg[fieldEntryName] = [generateSchemaDefinition(itemType, true)];
|
|
138
|
+
const fieldPath = `${path}.${fieldEntryName}`;
|
|
139
|
+
assertNoEmbeddedCycle(gqlType, itemType, ancestors, fieldPath);
|
|
140
|
+
schemaArg[fieldEntryName] = [generateSchemaDefinition(itemType, true, [...ancestors, gqlType], fieldPath)];
|
|
133
141
|
}
|
|
134
142
|
} else if (listItemMatchesScalar(type, GraphQLID)) {
|
|
135
143
|
schemaArg[fieldEntryName] = [mongoose.Schema.Types.ObjectId];
|
package/src/mongo/queries.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GraphQLEnumType,
|
|
3
3
|
GraphQLID,
|
|
4
|
-
GraphQLList,
|
|
5
|
-
GraphQLNonNull,
|
|
6
4
|
GraphQLObjectType,
|
|
7
5
|
GraphQLScalarType,
|
|
6
|
+
getNamedType,
|
|
8
7
|
} from 'graphql';
|
|
9
8
|
import mongoose from 'mongoose';
|
|
10
9
|
|
|
@@ -16,18 +15,6 @@ import {
|
|
|
16
15
|
import { castObjectId } from './ids.js';
|
|
17
16
|
import { resolveStorageScalar } from './models.js';
|
|
18
17
|
|
|
19
|
-
const isNonNullOfType = (fieldEntryType, graphQLType) => (
|
|
20
|
-
fieldEntryType instanceof GraphQLNonNull && fieldEntryType.ofType instanceof graphQLType
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const unwrapListAndNonNull = (type) => {
|
|
24
|
-
let unwrapped = type;
|
|
25
|
-
while (unwrapped instanceof GraphQLList || unwrapped instanceof GraphQLNonNull) {
|
|
26
|
-
unwrapped = unwrapped.ofType;
|
|
27
|
-
}
|
|
28
|
-
return unwrapped;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
18
|
const getEffectiveTypeName = (type) => (
|
|
32
19
|
type instanceof GraphQLScalarType ? resolveStorageScalar(type).name : type.name
|
|
33
20
|
);
|
|
@@ -127,12 +114,6 @@ const AGG_OP_TO_MONGO = {
|
|
|
127
114
|
|
|
128
115
|
const filterError = (message, code = 'INVALID_FILTER_VALUE') => new SimfinityError(message, code, 400);
|
|
129
116
|
|
|
130
|
-
const queryNamedType = (type) => {
|
|
131
|
-
let result = type;
|
|
132
|
-
while (result instanceof GraphQLList || result instanceof GraphQLNonNull) result = result.ofType;
|
|
133
|
-
return result;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
117
|
// `id` is the record key `_id` at an entity boundary: the root or a joined document. Inside an
|
|
137
118
|
// embedded object it follows what reads return. Entity types read the stored subdocument `_id`
|
|
138
119
|
// through the generated id resolver; that resolver lives on the type object, so any runtime that
|
|
@@ -159,7 +140,7 @@ const resolveQueryPath = (gqltype, path, aggregationsIncluded) => {
|
|
|
159
140
|
for (const [index, part] of parts.entries()) {
|
|
160
141
|
const field = Object.hasOwn(currentType.getFields(), part) ? currentType.getFields()[part] : null;
|
|
161
142
|
if (!field) throw filterError(`Unknown query field: ${path}`, 'INVALID_FILTER_FIELD');
|
|
162
|
-
const fieldType =
|
|
143
|
+
const fieldType = getNamedType(field.type);
|
|
163
144
|
const relation = field.extensions?.relation;
|
|
164
145
|
if (fieldType instanceof GraphQLObjectType) {
|
|
165
146
|
if (index === parts.length - 1) throw filterError(`Query path must end in a scalar field: ${path}`, 'INVALID_FILTER_PATH');
|
|
@@ -267,7 +248,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName, gqltype, aggrega
|
|
|
267
248
|
if (!qlField) throw filterError(`Unknown filter field: ${fieldName}`, 'INVALID_FILTER_FIELD');
|
|
268
249
|
if (filterField == null) return { aggregateClauses, matchesClauses };
|
|
269
250
|
if (typeof filterField !== 'object' || Array.isArray(filterField)) throw filterError(`Invalid filter for ${fieldName}`);
|
|
270
|
-
const isObject =
|
|
251
|
+
const isObject = getNamedType(qlField.type) instanceof GraphQLObjectType;
|
|
271
252
|
if (isObject && (!Array.isArray(filterField.terms) || filterField.terms.length === 0)) {
|
|
272
253
|
throw filterError(`Filter on ${fieldName} requires non-empty terms`, 'MISSING_FILTER_PATH');
|
|
273
254
|
}
|
|
@@ -309,8 +290,7 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
309
290
|
throw new SimfinityError(`Unknown filter field: ${condition.field}`, 'INVALID_FILTER_FIELD', 400);
|
|
310
291
|
}
|
|
311
292
|
|
|
312
|
-
const
|
|
313
|
-
const isObject = fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType);
|
|
293
|
+
const isObject = getNamedType(qlField.type) instanceof GraphQLObjectType;
|
|
314
294
|
|
|
315
295
|
let filterInput;
|
|
316
296
|
if (isObject) {
|
|
@@ -326,8 +306,6 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
326
306
|
}
|
|
327
307
|
|
|
328
308
|
const result = await buildQueryTerms(filterInput, qlField, condition.field, gqltype, aggregationsIncluded);
|
|
329
|
-
if (!result) continue;
|
|
330
|
-
|
|
331
309
|
appendLookups(aggregateClauses, aggregationsIncluded, result.aggregateClauses);
|
|
332
310
|
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
333
311
|
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
@@ -379,8 +357,6 @@ const collectFiltersAndLookups = async (input, gqltype, aggregateClauses, aggreg
|
|
|
379
357
|
if (controlKeys.has(key)) continue;
|
|
380
358
|
const qlField = fields[key];
|
|
381
359
|
const result = await buildQueryTerms(filterField, qlField, key, gqltype, aggregationsIncluded);
|
|
382
|
-
if (!result) continue;
|
|
383
|
-
|
|
384
360
|
appendLookups(aggregateClauses, aggregationsIncluded, result.aggregateClauses);
|
|
385
361
|
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
386
362
|
for (const [matchKey, match] of Object.entries(matchClause)) {
|
package/src/plugins.d.ts
ADDED
package/src/scalars.d.ts
ADDED
package/types/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type {
|
|
|
36
36
|
EnvelopSchemaPlugin,
|
|
37
37
|
FieldValidations,
|
|
38
38
|
FieldValidator,
|
|
39
|
+
ItemValidators,
|
|
39
40
|
MutationLimitsOptions,
|
|
40
41
|
PermissionSchema,
|
|
41
42
|
PolicyExpression,
|
|
@@ -51,6 +52,14 @@ export interface MongoAdapterOptions {
|
|
|
51
52
|
/** Fixed at adapter creation. Defaults to off; transactional mode requires initialize(). */
|
|
52
53
|
referentialIntegrity?: 'off' | 'transactional';
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* In the default 'off' mode, withTransaction(null, callback) without a model, as custom mutations
|
|
57
|
+
* call it, opens its session on the default mongoose.connection, unless that connection has no
|
|
58
|
+
* client, no model is compiled on it or on its useDb() descendants, and every model of the bound
|
|
59
|
+
* runtime's registrations uses one MongoDB client: then it opens it on that client. A supplied
|
|
60
|
+
* session or model is used as is. In transactional mode it uses the first protected model's
|
|
61
|
+
* connection.
|
|
62
|
+
*/
|
|
54
63
|
export interface MongoAdapter extends DatabaseAdapter {
|
|
55
64
|
readonly referentialIntegrity: 'off' | 'transactional';
|
|
56
65
|
/**
|
|
@@ -61,6 +70,14 @@ export interface MongoAdapter extends DatabaseAdapter {
|
|
|
61
70
|
castId(value: any): any;
|
|
62
71
|
/** Returns null for a hydrated nested path whose stored value is an explicit null; otherwise the value. */
|
|
63
72
|
readEmbeddedValue(value: any): any;
|
|
73
|
+
/**
|
|
74
|
+
* Returns the raw stored data of a hydrated nested path or subdocument, or a Mongoose map's
|
|
75
|
+
* entries as a plain object, without running schema getters or virtuals. Returns the value
|
|
76
|
+
* unchanged when its schema declares a getter, a virtual other than Mongoose's automatic `id`, an
|
|
77
|
+
* alias or a subdocument method on a member at any depth inside it, so Simfinity keeps the object
|
|
78
|
+
* as stored, and for any other value.
|
|
79
|
+
*/
|
|
80
|
+
rawEmbeddedValue(value: any): any;
|
|
64
81
|
/** After connecting MongoDB and createSchema(), await the transaction capability and existing-reference audit. */
|
|
65
82
|
initialize(): Promise<void>;
|
|
66
83
|
}
|
|
@@ -119,6 +136,20 @@ export function addNoEndpointType(gqltype: GraphQLObjectType): void;
|
|
|
119
136
|
* connectionField, whose resolver Simfinity would generate, throws INVALID_MODEL (400). In default
|
|
120
137
|
* mode, one with its own resolver or readOnly only logs a warning and rejects nested writes
|
|
121
138
|
* (INVALID_MODEL, 500); referentialIntegrity 'transactional' rejects every such collection.
|
|
139
|
+
* Embedded types that contain each other throw INVALID_MODEL (400, `Embedded cycle at …`) when a
|
|
140
|
+
* generated model's embedded fields reach them, and always in transactional mode; in default mode a
|
|
141
|
+
* cycle that only supplied models reach is not checked.
|
|
142
|
+
* A generated input that would have no fields is left out with a warning, together with the
|
|
143
|
+
* collection operation, embedded field or add/update/state-action mutation that would take it; a
|
|
144
|
+
* non-null embedded field whose embedded type has no writable fields throws INVALID_MODEL (400)
|
|
145
|
+
* when the generated mutations reach its owner. An application type named FieldExtensionsType or
|
|
146
|
+
* RelationType renames Simfinity's introspection metadata types (SimfinityFieldExtensionsType,
|
|
147
|
+
* SimfinityRelationType) with a warning when it is in the process's first Simfinity schema. That
|
|
148
|
+
* schema fixes the names: in a later createSchema(), an application type with one of the current
|
|
149
|
+
* names (the original ones, or the fallback names after a rename) throws RESERVED_TYPE_NAME (500),
|
|
150
|
+
* before models are created when it is an included endpoint type, a registered mutation's input or
|
|
151
|
+
* output type, an output type they reach or a field argument's type, and when the schema is
|
|
152
|
+
* constructed otherwise.
|
|
122
153
|
*/
|
|
123
154
|
export function createSchema(
|
|
124
155
|
includedQueryTypes?: GraphQLObjectType[] | null,
|
|
@@ -186,7 +217,8 @@ export function configureMutationLimits(options?: MutationLimitsOptions): void;
|
|
|
186
217
|
* Get the generated create input type, preserving field and list-item non-null
|
|
187
218
|
* wrappers. Update inputs remove only the outer wrapper (except entity id).
|
|
188
219
|
* References use IdInputType; embedded lists use nested inputs; referenced
|
|
189
|
-
* collections use added/updated/deleted operation inputs.
|
|
220
|
+
* collections use added/updated/deleted operation inputs. It has no fields for a type without
|
|
221
|
+
* writable fields; such inputs are not part of generated schemas.
|
|
190
222
|
*/
|
|
191
223
|
export function getInputType(type: GraphQLObjectType | { name: string }): GraphQLInputObjectType;
|
|
192
224
|
|