@simtlix/simfinity-js 3.7.0 → 3.9.0
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 -0
- package/package.json +3 -3
- package/src/mongo/adapter.js +5 -1
- package/src/mongo/models.js +20 -5
- package/src/mongo/queries.js +61 -6
- package/types/index.d.ts +22 -1
package/README.md
CHANGED
|
@@ -24,6 +24,10 @@ Generated single-reference reads can be batched within a request. Models with an
|
|
|
24
24
|
|
|
25
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
|
+
Virtual fields (`extensions: { virtual: true }`, with the field's own `resolve`) get no path, no `[]` default and no index in generated models, on entities and embedded types, and no entry in the transactional integrity registry, so they need no `relation` in either mode and documents look the same with or without them. Generated writes never set or unset them, and Mongoose's strict mode drops a value that a controller assigns to a generated document for one. An embedded type whose members are all virtual becomes a strict subdocument without paths rather than a Mixed path, so nothing is stored for it but the `_id` of list items. With a generated model, values stored before a field became virtual stay in the documents: Mongoose drops an `$unset` of a path the model does not declare, so remove them with a driver-level update, such as `getModel(Type).collection.updateMany({}, { $unset: { busyRanges: '' } })`, or with `updateMany(filter, update, { strict: false })`; a generated update that sets an embedded parent rewrites it, which drops the stored keys of its virtual members. A supplied model keeps the paths it declares: documents stored earlier keep their values for the field, which nothing reads or writes until `$unset` removes them, and an index on such a path stays until you drop it. `buildQuery()` rejects a path through a virtual field with `INVALID_FILTER_FIELD` (400).
|
|
28
|
+
|
|
29
|
+
Text filters compile to escaped regular expressions, so a value never becomes regex syntax: `LIKE` and `ILIKE` match anywhere, `STARTS_WITH` and `ISTARTS_WITH` are anchored with `^`, `ENDS_WITH` and `IENDS_WITH` with `\z` (so a trailing newline is part of the value), and `IEQ` with both. The `I` operators add the `i` option, which applies the Unicode simple case folding of the server's PCRE2 build (`É`/`é`, `ẞ`/`ß`, `σ`/`ς`, `µ`/`μ`, `ſ`/`s`), never a length-changing or Turkic folding (`ß`/`ss`, `İ`/`i`), and does not fold characters newer than that build, such as U+A7DC; the [queries guide](https://simtlix.github.io/simfinity.js/guide/queries.html) lists where PostgreSQL differs. Only `STARTS_WITH` reads a prefix range of an index on the field, a String list or an embedded path; the other text operators test every key of such an index, or every document without one, and `$regex` ignores collations, so a case-insensitive collation index does not serve them. A value whose escaped pattern is longer than MongoDB's 32,764-byte pattern limit fails with `INVALID_FILTER_VALUE` (400) before the query runs, also for `LIKE`. `createMongoAdapter()` declares every operator it compiles in `queryFeatures`; a copy of the adapter without it supports only the operators of 3.7.0.
|
|
30
|
+
|
|
27
31
|
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
32
|
|
|
29
33
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-js",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
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.
|
|
33
|
-
"@simtlix/simfinity-mcp": "3.
|
|
32
|
+
"@simtlix/simfinity-core": "3.9.0",
|
|
33
|
+
"@simtlix/simfinity-mcp": "3.9.0"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
package/src/mongo/adapter.js
CHANGED
|
@@ -3,7 +3,7 @@ import { SimfinityError } from '@simtlix/simfinity-core';
|
|
|
3
3
|
import { getListShape, normalizeConnectionField } from '@simtlix/simfinity-core/internal/relation-storage';
|
|
4
4
|
|
|
5
5
|
import { createMongoModel } from './models.js';
|
|
6
|
-
import { createMongoQueries } from './queries.js';
|
|
6
|
+
import { MONGO_QUERY_OPERATORS, createMongoQueries } from './queries.js';
|
|
7
7
|
import { withMongoTransaction } from './transactions.js';
|
|
8
8
|
import { createMongoIntegrity } from './integrity.js';
|
|
9
9
|
import {
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const withSession = (query, session) => (session ? query.session(session) : query);
|
|
14
14
|
|
|
15
|
+
// Every filter operator the query compiler supports; the runtime rejects any other one.
|
|
16
|
+
const QUERY_FEATURES = Object.freeze({ operators: MONGO_QUERY_OPERATORS });
|
|
17
|
+
|
|
15
18
|
// A write that fails only on malformed ObjectId values reports NOT_VALID_ID, not a driver error.
|
|
16
19
|
const mapIdCastErrors = async (write) => {
|
|
17
20
|
try {
|
|
@@ -216,6 +219,7 @@ export const createMongoAdapter = (options) => {
|
|
|
216
219
|
};
|
|
217
220
|
|
|
218
221
|
const adapter = {
|
|
222
|
+
queryFeatures: QUERY_FEATURES,
|
|
219
223
|
bind(binding) {
|
|
220
224
|
integrity.bind(binding);
|
|
221
225
|
getRegistrations = binding.getRegistrations;
|
package/src/mongo/models.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from 'graphql';
|
|
13
13
|
import mongoose from 'mongoose';
|
|
14
14
|
import { SimfinityError } from '@simtlix/simfinity-core';
|
|
15
|
-
import { getFieldStorageName } from '@simtlix/simfinity-core/internal/relation-storage';
|
|
15
|
+
import { getFieldStorageName, isVirtualField } from '@simtlix/simfinity-core/internal/relation-storage';
|
|
16
16
|
|
|
17
17
|
const isNonNullOfType = (fieldEntryType, graphQLType) => (
|
|
18
18
|
fieldEntryType instanceof GraphQLNonNull && fieldEntryType.ofType instanceof graphQLType
|
|
@@ -97,11 +97,24 @@ const assertNoEmbeddedCycle = (gqlType, entryType, ancestors, fieldPath) => {
|
|
|
97
97
|
if (ancestors.includes(entryType)) throw new SimfinityError(`Embedded cycle at ${fieldPath}`, 'INVALID_MODEL', 400);
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
// Mongoose reads an empty definition, `{}` or `[{}]`, as Mixed, which stores any keys. An embedded
|
|
101
|
+
// type whose members are all virtual has such a definition, so it gets a strict subdocument without
|
|
102
|
+
// paths instead: no value of its virtual members, or any other key, is stored. A list item keeps its
|
|
103
|
+
// `_id`, as other embedded list items do. Embedded types without virtual members keep their
|
|
104
|
+
// definition, even an empty one.
|
|
105
|
+
const storedEmbeddedDefinition = (entryType, definition, list) => {
|
|
106
|
+
if (Object.keys(definition).length || !Object.values(entryType.getFields()).some(isVirtualField)) return definition;
|
|
107
|
+
return list ? new mongoose.Schema({}) : new mongoose.Schema({}, { _id: false });
|
|
108
|
+
};
|
|
109
|
+
|
|
100
110
|
const generateSchemaDefinition = (gqlType, nested = false, ancestors = [], path = gqlType.name) => {
|
|
101
111
|
const argTypes = gqlType.getFields();
|
|
102
112
|
const schemaArg = {};
|
|
103
113
|
|
|
104
114
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
115
|
+
// A virtual field is resolved by its own resolver and stored nowhere, on entities and embedded
|
|
116
|
+
// types alike: it gets no path, so no `[]` default and no ObjectId index either.
|
|
117
|
+
if (isVirtualField(fieldEntry)) continue;
|
|
105
118
|
const { type } = fieldEntry;
|
|
106
119
|
|
|
107
120
|
if (matchesScalar(type, GraphQLID)) {
|
|
@@ -123,9 +136,10 @@ const generateSchemaDefinition = (gqlType, nested = false, ancestors = [], path
|
|
|
123
136
|
const entryType = unwrapNonNull(type);
|
|
124
137
|
const fieldPath = `${path}.${fieldEntryName}`;
|
|
125
138
|
assertNoEmbeddedCycle(gqlType, entryType, ancestors, fieldPath);
|
|
126
|
-
const definition =
|
|
139
|
+
const definition = storedEmbeddedDefinition(entryType,
|
|
140
|
+
generateSchemaDefinition(entryType, true, [...ancestors, gqlType], fieldPath), false);
|
|
127
141
|
// A nested object under a `type` key can only be a subdocument; like other embedded objects, it has no _id.
|
|
128
|
-
schemaArg[fieldEntryName] = nested && fieldEntryName === 'type'
|
|
142
|
+
schemaArg[fieldEntryName] = nested && fieldEntryName === 'type' && !(definition instanceof mongoose.Schema)
|
|
129
143
|
? new mongoose.Schema(definition, { _id: false })
|
|
130
144
|
: definition;
|
|
131
145
|
}
|
|
@@ -137,7 +151,8 @@ const generateSchemaDefinition = (gqlType, nested = false, ancestors = [], path
|
|
|
137
151
|
assertEmbeddedFieldName(gqlType, fieldEntryName);
|
|
138
152
|
const fieldPath = `${path}.${fieldEntryName}`;
|
|
139
153
|
assertNoEmbeddedCycle(gqlType, itemType, ancestors, fieldPath);
|
|
140
|
-
schemaArg[fieldEntryName] = [
|
|
154
|
+
schemaArg[fieldEntryName] = [storedEmbeddedDefinition(itemType,
|
|
155
|
+
generateSchemaDefinition(itemType, true, [...ancestors, gqlType], fieldPath), true)];
|
|
141
156
|
}
|
|
142
157
|
} else if (listItemMatchesScalar(type, GraphQLID)) {
|
|
143
158
|
schemaArg[fieldEntryName] = [mongoose.Schema.Types.ObjectId];
|
|
@@ -186,7 +201,7 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
186
201
|
if (definition === mongoose.Schema.Types.ObjectId) {
|
|
187
202
|
objectIdFields.push(currentPath);
|
|
188
203
|
} else if (Array.isArray(definition)) {
|
|
189
|
-
const arrayElement = definition[0];
|
|
204
|
+
const arrayElement = definition[0] instanceof mongoose.Schema ? definition[0].obj : definition[0];
|
|
190
205
|
if (typeof arrayElement === 'object' && arrayElement !== null) {
|
|
191
206
|
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
192
207
|
}
|
package/src/mongo/queries.js
CHANGED
|
@@ -9,7 +9,10 @@ import mongoose from 'mongoose';
|
|
|
9
9
|
|
|
10
10
|
import { SimfinityError, QLOperator, paginationStages } from '@simtlix/simfinity-core';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
TEXT_QUERY_OPERATORS, textQueryOperator, validateTextFilterValue,
|
|
13
|
+
} from '@simtlix/simfinity-core/internal/query-operators';
|
|
14
|
+
import {
|
|
15
|
+
getFieldStorageName, getListShape, hasStoredIdentity, isVirtualField, normalizeConnectionField,
|
|
13
16
|
} from '@simtlix/simfinity-core/internal/relation-storage';
|
|
14
17
|
|
|
15
18
|
import { castObjectId } from './ids.js';
|
|
@@ -26,6 +29,15 @@ const isGraphQLisoDate = (typeName) => (
|
|
|
26
29
|
// GraphQL reserves `__` names, so these aliases never replace a stored field.
|
|
27
30
|
const LOOKUP_ALIAS_PREFIX = '__sf_l';
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The filter operators this compiler supports, which the adapter declares as its queryFeatures. They
|
|
34
|
+
* are listed explicitly, so that a QLOperator value added later stays unsupported until it compiles.
|
|
35
|
+
*/
|
|
36
|
+
export const MONGO_QUERY_OPERATORS = Object.freeze([
|
|
37
|
+
'EQ', 'NE', 'LT', 'LTE', 'GT', 'GTE', 'BTW', 'IN', 'NIN', 'LIKE',
|
|
38
|
+
'ILIKE', 'STARTS_WITH', 'ISTARTS_WITH', 'ENDS_WITH', 'IENDS_WITH', 'IEQ',
|
|
39
|
+
]);
|
|
40
|
+
|
|
29
41
|
export const createMongoQueries = ({ getModel, getRegistrations }) => {
|
|
30
42
|
// One alias per relation path for each query build, keyed by the build's `included` accumulator.
|
|
31
43
|
const lookupAliases = new WeakMap();
|
|
@@ -62,6 +74,34 @@ const buildRelationLookup = ({
|
|
|
62
74
|
|
|
63
75
|
const escapeRegex = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
76
|
|
|
77
|
+
// MongoDB rejects a $regex pattern longer than 32,764 UTF-8 bytes on the server (Location51091),
|
|
78
|
+
// which would report a client's long value as an internal error. Escaping adds a byte to each
|
|
79
|
+
// metacharacter and the operators add their anchors, so the limit applies to the compiled pattern.
|
|
80
|
+
const MAX_REGEX_PATTERN_BYTES = 32764;
|
|
81
|
+
// A $regex match, rejected with INVALID_FILTER_VALUE before the query runs when MongoDB would reject
|
|
82
|
+
// its pattern. `path` is the condition's GraphQL path, for the message.
|
|
83
|
+
const regexMatch = (operator, pattern, ignoreCase, path) => {
|
|
84
|
+
const bytes = Buffer.byteLength(pattern, 'utf8');
|
|
85
|
+
if (bytes > MAX_REGEX_PATTERN_BYTES) {
|
|
86
|
+
const where = path == null ? '' : ` on ${path}`;
|
|
87
|
+
throw filterError(`${operator} value${where} is too long: MongoDB accepts regular expressions of at most ${MAX_REGEX_PATTERN_BYTES} bytes, and this one has ${bytes}`);
|
|
88
|
+
}
|
|
89
|
+
return ignoreCase ? { $regex: pattern, $options: 'i' } : { $regex: pattern };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// The literal text operators: the escaped value, anchored with `^` at the start of the string and
|
|
93
|
+
// `\z` at its very end (`$` would also match before a final newline), with the `i` option to ignore
|
|
94
|
+
// case. Without the `m` option, `^` never matches after a newline.
|
|
95
|
+
const TEXT_PATTERNS = {
|
|
96
|
+
contains: (escaped) => escaped,
|
|
97
|
+
prefix: (escaped) => `^${escaped}`,
|
|
98
|
+
suffix: (escaped) => `${escaped}\\z`,
|
|
99
|
+
exact: (escaped) => `^${escaped}\\z`,
|
|
100
|
+
};
|
|
101
|
+
const textRegex = (operator, { match, ignoreCase }) => (value, path) => (
|
|
102
|
+
regexMatch(operator, TEXT_PATTERNS[match](escapeRegex(value)), ignoreCase, path)
|
|
103
|
+
);
|
|
104
|
+
|
|
65
105
|
const PATH_SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
66
106
|
|
|
67
107
|
const assertValidFilterPath = (path) => {
|
|
@@ -101,7 +141,8 @@ const OP_TO_MONGO = {
|
|
|
101
141
|
GTE: (v) => ({ $gte: v }),
|
|
102
142
|
NE: (v) => ({ $ne: v }),
|
|
103
143
|
BTW: (v) => ({ $gte: v[0], $lte: v[1] }),
|
|
104
|
-
LIKE: (v) => (
|
|
144
|
+
LIKE: (v, path) => regexMatch('LIKE', `.*${escapeRegex(v)}.*`, false, path),
|
|
145
|
+
...Object.fromEntries(Object.entries(TEXT_QUERY_OPERATORS).map(([op, text]) => [op, textRegex(op, text)])),
|
|
105
146
|
};
|
|
106
147
|
|
|
107
148
|
const AGG_OP_TO_MONGO = {
|
|
@@ -140,6 +181,12 @@ const resolveQueryPath = (gqltype, path, aggregationsIncluded) => {
|
|
|
140
181
|
for (const [index, part] of parts.entries()) {
|
|
141
182
|
const field = Object.hasOwn(currentType.getFields(), part) ? currentType.getFields()[part] : null;
|
|
142
183
|
if (!field) throw filterError(`Unknown query field: ${path}`, 'INVALID_FILTER_FIELD');
|
|
184
|
+
// Core rejects generated and trusted paths through a virtual field before the adapter runs. A
|
|
185
|
+
// native buildQuery() or buildFilterGroupMatch() call fails here, instead of matching storage
|
|
186
|
+
// that does not exist.
|
|
187
|
+
if (isVirtualField(field)) {
|
|
188
|
+
throw filterError(`Query path ${path} uses the virtual field ${currentType.name}.${part}, which is not stored`, 'INVALID_FILTER_FIELD');
|
|
189
|
+
}
|
|
143
190
|
const fieldType = getNamedType(field.type);
|
|
144
191
|
const relation = field.extensions?.relation;
|
|
145
192
|
if (fieldType instanceof GraphQLObjectType) {
|
|
@@ -219,11 +266,18 @@ const castFilterValue = (value, resolved) => {
|
|
|
219
266
|
}
|
|
220
267
|
};
|
|
221
268
|
|
|
222
|
-
|
|
269
|
+
// `path` is the condition's GraphQL path, for messages.
|
|
270
|
+
const buildMatchesClause = (resolved, operator, value, path) => {
|
|
223
271
|
const op = operator ?? 'EQ';
|
|
224
272
|
if (!QLOperator.getValues().some((entry) => entry.value === op)) {
|
|
225
273
|
throw filterError(`Unsupported filter operator: ${op}`, 'INVALID_FILTER_OPERATOR');
|
|
226
274
|
}
|
|
275
|
+
// Text operators take the value as is, once core's shared rule accepts it: a String field (a
|
|
276
|
+
// validated scalar by its base scalar) and a string without U+0000.
|
|
277
|
+
if (textQueryOperator(op)) {
|
|
278
|
+
const text = validateTextFilterValue(op, value, getEffectiveTypeName(resolved.fieldType), path);
|
|
279
|
+
return { [resolved.mongoPath]: OP_TO_MONGO[op](text, path) };
|
|
280
|
+
}
|
|
227
281
|
const collectionOperator = op === 'IN' || op === 'NIN' || op === 'BTW';
|
|
228
282
|
if (collectionOperator && (!Array.isArray(value) || (op === 'BTW' && value.length !== 2))) {
|
|
229
283
|
throw filterError(`${op} requires ${op === 'BTW' ? 'exactly two values' : 'an array value'}`);
|
|
@@ -239,7 +293,7 @@ const buildMatchesClause = (resolved, operator, value) => {
|
|
|
239
293
|
const coerced = collectionOperator ? value.map(cast) : op === 'LIKE' ? value : cast(value);
|
|
240
294
|
if (op === 'EQ') return { [resolved.mongoPath]: coerced };
|
|
241
295
|
if (op === 'IN' || op === 'NIN') return { [resolved.mongoPath]: { [op === 'IN' ? '$in' : '$nin']: coerced } };
|
|
242
|
-
return { [resolved.mongoPath]: OP_TO_MONGO[op](coerced) };
|
|
296
|
+
return { [resolved.mongoPath]: OP_TO_MONGO[op](coerced, path) };
|
|
243
297
|
};
|
|
244
298
|
|
|
245
299
|
const buildQueryTerms = async (filterField, qlField, fieldName, gqltype, aggregationsIncluded) => {
|
|
@@ -256,9 +310,10 @@ const buildQueryTerms = async (filterField, qlField, fieldName, gqltype, aggrega
|
|
|
256
310
|
for (const term of terms) {
|
|
257
311
|
if (!term || typeof term !== 'object' || Array.isArray(term)) throw filterError(`Invalid term for ${fieldName}`);
|
|
258
312
|
if (isObject) assertValidFilterPath(term.path);
|
|
259
|
-
const
|
|
313
|
+
const path = isObject ? `${fieldName}.${term.path}` : fieldName;
|
|
314
|
+
const resolved = resolveQueryPath(gqltype, path, aggregationsIncluded);
|
|
260
315
|
Object.assign(aggregateClauses, resolved.aggregateClauses);
|
|
261
|
-
matchesClauses.push(buildMatchesClause(resolved, term.operator, term.value));
|
|
316
|
+
matchesClauses.push(buildMatchesClause(resolved, term.operator, term.value, path));
|
|
262
317
|
}
|
|
263
318
|
return { aggregateClauses, matchesClauses };
|
|
264
319
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
import type {
|
|
16
16
|
DatabaseAdapter,
|
|
17
17
|
MutationLimitsOptions,
|
|
18
|
+
QueryFeatures,
|
|
18
19
|
RuntimeRegistration,
|
|
19
20
|
SimfinityError,
|
|
20
21
|
} from '@simtlix/simfinity-core';
|
|
@@ -46,6 +47,8 @@ export type {
|
|
|
46
47
|
ScopeParams,
|
|
47
48
|
TypePermissions,
|
|
48
49
|
TypeScopes,
|
|
50
|
+
VirtualFieldDeclaration,
|
|
51
|
+
VirtualFieldOptions,
|
|
49
52
|
} from '@simtlix/simfinity-core';
|
|
50
53
|
export * from '@simtlix/simfinity-mcp';
|
|
51
54
|
export { default as mcp } from '@simtlix/simfinity-mcp';
|
|
@@ -86,6 +89,13 @@ export interface MongoAdapterOptions {
|
|
|
86
89
|
*/
|
|
87
90
|
export interface MongoAdapter extends DatabaseAdapter {
|
|
88
91
|
readonly referentialIntegrity: 'off' | 'transactional';
|
|
92
|
+
/**
|
|
93
|
+
* Frozen; lists every `QLOperator` value. The literal text operators compile to escaped regular
|
|
94
|
+
* expressions: `ILIKE` unanchored, `STARTS_WITH` and `ISTARTS_WITH` anchored with `^`, `ENDS_WITH` and
|
|
95
|
+
* `IENDS_WITH` with `\z`, and `IEQ` with both, with the `i` option for the case-insensitive ones. A
|
|
96
|
+
* copy of the adapter without this member supports only the operators of 3.7.0.
|
|
97
|
+
*/
|
|
98
|
+
readonly queryFeatures: QueryFeatures;
|
|
89
99
|
/**
|
|
90
100
|
* Accepts only an ObjectId or a 24-character hexadecimal string, normalized to lowercase, and
|
|
91
101
|
* throws `NOT_VALID_ID` (400) for anything else, including undefined, null and numbers. It never
|
|
@@ -163,6 +173,13 @@ export function addNoEndpointType(gqltype: GraphQLObjectType): void;
|
|
|
163
173
|
* Embedded types that contain each other throw INVALID_MODEL (400, `Embedded cycle at …`) when a
|
|
164
174
|
* generated model's embedded fields reach them, and always in transactional mode; in default mode a
|
|
165
175
|
* cycle that only supplied models reach is not checked.
|
|
176
|
+
* Invalid virtual field declarations (`extensions.virtual`, see `VirtualFieldOptions`) throw
|
|
177
|
+
* INVALID_MODEL (400) before models are created, in both referentialIntegrity modes. A virtual field
|
|
178
|
+
* gets no path, `[]` default or index in generated models, on entities and embedded types, and no
|
|
179
|
+
* entry in the transactional integrity registry, so it needs no `relation` in either mode; generated
|
|
180
|
+
* writes never set or unset it. A supplied model keeps the paths it declares. A readOnly field with
|
|
181
|
+
* arguments and its own resolver that is not virtual, and is not a reference or collection, logs a
|
|
182
|
+
* `Configuration issue` warning once.
|
|
166
183
|
* A generated input that would have no fields is left out with a warning, together with the
|
|
167
184
|
* collection operation, embedded field or add/update/state-action mutation that would take it; a
|
|
168
185
|
* non-null embedded field whose embedded type has no writable fields throws INVALID_MODEL (400)
|
|
@@ -296,6 +313,7 @@ export interface FilterGroupInput {
|
|
|
296
313
|
* pipeline. `input.aggregation` filters a field named `aggregation`; the aggregate stage is never
|
|
297
314
|
* built here. An embedded `.id` path uses the declared `id` member, except for entity types embedded
|
|
298
315
|
* as lists and supplied subdocument schemas without an `id` path, which use the subdocument `_id`.
|
|
316
|
+
* A filter, condition or sort path through a virtual field rejects with `INVALID_FILTER_FIELD` (400).
|
|
299
317
|
*/
|
|
300
318
|
export function buildQuery(
|
|
301
319
|
input: Record<string, any>,
|
|
@@ -303,7 +321,10 @@ export function buildQuery(
|
|
|
303
321
|
isCount?: boolean,
|
|
304
322
|
): Promise<Array<Record<string, any>>>;
|
|
305
323
|
|
|
306
|
-
/**
|
|
324
|
+
/**
|
|
325
|
+
* Translate a QLFilterGroup into a MongoDB `$match` expression, collecting required lookups. A
|
|
326
|
+
* condition path through a virtual field rejects with `INVALID_FILTER_FIELD` (400).
|
|
327
|
+
*/
|
|
307
328
|
export function buildFilterGroupMatch(
|
|
308
329
|
filterGroup: FilterGroupInput,
|
|
309
330
|
gqltype: GraphQLObjectType,
|