@simtlix/simfinity-core 3.5.8 → 3.6.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 +3 -3
- package/package.json +1 -1
- package/src/index.js +3 -0
- package/src/introspection.js +42 -8
- package/src/runtime.js +423 -59
- package/types/auth/errors.d.ts +2 -6
- package/types/index.d.ts +63 -3
- package/types/plugins.d.ts +3 -1
package/README.md
CHANGED
|
@@ -16,13 +16,13 @@ For relational storage, `@simtlix/simfinity-sql` builds on core and delegates ph
|
|
|
16
16
|
- `preventCreatingCollection` to pass validation-only storage setup to an adapter;
|
|
17
17
|
- `configureQueryLimits` and `configureMutationLimits` for the process-wide page-size and nested-operation limits.
|
|
18
18
|
|
|
19
|
-
The adapter supplies model creation, identifiers, transactions, record reads and writes, queries, counts, aggregations, and inverse collection reads. `DatabaseAdapter`, `Runtime`, controller, middleware, registration, and state-machine interfaces are exported in the TypeScript declarations. A runtime binds one adapter; create another runtime to use another database or configuration. An adapter can also define `getByIds(model, ids, { context })`, which returns the records it finds in `getById` shape and any order; the runtime then batches generated single-reference reads per request and type, matching records by `castId`, splitting batches at `maxPageSize`, and reading an ID with `getById` when a batch read fails or when `castId` would normalize or reject it. `castId` should throw a `NOT_VALID_ID` (400) `SimfinityError` for a malformed value and must not create a new identifier. When `getById` rejects a stored reference value that way, including one that `castId` rejected first, the generated reference field fails with an `InternalServerError` (500), since the client did not send the value. `update()` resolves to the updated record as stored, or null; nested collection children of that record are linked to and checked against its `_id`, else `id`, else the requested ID. An adapter that hydrates records can define `readEmbeddedValue(value)`: every nullable, singular embedded object field without a resolver then passes its value through it, so an explicitly stored `null` reads as `null`. The field is read as graphql's default resolver reads it, so a method on the parent is called and a promise it returns is awaited before the hook receives the value. The MongoDB adapter defines
|
|
19
|
+
The adapter supplies model creation, identifiers, transactions, record reads and writes, queries, counts, aggregations, and inverse collection reads. `DatabaseAdapter`, `Runtime`, controller, middleware, registration, and state-machine interfaces are exported in the TypeScript declarations. A runtime binds one adapter; create another runtime to use another database or configuration. An adapter can also define `getByIds(model, ids, { context })`, which returns the records it finds in `getById` shape and any order; the runtime then batches generated single-reference reads per request and type, matching records by `castId`, splitting batches at `maxPageSize`, and reading an ID with `getById` when a batch read fails or when `castId` would normalize or reject it. `castId` should throw a `NOT_VALID_ID` (400) `SimfinityError` for a malformed value and must not create a new identifier. An adapter can define `readByIdIsInert(model)`, returning `true` when a `getById` of that model has no side effects and `update()` and `delete()` reject the same malformed IDs with `NOT_VALID_ID` (400) and return null for a missing record; the runtime then skips the reads before deletes without an `onDelete` hook and before updates that merge no embedded value and have neither `onUpdating` nor nested collection items. The SQL adapter always reports it, and the MongoDB adapter does for models without `find`, `findOne`, `findOneAndUpdate` or `findOneAndDelete` middleware and without application `_id` setters or overrides of the statics those reads and writes call. When `getById` rejects a stored reference value that way, including one that `castId` rejected first, the generated reference field fails with an `InternalServerError` (500), since the client did not send the value. `update()` resolves to the updated record as stored, or null; nested collection children of that record are linked to and checked against its `_id`, else `id`, else the requested ID. An adapter that hydrates records can define `readEmbeddedValue(value)`: every nullable, singular embedded object field without a resolver then passes its value through it, so an explicitly stored `null` reads as `null`. The field is read as graphql's default resolver reads it, so a method on the parent is called and a promise it returns is awaited before the hook receives the value. Simfinity then reads as `null` an object that holds no data when a required member would read as `null`, not counting `id`, `readOnly`, interface and union members, members with an application resolver and referenced collections; runtimes that share the type read the same. It reads the stored members, never calling a getter, and counts a stored `_id` as `id`; it reads nothing from objects whose type has no member that counts, and an error while reading returns the object unchanged. An adapter whose hydrated records render members through getters can also define `rawEmbeddedValue(value)`, which returns a record's stored data as a plain object, or the record unchanged when that data does not show what it renders; a value that is not a plain object counts as data. The MongoDB adapter defines both.
|
|
20
20
|
|
|
21
21
|
Entity types, those registered with `connect()` and the targets of non-embedded relations, resolve `id` to `_id`, falling back to `id`, whatever the `createSchema()` allowlists include; types used only as embedded values keep their declared `id`. Embedded copies of an entity type resolve the same way, so they return their declared `id` unless the adapter stores an `_id` for them, as MongoDB does for embedded list items. The resolver is set on the type object, so it applies in every runtime that shares the type. `createSchema()` marks these registrations with `storedIdentity: true` before calling `adapter.prepare()`.
|
|
22
22
|
|
|
23
23
|
When a runtime first creates a schema, it binds each GraphQL object type whose non-embedded relation fields receive its generated resolvers. It also reserves each reachable object type that still has a non-embedded relation field without a resolver, such as an unregistered custom mutation result. Registering a bound or reserved type in another runtime, or building another runtime's schema that reaches it through fields, interfaces, union members or custom mutation results, throws `TYPE_BOUND_TO_OTHER_RUNTIME` (409). `createSchema()` also rejects a type with a relation field copied, by `toConfig()` or a field spread, after another runtime generated that field's resolver. Create type objects once per runtime, for example with a factory function. Types without non-embedded relation fields, and types whose relation fields all have application resolvers, can be shared, and another runtime can use a `toConfig()` copy made before the first runtime built its schema. A shared custom mutation result type with an unresolved relation field is rejected.
|
|
24
24
|
|
|
25
|
-
Registration and `createSchema()` validate `extensions.scope` (`INVALID_SCOPE`) and reject list or embedded relations to unregistered types (`UNREGISTERED_RELATION_TARGET`); `use()` rejects non-functions (`INVALID_MIDDLEWARE`). Before `adapter.prepare()`, `createSchema()` rejects a writable non-embedded list relation without a non-empty child `connectionField` whose resolver it would generate (`INVALID_MODEL`, 400). A collection without `connectionField` that has an application resolver, or is `readOnly`, only logs a `Configuration issue` warning, unless it is both, and nested writes through it fail with `INVALID_MODEL` (500) before anything is written; adapters that describe the whole model, such as PostgreSQL and transactional MongoDB, still reject it in `prepare()`. Fields named like generated query arguments (`sort`, `pagination`, `AND`, `OR`, and `aggregation` on aggregate endpoints) also log a warning once per runtime.
|
|
25
|
+
Registration and `createSchema()` validate `extensions.scope` (`INVALID_SCOPE`) and reject list or embedded relations to unregistered types (`UNREGISTERED_RELATION_TARGET`); `use()` rejects non-functions (`INVALID_MIDDLEWARE`). Before `adapter.prepare()`, `createSchema()` rejects a writable non-embedded list relation without a non-empty child `connectionField` whose resolver it would generate (`INVALID_MODEL`, 400). A collection without `connectionField` that has an application resolver, or is `readOnly`, only logs a `Configuration issue` warning, unless it is both, and nested writes through it fail with `INVALID_MODEL` (500) before anything is written; adapters that describe the whole model, such as PostgreSQL and transactional MongoDB, still reject it in `prepare()`. Fields named like generated query arguments (`sort`, `pagination`, `AND`, `OR`, and `aggregation` on aggregate endpoints) also log a warning once per runtime. Generated inputs that would have no fields are left out with a `Configuration issue` warning, with the collection operations, embedded fields and mutations that would take them; a non-null embedded field whose embedded type has no writable fields throws `INVALID_MODEL` (400) when generated mutations reach its owner. An application type named `FieldExtensionsType` or `RelationType` makes Simfinity rename its introspection metadata types to `SimfinityFieldExtensionsType` and `SimfinityRelationType` when it is in the process's first Simfinity schema. That schema fixes the names: in a later `createSchema()`, an application type with one of the current names, the original ones or the fallback names after a rename, throws `RESERVED_TYPE_NAME` (500), before `adapter.prepare()` when it is an included endpoint type, a registered mutation's input or output type, an output type they reach or a field argument's type, and when the schema is constructed otherwise, such as for a type inside an input object's fields.
|
|
26
26
|
|
|
27
27
|
Generated and custom mutations run through `adapter.withTransaction`. Retried attempts receive fresh clones of GraphQL input objects, lists, and dates while enum values keep their identity. An adapter can define `stateValue` when its persisted enum representation differs from GraphQL enum names.
|
|
28
28
|
|
|
@@ -34,6 +34,6 @@ Importing the runtime installs Simfinity's `__Field.extensions` introspection fi
|
|
|
34
34
|
|
|
35
35
|
`createQueryPlan` and `resolveModelPath` turn Simfinity filter, sort, pagination, and aggregation inputs into a driver-neutral plan resolved against that metadata.
|
|
36
36
|
|
|
37
|
-
The package also exports the `auth`, `validators`, `scalars`, and `plugins` helper namespaces alongside `createValidatedScalar`, `SimfinityError`, `InternalServerError`, `buildErrorFormatter`, `configureQueryLimits`, `configureMutationLimits`, `QLOperator`, `QLSort`, and `QLValue`. `buildErrorFormatter` returns a `GraphQLError` whose `originalError` is the classified Simfinity error. Database packages reuse these exports so helpers, errors, and globally named GraphQL types retain one identity.
|
|
37
|
+
The package also exports the `auth`, `validators`, `scalars`, and `plugins` helper namespaces alongside `createValidatedScalar`, `SimfinityError`, `InternalServerError`, `UnauthenticatedError` and `ForbiddenError` (the same classes as `auth.UnauthenticatedError` and `auth.ForbiddenError`), `buildErrorFormatter`, `configureQueryLimits`, `configureMutationLimits`, `QLOperator`, `QLSort`, and `QLValue`. `buildErrorFormatter` returns a `GraphQLError` whose `originalError` is the classified Simfinity error. Database packages reuse these exports so helpers, errors, and globally named GraphQL types retain one identity.
|
|
38
38
|
|
|
39
39
|
The same helpers are available from the `@simtlix/simfinity-core/auth`, `/auth/errors`, `/auth/expressions`, `/auth/rules`, `/plugins`, `/scalars` and `/validators` subpaths, each with TypeScript declarations whose types match the root namespaces. `moduleResolution` `node16`, `nodenext` and `bundler` read them through the export map, and `node10` through `typesVersions`. The `internal/*` subpaths are not public API and have no declarations.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -13,6 +13,9 @@ export { default as QLValue } from './const/QLValue.js';
|
|
|
13
13
|
export { configureQueryLimits, paginationStages } from './query-limits.js';
|
|
14
14
|
export { configureMutationLimits } from './mutation-limits.js';
|
|
15
15
|
export { default as auth } from './auth/index.js';
|
|
16
|
+
// The same classes as auth.UnauthenticatedError and auth.ForbiddenError; types/index.d.ts has
|
|
17
|
+
// always declared them as root exports.
|
|
18
|
+
export { UnauthenticatedError, ForbiddenError } from './auth/errors.js';
|
|
16
19
|
export { default as plugins } from './plugins.js';
|
|
17
20
|
export { default as scalars } from './scalars.js';
|
|
18
21
|
export { default as validators } from './validators.js';
|
package/src/introspection.js
CHANGED
|
@@ -5,12 +5,11 @@ import {
|
|
|
5
5
|
__Field,
|
|
6
6
|
} from 'graphql';
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
if (!introspectionFields.extensions) {
|
|
8
|
+
// The object types of `__Field.extensions`: FieldExtensionsType and the RelationType of its
|
|
9
|
+
// `relation` field, under the given names.
|
|
10
|
+
const createFieldMetadataType = (extensionsName, relationName) => {
|
|
12
11
|
const RelationType = new GraphQLObjectType({
|
|
13
|
-
name:
|
|
12
|
+
name: relationName,
|
|
14
13
|
fields: () => ({
|
|
15
14
|
embedded: { type: GraphQLBoolean },
|
|
16
15
|
connectionField: { type: GraphQLString },
|
|
@@ -18,20 +17,55 @@ if (!introspectionFields.extensions) {
|
|
|
18
17
|
}),
|
|
19
18
|
});
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
name:
|
|
20
|
+
return new GraphQLObjectType({
|
|
21
|
+
name: extensionsName,
|
|
23
22
|
fields: () => ({
|
|
24
23
|
relation: { type: RelationType },
|
|
25
24
|
stateMachine: { type: GraphQLBoolean },
|
|
26
25
|
readOnly: { type: GraphQLBoolean },
|
|
27
26
|
}),
|
|
28
27
|
});
|
|
28
|
+
};
|
|
29
29
|
|
|
30
|
+
// getFields() supports both lazy and already materialized introspection fields.
|
|
31
|
+
// Reuse the shared field on repeated module evaluation to preserve type identity.
|
|
32
|
+
const introspectionFields = __Field.getFields();
|
|
33
|
+
if (!introspectionFields.extensions) {
|
|
30
34
|
introspectionFields.extensions = {
|
|
31
|
-
type: FieldExtensionsType,
|
|
35
|
+
type: createFieldMetadataType('FieldExtensionsType', 'RelationType'),
|
|
32
36
|
name: 'extensions',
|
|
33
37
|
resolve: (obj) => obj.extensions,
|
|
34
38
|
args: [],
|
|
35
39
|
isDeprecated: false,
|
|
36
40
|
};
|
|
37
41
|
}
|
|
42
|
+
|
|
43
|
+
// Set on the shared extensions field, by any evaluation of this module, once a Simfinity schema
|
|
44
|
+
// contains the metadata types; their names do not change after that.
|
|
45
|
+
const NAMES_FIXED = Symbol.for('simfinity.fieldMetadataNamesFixed');
|
|
46
|
+
const extensionsField = () => __Field.getFields().extensions;
|
|
47
|
+
|
|
48
|
+
/** The metadata types `__Field.extensions` returns now: [FieldExtensionsType, RelationType]. */
|
|
49
|
+
export const fieldMetadataTypes = () => {
|
|
50
|
+
const extensionsType = extensionsField().type;
|
|
51
|
+
return [extensionsType, extensionsType.getFields().relation.type];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const fieldMetadataNamesFixed = () => extensionsField()[NAMES_FIXED] === true;
|
|
55
|
+
|
|
56
|
+
export const fixFieldMetadataNames = () => {
|
|
57
|
+
extensionsField()[NAMES_FIXED] = true;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Every schema constructed afterwards, in the whole process, contains the metadata types under the
|
|
61
|
+
// new names. The extensions field object keeps its identity, so other evaluations of this module and
|
|
62
|
+
// the auth plugin, which read its type when they need it, follow the change. Returns a function that
|
|
63
|
+
// puts the previous type objects back, for a schema whose construction fails.
|
|
64
|
+
export const renameFieldMetadataTypes = (extensionsName, relationName) => {
|
|
65
|
+
const field = extensionsField();
|
|
66
|
+
const previous = field.type;
|
|
67
|
+
field.type = createFieldMetadataType(extensionsName, relationName);
|
|
68
|
+
return () => {
|
|
69
|
+
field.type = previous;
|
|
70
|
+
};
|
|
71
|
+
};
|
package/src/runtime.js
CHANGED
|
@@ -19,7 +19,9 @@ import { isThenable } from './auth/thenable.js';
|
|
|
19
19
|
import {
|
|
20
20
|
getFieldStorageName, getListShape, markStoredIdentity, normalizeConnectionField,
|
|
21
21
|
} from './relation-storage.js';
|
|
22
|
-
import
|
|
22
|
+
import {
|
|
23
|
+
fieldMetadataNamesFixed, fieldMetadataTypes, fixFieldMetadataNames, renameFieldMetadataTypes,
|
|
24
|
+
} from './introspection.js';
|
|
23
25
|
|
|
24
26
|
// Resolvers Simfinity generates. Any other resolver on a registered type when its schema is first
|
|
25
27
|
// created is application code, such as a masking resolver, recorded once per type so later
|
|
@@ -212,8 +214,9 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
212
214
|
|
|
213
215
|
// The type called `name` that a schema built from these roots would contain, other than through
|
|
214
216
|
// `skip`, following the references GraphQLSchema collects: fields, arguments, interfaces, union
|
|
215
|
-
// members and input fields.
|
|
216
|
-
|
|
217
|
+
// members and input fields. With `inputFields` false, input objects are matched by their own name
|
|
218
|
+
// only, and their fields are never read.
|
|
219
|
+
const findSchemaType = (roots, name, skip, { inputFields = true } = {}) => {
|
|
217
220
|
const visited = new Set();
|
|
218
221
|
const pending = [...roots];
|
|
219
222
|
while (pending.length) {
|
|
@@ -228,13 +231,75 @@ const findSchemaType = (roots, name, skip) => {
|
|
|
228
231
|
pending.push(field.type, ...field.args.map((arg) => arg.type));
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
|
-
if (type instanceof GraphQLInputObjectType) {
|
|
234
|
+
if (inputFields && type instanceof GraphQLInputObjectType) {
|
|
232
235
|
for (const field of Object.values(type.getFields())) pending.push(field.type);
|
|
233
236
|
}
|
|
234
237
|
}
|
|
235
238
|
return null;
|
|
236
239
|
};
|
|
237
240
|
|
|
241
|
+
// Every schema contains Simfinity's field metadata types, the types of `__Field.extensions`, named
|
|
242
|
+
// FieldExtensionsType and RelationType unless a clash renamed them. Returns the type of a schema built
|
|
243
|
+
// from these roots that has the name of one of them, or null (#162).
|
|
244
|
+
const fieldMetadataClash = (roots, options) => fieldMetadataTypes()
|
|
245
|
+
.map((own) => findSchemaType(roots, own.name, own, options)).find(Boolean) || null;
|
|
246
|
+
|
|
247
|
+
// The first schema that contains the metadata types fixes their names for the process, so a later
|
|
248
|
+
// schema with a type of the same name is rejected.
|
|
249
|
+
const reservedTypeNameError = (clash) => new SimfinityError(
|
|
250
|
+
`Type ${clash.name} has the name of a type that Simfinity adds to every schema for __Field.extensions, `
|
|
251
|
+
+ 'and a schema created earlier in this process already contains that type under this name. Rename '
|
|
252
|
+
+ `${clash.name}, or create this schema first, so that Simfinity gives its types other names, such as `
|
|
253
|
+
+ 'SimfinityFieldExtensionsType and SimfinityRelationType',
|
|
254
|
+
'RESERVED_TYPE_NAME',
|
|
255
|
+
500,
|
|
256
|
+
);
|
|
257
|
+
// The check before anything is built reads only what the output type check before it reads: output
|
|
258
|
+
// types, their fields, arguments, interfaces and union members. Input objects, a registered
|
|
259
|
+
// mutation's input among them, are checked by their own name, because their fields function may read
|
|
260
|
+
// generated inputs (getInputType) that do not exist yet, and graphql-js keeps what it returns. A clash
|
|
261
|
+
// inside an input's fields is found when the schema is constructed.
|
|
262
|
+
const assertFieldMetadataNamesFree = (roots) => {
|
|
263
|
+
const clash = fieldMetadataNamesFixed() && fieldMetadataClash(roots, { inputFields: false });
|
|
264
|
+
if (clash) throw reservedTypeNameError(clash);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Constructs the schema of these roots. When one of its types has the name of a field metadata type,
|
|
268
|
+
// those are renamed SimfinityFieldExtensionsType and SimfinityRelationType, with a number appended
|
|
269
|
+
// when the schema also has those names, in every schema the process constructs from then on. A
|
|
270
|
+
// construction that fails puts the previous metadata types back, so only a schema that exists fixes
|
|
271
|
+
// the names.
|
|
272
|
+
const constructSchema = (query, mutation) => {
|
|
273
|
+
const roots = [query, mutation];
|
|
274
|
+
const clash = fieldMetadataClash(roots);
|
|
275
|
+
if (clash && fieldMetadataNamesFixed()) throw reservedTypeNameError(clash);
|
|
276
|
+
let names = null;
|
|
277
|
+
if (clash) {
|
|
278
|
+
const taken = (name) => findSchemaType(roots, name) !== null;
|
|
279
|
+
let suffix = '';
|
|
280
|
+
for (let n = 2; taken(`SimfinityFieldExtensionsType${suffix}`) || taken(`SimfinityRelationType${suffix}`); n += 1) {
|
|
281
|
+
suffix = String(n);
|
|
282
|
+
}
|
|
283
|
+
names = [`SimfinityFieldExtensionsType${suffix}`, `SimfinityRelationType${suffix}`];
|
|
284
|
+
}
|
|
285
|
+
const restore = names ? renameFieldMetadataTypes(...names) : null;
|
|
286
|
+
let schema;
|
|
287
|
+
try {
|
|
288
|
+
schema = new GraphQLSchema({ query, mutation });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (restore) restore();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
fixFieldMetadataNames();
|
|
294
|
+
if (names) {
|
|
295
|
+
console.warn(`Configuration issue: the schema has a type named ${clash.name}, the name of a type that `
|
|
296
|
+
+ `Simfinity adds to every schema for __Field.extensions, so Simfinity names its types ${names[0]} and `
|
|
297
|
+
+ `${names[1]} in every schema this process creates from now on. Use these names in named fragments `
|
|
298
|
+
+ 'and permission maps.');
|
|
299
|
+
}
|
|
300
|
+
return schema;
|
|
301
|
+
};
|
|
302
|
+
|
|
238
303
|
// graphql-js wraps a value thrown while resolving a field (locatedError): the wrapper has the field
|
|
239
304
|
// path and repeats the thrown error's message. A thrown non-Error becomes a NonErrorThrown whose
|
|
240
305
|
// message prints the value.
|
|
@@ -362,6 +427,21 @@ const cloneInput = (value, type) => {
|
|
|
362
427
|
return value;
|
|
363
428
|
};
|
|
364
429
|
|
|
430
|
+
// Each transaction attempt works on its own copy of the request input, since hooks, validators and
|
|
431
|
+
// state actions may change it. The first copy is taken now, before the transaction starts, and is
|
|
432
|
+
// handed out once; a retry copies the request input again, which no attempt receives or changes.
|
|
433
|
+
const attemptInputs = (input, type) => {
|
|
434
|
+
let first = cloneInput(input, type);
|
|
435
|
+
let taken = false;
|
|
436
|
+
return () => {
|
|
437
|
+
if (taken) return cloneInput(input, type);
|
|
438
|
+
taken = true;
|
|
439
|
+
const value = first;
|
|
440
|
+
first = undefined;
|
|
441
|
+
return value;
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
365
445
|
export const createRuntime = (adapter) => {
|
|
366
446
|
if (!adapter) {
|
|
367
447
|
throw new SimfinityError('A database adapter is required', 'ADAPTER_REQUIRED', 500);
|
|
@@ -609,6 +689,10 @@ const wrapListInputType = (itemType, listShape, preserveOuterNonNull) => {
|
|
|
609
689
|
|
|
610
690
|
const capitalize = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
611
691
|
|
|
692
|
+
// A GraphQL input object needs at least one field, so a generated input that would have none is
|
|
693
|
+
// left out, together with the operations, fields and mutations that would take it (#161).
|
|
694
|
+
const hasInputFields = (inputType) => Object.keys(inputType.getFields()).length > 0;
|
|
695
|
+
|
|
612
696
|
/**
|
|
613
697
|
* The `added` item input of a referenced collection: the item type's create input without the
|
|
614
698
|
* back-reference that the parent supplies. Its fields are read from the item's create input when
|
|
@@ -649,9 +733,11 @@ const createTypeWithExcludedField = (inputNamePrefix, itemType, fieldToExclude,
|
|
|
649
733
|
};
|
|
650
734
|
|
|
651
735
|
// A referenced collection's operation input. The item inputs are read when the schema is built, so
|
|
652
|
-
// the item type's inputs need not exist yet.
|
|
736
|
+
// the item type's inputs need not exist yet. `added` or `updated` is left out when its item input
|
|
737
|
+
// would have no fields, such as `added` for a child whose only writable field is the back-reference;
|
|
738
|
+
// `deleted` always remains. `owner` names the collection, as `Type.field`, in the warning.
|
|
653
739
|
const createOneToManyInputType = (name, inputNamePrefix, itemType, connection,
|
|
654
|
-
itemNonNull = false, optionalFields = []) => {
|
|
740
|
+
itemNonNull = false, optionalFields = [], owner = name) => {
|
|
655
741
|
const inputTypeForAdd = connection.declaredFieldName
|
|
656
742
|
? createTypeWithExcludedField(
|
|
657
743
|
inputNamePrefix,
|
|
@@ -663,17 +749,40 @@ const createOneToManyInputType = (name, inputNamePrefix, itemType, connection,
|
|
|
663
749
|
: null;
|
|
664
750
|
const listOf = (type) => new GraphQLList(itemNonNull ? new GraphQLNonNull(type) : type);
|
|
665
751
|
|
|
752
|
+
const omittedOperation = {
|
|
753
|
+
added: () => `because ${itemType.name} has no writable fields`
|
|
754
|
+
+ `${connection.declaredFieldName ? ` besides its connectionField ${connection.declaredFieldName}` : ''}; `
|
|
755
|
+
+ `create ${itemType.name} records with its own mutations or a registered mutation.`,
|
|
756
|
+
updated: () => `because ${itemType.name} has no writable fields and no writable id; update `
|
|
757
|
+
+ `${itemType.name} records with a registered mutation.`,
|
|
758
|
+
};
|
|
759
|
+
|
|
666
760
|
return new GraphQLInputObjectType({
|
|
667
761
|
name,
|
|
668
|
-
fields: () =>
|
|
669
|
-
|
|
670
|
-
|
|
762
|
+
fields: () => {
|
|
763
|
+
const fields = {};
|
|
764
|
+
const itemInputs = {
|
|
765
|
+
added: inputTypeForAdd || getInputType(itemType),
|
|
766
|
+
updated: typesDictForUpdate.types[itemType.name].inputType,
|
|
767
|
+
};
|
|
768
|
+
for (const [operation, itemInput] of Object.entries(itemInputs)) {
|
|
769
|
+
if (hasInputFields(itemInput)) {
|
|
770
|
+
fields[operation] = { type: listOf(itemInput) };
|
|
771
|
+
} else {
|
|
772
|
+
warnOnce(itemType, `emptyItemInput:${owner}:${operation}`, `Configuration issue: ${owner} has no `
|
|
773
|
+
+ `\`${operation}\` operation in generated inputs, ${omittedOperation[operation]()}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
671
776
|
// Null IDs are skipped, so `deleted` accepts them whatever the collection's item nullability.
|
|
672
|
-
deleted
|
|
673
|
-
|
|
777
|
+
fields.deleted = { type: new GraphQLList(GraphQLID) };
|
|
778
|
+
return fields;
|
|
779
|
+
},
|
|
674
780
|
});
|
|
675
781
|
};
|
|
676
782
|
|
|
783
|
+
// What graphQLListInputType returns for a list of an embedded type whose input has no fields.
|
|
784
|
+
const NO_INPUT_FIELDS = Symbol('no input fields');
|
|
785
|
+
|
|
677
786
|
const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
678
787
|
inputNamePrefix, connectionField, preserveOuterNonNull, ownerName) => {
|
|
679
788
|
const listShape = getListShape(fieldEntry.type);
|
|
@@ -686,13 +795,15 @@ const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
|
686
795
|
if (!fieldEntry.extensions?.relation?.embedded) {
|
|
687
796
|
const oneToMany = createOneToManyInputType(`OneToMany${inputNamePrefix}${fieldEntryName}`,
|
|
688
797
|
inputNamePrefix, itemType, normalizeConnectionField(itemType, connectionField),
|
|
689
|
-
listShape.itemNonNull);
|
|
798
|
+
listShape.itemNonNull, [], `${ownerName}.${fieldEntryName}`);
|
|
690
799
|
return preserveOuterNonNull && listShape.outerNonNull
|
|
691
800
|
? new GraphQLNonNull(oneToMany)
|
|
692
801
|
: oneToMany;
|
|
693
802
|
}
|
|
694
803
|
if (registration.inputType) {
|
|
695
|
-
return
|
|
804
|
+
return hasInputFields(registration.inputType)
|
|
805
|
+
? wrapListInputType(registration.inputType, listShape, preserveOuterNonNull)
|
|
806
|
+
: NO_INPUT_FIELDS;
|
|
696
807
|
}
|
|
697
808
|
} else if (itemType instanceof GraphQLScalarType || itemType instanceof GraphQLEnumType) {
|
|
698
809
|
return wrapListInputType(itemType, listShape, preserveOuterNonNull);
|
|
@@ -725,14 +836,47 @@ const selfCollectionInputType = (gqltype, fieldEntryName, fieldEntry, forUpdate,
|
|
|
725
836
|
// optional there, as such items had no such fields before; items added by an update keep them
|
|
726
837
|
// as the type declares them.
|
|
727
838
|
forUpdate ? [] : selfFields,
|
|
839
|
+
`${gqltype.name}.${fieldEntryName}`,
|
|
728
840
|
);
|
|
729
841
|
return !forUpdate && listShape.outerNonNull ? new GraphQLNonNull(oneToMany) : oneToMany;
|
|
730
842
|
};
|
|
731
843
|
|
|
844
|
+
// An embedded field, single or list, whose embedded type's create or update input has no fields is
|
|
845
|
+
// left out of that input of its owner. Only the create input is empty when the type's writable fields
|
|
846
|
+
// hold only ids, which create inputs leave out; generated updates still set the field then.
|
|
847
|
+
const warnEmptyEmbeddedInputs = (gqltype, fieldName, embeddedTypeName, noCreate, noUpdate) => {
|
|
848
|
+
if (!noCreate && !noUpdate) return;
|
|
849
|
+
const field = `Configuration issue: ${gqltype.name}.${fieldName} `;
|
|
850
|
+
if (!noUpdate) {
|
|
851
|
+
warnOnce(gqltype, `emptyEmbeddedInput:${fieldName}`, `${field}is left out of generated create inputs, because `
|
|
852
|
+
+ `the create input of its embedded type ${embeddedTypeName} would have no fields (create inputs leave out `
|
|
853
|
+
+ 'ids), so generated creates cannot set it, while generated updates still can; to set it on create, use a '
|
|
854
|
+
+ 'controller or a registered mutation.');
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const inputs = [noCreate && 'create', 'update'].filter(Boolean).join(' and ');
|
|
858
|
+
warnOnce(gqltype, `emptyEmbeddedInput:${fieldName}`, `${field}is left out of generated ${inputs} inputs, because `
|
|
859
|
+
+ `its embedded type ${embeddedTypeName} has no writable fields, so generated mutations cannot set it; set it `
|
|
860
|
+
+ 'in a controller or a registered mutation, and mark it readOnly to state that it is output-only.');
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// A non-null embedded field, single or list, that generated create inputs cannot set. Generated
|
|
864
|
+
// mutations that reach the owner's create input would store records without it, so such a schema is
|
|
865
|
+
// rejected; one whose generated mutations cannot reach that input only leaves the field out.
|
|
866
|
+
const requiredEmbeddedWithoutInputError = (gqltype, { field, embeddedTypeName }) => new SimfinityError(
|
|
867
|
+
`${gqltype.name}.${field} is non-null but its embedded type ${embeddedTypeName} has no writable fields, so `
|
|
868
|
+
+ 'generated create inputs cannot set it; mark it readOnly and set it in a controller, or make it nullable',
|
|
869
|
+
'INVALID_MODEL',
|
|
870
|
+
400,
|
|
871
|
+
);
|
|
872
|
+
// Registration -> the non-null embedded fields its create input left out.
|
|
873
|
+
const requiredFieldsWithoutCreateInput = new WeakMap();
|
|
874
|
+
|
|
732
875
|
// Builds a type's create and update inputs, or returns every field whose input cannot be built yet:
|
|
733
876
|
// `{ unresolvedFields: [{ field, waitsFor }] }`, where `waitsFor` names the embedded type whose
|
|
734
|
-
// inputs the field waits for, and is absent when the field can have no generated input.
|
|
735
|
-
|
|
877
|
+
// inputs the field waits for, and is absent when the field can have no generated input. `reachable`
|
|
878
|
+
// holds the types whose inputs the generated mutations of the schema reach.
|
|
879
|
+
const buildInputType = (gqltype, reachable) => {
|
|
736
880
|
const argTypes = gqltype.getFields();
|
|
737
881
|
|
|
738
882
|
const fieldsArgs = {};
|
|
@@ -740,6 +884,13 @@ const buildInputType = (gqltype) => {
|
|
|
740
884
|
|
|
741
885
|
const selfReferenceCollections = {};
|
|
742
886
|
const unresolvedFields = [];
|
|
887
|
+
const requiredWithoutCreateInput = [];
|
|
888
|
+
// Leaves a non-null embedded field without a create input out, or rejects the schema.
|
|
889
|
+
const omitRequiredEmbedded = (field, embeddedTypeName) => {
|
|
890
|
+
const omitted = { field, embeddedTypeName };
|
|
891
|
+
if (reachable.has(gqltype)) throw requiredEmbeddedWithoutInputError(gqltype, omitted);
|
|
892
|
+
requiredWithoutCreateInput.push(omitted);
|
|
893
|
+
};
|
|
743
894
|
|
|
744
895
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
745
896
|
const fieldArg = {};
|
|
@@ -774,8 +925,17 @@ const buildInputType = (gqltype) => {
|
|
|
774
925
|
} else if (requireRegisteredType(typesDict, unwrapNonNull(fieldEntry.type), fieldEntryName,
|
|
775
926
|
gqltype.name).inputType
|
|
776
927
|
&& typesDictForUpdate.types[fieldEntryNameValue].inputType) {
|
|
777
|
-
|
|
778
|
-
|
|
928
|
+
// Create and update inputs are decided apart: an embedded type whose only writable
|
|
929
|
+
// field is `id` has one only for updates.
|
|
930
|
+
const embeddedCreate = typesDict.types[fieldEntryNameValue].inputType;
|
|
931
|
+
const embeddedUpdate = typesDictForUpdate.types[fieldEntryNameValue].inputType;
|
|
932
|
+
if (hasInputFields(embeddedCreate)) fieldArg.type = embeddedCreate;
|
|
933
|
+
else if (fieldEntry.type instanceof GraphQLNonNull) {
|
|
934
|
+
omitRequiredEmbedded(fieldEntryName, fieldEntryNameValue);
|
|
935
|
+
}
|
|
936
|
+
if (hasInputFields(embeddedUpdate)) fieldArgForUpdate.type = embeddedUpdate;
|
|
937
|
+
warnEmptyEmbeddedInputs(gqltype, fieldEntryName, fieldEntryNameValue,
|
|
938
|
+
!fieldArg.type, !fieldArgForUpdate.type);
|
|
779
939
|
} else {
|
|
780
940
|
unresolvedFields.push({ field: fieldEntryName, waitsFor: fieldEntryNameValue });
|
|
781
941
|
}
|
|
@@ -802,8 +962,11 @@ const buildInputType = (gqltype) => {
|
|
|
802
962
|
fieldEntryName, `${gqltype.name}U`,
|
|
803
963
|
fieldEntry.extensions?.relation?.connectionField, false, gqltype.name);
|
|
804
964
|
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
805
|
-
fieldArg.type = listInputTypeForAdd;
|
|
806
|
-
|
|
965
|
+
if (listInputTypeForAdd !== NO_INPUT_FIELDS) fieldArg.type = listInputTypeForAdd;
|
|
966
|
+
else if (listShape.outerNonNull) omitRequiredEmbedded(fieldEntryName, listShape.itemType.name);
|
|
967
|
+
if (listInputTypeForUpdate !== NO_INPUT_FIELDS) fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
968
|
+
warnEmptyEmbeddedInputs(gqltype, fieldEntryName, listShape.itemType.name,
|
|
969
|
+
listInputTypeForAdd === NO_INPUT_FIELDS, listInputTypeForUpdate === NO_INPUT_FIELDS);
|
|
807
970
|
} else {
|
|
808
971
|
// An object item here is embedded and its inputs are not built yet; any other item,
|
|
809
972
|
// such as a list, has no generated input.
|
|
@@ -858,7 +1021,9 @@ const buildInputType = (gqltype) => {
|
|
|
858
1021
|
fields: fieldsArgForUpdate,
|
|
859
1022
|
});
|
|
860
1023
|
|
|
861
|
-
return {
|
|
1024
|
+
return {
|
|
1025
|
+
inputTypeBody: inputTypeForAdd, inputTypeBodyForUpdate: inputTypeForUpdate, requiredWithoutCreateInput,
|
|
1026
|
+
};
|
|
862
1027
|
};
|
|
863
1028
|
|
|
864
1029
|
const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
@@ -964,6 +1129,14 @@ const unresolvedInputTypesMessage = (pendingTypeNames, unresolvedByType) => {
|
|
|
964
1129
|
|
|
965
1130
|
const buildPendingInputTypes = (waitingForInputType, includedMutationTypes) => {
|
|
966
1131
|
claimSelfCollectionNames(waitingForInputType, includedMutationTypes);
|
|
1132
|
+
// Inputs built for an earlier schema, which did not reach them, may leave out a non-null embedded
|
|
1133
|
+
// field that this schema's generated mutations would need; inputs built now are checked as built.
|
|
1134
|
+
const reachable = typesWithReachableInputs(includedMutationTypes);
|
|
1135
|
+
for (const gqltype of reachable) {
|
|
1136
|
+
const registration = typesDict.types[gqltype.name];
|
|
1137
|
+
const [omitted] = (registration.inputType && requiredFieldsWithoutCreateInput.get(registration)) || [];
|
|
1138
|
+
if (omitted) throw requiredEmbeddedWithoutInputError(gqltype, omitted);
|
|
1139
|
+
}
|
|
967
1140
|
let pending = waitingForInputType;
|
|
968
1141
|
let previousPendingCount = Object.keys(pending).length + 1;
|
|
969
1142
|
// Type name -> the fields whose input could not be built in the last pass.
|
|
@@ -983,10 +1156,11 @@ const buildPendingInputTypes = (waitingForInputType, includedMutationTypes) => {
|
|
|
983
1156
|
const { gqltype } = value;
|
|
984
1157
|
if (typesDict.types[gqltype.name].inputType) continue;
|
|
985
1158
|
|
|
986
|
-
const result = buildInputType(gqltype);
|
|
1159
|
+
const result = buildInputType(gqltype, reachable);
|
|
987
1160
|
if (result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
988
1161
|
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
989
1162
|
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
1163
|
+
requiredFieldsWithoutCreateInput.set(typesDict.types[gqltype.name], result.requiredWithoutCreateInput);
|
|
990
1164
|
} else {
|
|
991
1165
|
stillWaiting[key] = value;
|
|
992
1166
|
unresolvedFields[gqltype.name] = result.unresolvedFields;
|
|
@@ -1087,10 +1261,10 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
1087
1261
|
};
|
|
1088
1262
|
|
|
1089
1263
|
const executeRegisteredMutation = (args, callback, context, session, inputModel) => {
|
|
1090
|
-
const
|
|
1264
|
+
const nextInput = attemptInputs(args, inputModel);
|
|
1091
1265
|
return adapter.withTransaction(
|
|
1092
1266
|
session,
|
|
1093
|
-
(mySession) => callback(
|
|
1267
|
+
(mySession) => callback(nextInput(), mySession, context),
|
|
1094
1268
|
);
|
|
1095
1269
|
};
|
|
1096
1270
|
|
|
@@ -1111,10 +1285,23 @@ const iterateOnCollectionFields = async (materializedModel, gqltype, objectId, s
|
|
|
1111
1285
|
}
|
|
1112
1286
|
};
|
|
1113
1287
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1288
|
+
// Whether the runtime may skip a read by ID whose result it does not use: the adapter reports that
|
|
1289
|
+
// such a read of the model has no side effects, such as query middleware, and fails only where the
|
|
1290
|
+
// update or delete by that ID fails too. Without the report, those reads run as before.
|
|
1291
|
+
const readByIdIsInert = (Model) => typeof adapter.readByIdIsInert === 'function'
|
|
1292
|
+
&& adapter.readByIdIsInert(Model) === true;
|
|
1116
1293
|
|
|
1117
|
-
|
|
1294
|
+
// The record is read for the onDelete hook; adapter.delete casts the ID, deletes and returns the
|
|
1295
|
+
// record itself. Without a hook, the read is skipped when it is inert. A nested delete passes the
|
|
1296
|
+
// child it has already read as `current`.
|
|
1297
|
+
const onDelete = async (Model, controller, id, session, context, current) => {
|
|
1298
|
+
const hasHook = !!(controller && controller.onDelete);
|
|
1299
|
+
let currentObject = current;
|
|
1300
|
+
if (currentObject === undefined && (hasHook || !readByIdIsInert(Model))) {
|
|
1301
|
+
currentObject = await adapter.getById(Model, id, session, { plain: true });
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (hasHook) {
|
|
1118
1305
|
await controller.onDelete(currentObject, session, context);
|
|
1119
1306
|
}
|
|
1120
1307
|
|
|
@@ -1187,13 +1374,35 @@ const clearNullEmbeddedMembers = (merged, fieldEntry, patch) => {
|
|
|
1187
1374
|
}
|
|
1188
1375
|
};
|
|
1189
1376
|
|
|
1190
|
-
|
|
1377
|
+
// Whether an update reads the stored embedded values of a type with embedded fields. They are used
|
|
1378
|
+
// to merge a singular embedded object, and a non-empty embedded list is completed only for an
|
|
1379
|
+
// existing record; empty lists and nulls use nothing. A top-level update also keeps the read before
|
|
1380
|
+
// onUpdating, so that a malformed ID fails before the hook, and before nested collection items, so
|
|
1381
|
+
// that the SQL adapter locks the parent row before child writes; otherwise it skips the read only
|
|
1382
|
+
// when it is inert. A state action or a nested update has already read and checked the record
|
|
1383
|
+
// (`recordChecked`).
|
|
1384
|
+
const readsStoredEmbedded = (Model, controller, materializedModel, embeddedFieldNames, recordChecked) => {
|
|
1385
|
+
const usesStored = embeddedFieldNames.some((name) => {
|
|
1386
|
+
const value = ownValue(materializedModel.modelArgs, name);
|
|
1387
|
+
return value && (!Array.isArray(value) || value.length > 0);
|
|
1388
|
+
});
|
|
1389
|
+
if (usesStored) return true;
|
|
1390
|
+
if (recordChecked) return false;
|
|
1391
|
+
return !!(controller && controller.onUpdating)
|
|
1392
|
+
|| Object.values(materializedModel.collectionFields).some((field) => field
|
|
1393
|
+
&& ['added', 'updated', 'deleted'].some((key) => field[key] && hasNestedItems(field[key])))
|
|
1394
|
+
|| !readByIdIsInert(Model);
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context,
|
|
1398
|
+
recordChecked = false) => {
|
|
1191
1399
|
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
|
|
1192
1400
|
const objectId = args.id;
|
|
1193
1401
|
const argTypes = gqltype.getFields();
|
|
1194
1402
|
const embeddedFieldNames = getEmbeddedFieldNames(gqltype);
|
|
1195
1403
|
|
|
1196
|
-
if (embeddedFieldNames.length > 0
|
|
1404
|
+
if (embeddedFieldNames.length > 0
|
|
1405
|
+
&& readsStoredEmbedded(Model, controller, materializedModel, embeddedFieldNames, recordChecked)) {
|
|
1197
1406
|
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
1198
1407
|
const currentObject = await adapter.getById(Model, objectId, session, {
|
|
1199
1408
|
projection,
|
|
@@ -1206,7 +1415,6 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
1206
1415
|
const newObjectData = ownValue(materializedModel.modelArgs, fieldEntryName);
|
|
1207
1416
|
if (newObjectData) {
|
|
1208
1417
|
if (Array.isArray(newObjectData)) {
|
|
1209
|
-
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
1210
1418
|
completeEmbeddedValue(fieldEntryName, argTypes[fieldEntryName], newObjectData);
|
|
1211
1419
|
} else {
|
|
1212
1420
|
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
@@ -1268,7 +1476,7 @@ const onStateChanged = async (Model, gqltype, controller, args, session, actionF
|
|
|
1268
1476
|
}
|
|
1269
1477
|
|
|
1270
1478
|
args.state = stateValue(actionField.to);
|
|
1271
|
-
let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context);
|
|
1479
|
+
let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context, true);
|
|
1272
1480
|
result = adapter.toObject(result);
|
|
1273
1481
|
result.state = actionField.to.value;
|
|
1274
1482
|
return result;
|
|
@@ -1384,13 +1592,11 @@ const executeOperation = (Model, gqltype, controller, args, operation, actionFie
|
|
|
1384
1592
|
const limit = getMaxNestedOperations();
|
|
1385
1593
|
if (limit !== null && operation !== operations.DELETE) assertNestedOperations(gqltype, args, limit);
|
|
1386
1594
|
if (operation !== operations.DELETE) assertCollectionWrites(gqltype, args);
|
|
1387
|
-
const
|
|
1595
|
+
const nextInput = operation === operations.DELETE ? () => args : attemptInputs(args, inputType);
|
|
1388
1596
|
return adapter.withTransaction(
|
|
1389
1597
|
session,
|
|
1390
1598
|
async (mySession) => {
|
|
1391
|
-
const attemptArgs =
|
|
1392
|
-
? inputSnapshot
|
|
1393
|
-
: cloneInput(inputSnapshot, inputType);
|
|
1599
|
+
const attemptArgs = nextInput();
|
|
1394
1600
|
switch (operation) {
|
|
1395
1601
|
case operations.SAVE:
|
|
1396
1602
|
return onSaveObject(Model, gqltype, controller, attemptArgs, mySession, null, context);
|
|
@@ -1435,9 +1641,12 @@ const executeItemFunction = async (gqltype, collectionField, parentKey, session,
|
|
|
1435
1641
|
const args = operationType === operations.DELETE ? { id: element } : { input: element };
|
|
1436
1642
|
await executeMiddleware({ type, args, operation: operationType, context });
|
|
1437
1643
|
|
|
1644
|
+
// The ownership check reads the child (the SQL adapter locks its row). onDelete receives this
|
|
1645
|
+
// record, and an update reads the child again only to merge embedded values.
|
|
1646
|
+
let child;
|
|
1438
1647
|
if (operationType === operations.UPDATE || operationType === operations.DELETE) {
|
|
1439
1648
|
const id = operationType === operations.DELETE ? args.id : args.input.id;
|
|
1440
|
-
|
|
1649
|
+
child = await adapter.getById(type.model, id, session, { plain: true, lock: true });
|
|
1441
1650
|
if (!child) {
|
|
1442
1651
|
throw new SimfinityError(`${collectionGQLType.name} ${id} is not valid`, 'NOT_VALID_ID', 404);
|
|
1443
1652
|
}
|
|
@@ -1453,10 +1662,10 @@ const executeItemFunction = async (gqltype, collectionField, parentKey, session,
|
|
|
1453
1662
|
break;
|
|
1454
1663
|
case operations.UPDATE:
|
|
1455
1664
|
await onUpdateSubject(type.model, collectionGQLType, type.controller,
|
|
1456
|
-
args.input, session, linkToParent, context);
|
|
1665
|
+
args.input, session, linkToParent, context, true);
|
|
1457
1666
|
break;
|
|
1458
1667
|
case operations.DELETE:
|
|
1459
|
-
await onDelete(type.model, type.controller, args.id, session, context);
|
|
1668
|
+
await onDelete(type.model, type.controller, args.id, session, context, child);
|
|
1460
1669
|
}
|
|
1461
1670
|
}
|
|
1462
1671
|
};
|
|
@@ -1707,9 +1916,22 @@ const resolveById = async (type, args, context, requiredId) => {
|
|
|
1707
1916
|
return results[0] || null;
|
|
1708
1917
|
};
|
|
1709
1918
|
|
|
1919
|
+
// Why a type's generated create or update input has no fields, for the warnings of buildMutation. An
|
|
1920
|
+
// update input also needs a writable id.
|
|
1921
|
+
const withoutWritableFields = (type, { noWritableId = false } = {}) => {
|
|
1922
|
+
const stateField = type.gqltype.getFields().state;
|
|
1923
|
+
const managedState = type.stateMachine && stateField && !stateField.extensions?.readOnly;
|
|
1924
|
+
const state = managedState ? ' besides its state, which its state machine manages' : '';
|
|
1925
|
+
const id = noWritableId ? `${state ? ',' : ''} and no writable id` : '';
|
|
1926
|
+
return `${type.gqltype.name} has no writable fields${state}${id}`;
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1710
1929
|
// Every generated mutation field carries extensions.simfinityMutation with its operation (and, for
|
|
1711
1930
|
// a state-machine action, the action and its from/to states), so tools such as the MCP package can
|
|
1712
1931
|
// classify it without guessing from its name. Extensions do not appear in SDL or introspection.
|
|
1932
|
+
// An input object needs a field, so a type whose create input has none gets no add mutation, and
|
|
1933
|
+
// one whose update input has none, which only a type without a writable id can have, gets no update
|
|
1934
|
+
// mutation or state machine actions either (#161).
|
|
1713
1935
|
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
1714
1936
|
const rootQueryArgs = {};
|
|
1715
1937
|
rootQueryArgs.name = name;
|
|
@@ -1722,24 +1944,33 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1722
1944
|
if (type.endpoint) {
|
|
1723
1945
|
const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
|
|
1724
1946
|
|
|
1725
|
-
|
|
1726
|
-
type
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1947
|
+
if (hasInputFields(type.inputType)) {
|
|
1948
|
+
rootQueryArgs.fields[`add${type.simpleEntityEndpointName}`] = {
|
|
1949
|
+
type: type.gqltype,
|
|
1950
|
+
description: 'add',
|
|
1951
|
+
args: argsObject,
|
|
1952
|
+
extensions: { simfinityMutation: { typeName: type.gqltype.name, operation: operations.SAVE } },
|
|
1953
|
+
async resolve(parent, args, context) {
|
|
1954
|
+
const params = {
|
|
1955
|
+
type,
|
|
1956
|
+
args,
|
|
1957
|
+
operation: operations.SAVE,
|
|
1958
|
+
context,
|
|
1959
|
+
};
|
|
1737
1960
|
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1961
|
+
await executeMiddleware(params);
|
|
1962
|
+
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1963
|
+
args.input, operations.SAVE, null, null, context, type.inputType);
|
|
1964
|
+
},
|
|
1965
|
+
};
|
|
1966
|
+
} else {
|
|
1967
|
+
warnOnce(type.gqltype, 'emptyCreateInput', `Configuration issue: ${withoutWritableFields(type)}, so the `
|
|
1968
|
+
+ `schema has no add${type.simpleEntityEndpointName} mutation; create its records with a registered `
|
|
1969
|
+
+ (type.stateMachine
|
|
1970
|
+
? 'mutation, which must set the initial state (saveObject() sets it).'
|
|
1971
|
+
: `mutation that takes no input or an input of its own, since getInputType(${type.gqltype.name}) `
|
|
1972
|
+
+ 'has no fields.'));
|
|
1973
|
+
}
|
|
1743
1974
|
rootQueryArgs.fields[`delete${type.simpleEntityEndpointName}`] = {
|
|
1744
1975
|
type: type.gqltype,
|
|
1745
1976
|
description: 'delete',
|
|
@@ -1764,7 +1995,12 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1764
1995
|
|
|
1765
1996
|
for (const type of Object.values(typesDictForUpdate.types)) {
|
|
1766
1997
|
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
1767
|
-
if (type.endpoint) {
|
|
1998
|
+
if (type.endpoint && !hasInputFields(type.inputType)) {
|
|
1999
|
+
const reason = withoutWritableFields(type, { noWritableId: true });
|
|
2000
|
+
warnOnce(type.gqltype, 'emptyUpdateInput', `Configuration issue: ${reason}, so its generated update input `
|
|
2001
|
+
+ `would have no fields and the schema has no update${type.simpleEntityEndpointName} mutation`
|
|
2002
|
+
+ `${type.stateMachine ? ' and no state machine actions' : ''}.`);
|
|
2003
|
+
} else if (type.endpoint) {
|
|
1768
2004
|
const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
|
|
1769
2005
|
rootQueryArgs.fields[`update${type.simpleEntityEndpointName}`] = {
|
|
1770
2006
|
type: type.gqltype,
|
|
@@ -1967,6 +2203,102 @@ const installIdResolver = (gqltype) => {
|
|
|
1967
2203
|
if (idField && !idField.resolve) idField.resolve = markGenerated((parent) => parent._id ?? parent.id);
|
|
1968
2204
|
};
|
|
1969
2205
|
|
|
2206
|
+
// A rendered embedded value that may hold no data: a plain object, or a hydrated record, which
|
|
2207
|
+
// converts itself with toObject() (MongoDB documents and their nested paths do). Promises, arrays,
|
|
2208
|
+
// functions and other objects, such as dates or identifiers, are values.
|
|
2209
|
+
const isRecordObject = (value) => {
|
|
2210
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || isThenable(value)) return false;
|
|
2211
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2212
|
+
return prototype === Object.prototype || prototype === null || typeof value.toObject === 'function';
|
|
2213
|
+
};
|
|
2214
|
+
const isPlainObject = (value) => {
|
|
2215
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2216
|
+
return prototype === Object.prototype || prototype === null;
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
// The members an embedded value stores. The rule below reads them instead of the value, so no getter
|
|
2220
|
+
// or virtual of the application runs, whatever the selection. An adapter that hydrates records
|
|
2221
|
+
// returns their stored data through rawEmbeddedValue (MongoDB: a nested path's raw value, a
|
|
2222
|
+
// subdocument's _doc); an absent stored value has no members. A plain object is read as it is.
|
|
2223
|
+
// Returns null for anything else, such as a record the adapter does not convert, which counts as data.
|
|
2224
|
+
const storedMembersOf = (value) => {
|
|
2225
|
+
if (!isRecordObject(value)) return null;
|
|
2226
|
+
const stored = typeof adapter.rawEmbeddedValue === 'function' ? adapter.rawEmbeddedValue(value) : value;
|
|
2227
|
+
if (stored === null || stored === undefined) return {};
|
|
2228
|
+
return typeof stored === 'object' && !Array.isArray(stored) && !isThenable(stored) && isPlainObject(stored)
|
|
2229
|
+
? stored : null;
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
// A stored member: an own data property, read without calling accessors. An accessor is present,
|
|
2233
|
+
// whatever it would return.
|
|
2234
|
+
const PRESENT = Symbol('present');
|
|
2235
|
+
const ownStoredValue = (stored, key) => {
|
|
2236
|
+
const descriptor = Object.getOwnPropertyDescriptor(stored, key);
|
|
2237
|
+
if (!descriptor) return undefined;
|
|
2238
|
+
if (!('value' in descriptor)) return PRESENT;
|
|
2239
|
+
return isObjectMember(key, descriptor.value) ? undefined : descriptor.value;
|
|
2240
|
+
};
|
|
2241
|
+
// A referenced collection stores nothing in the object. `id` reads the stored identity, `_id` before
|
|
2242
|
+
// `id`, as the generated entity resolver (installIdResolver) and Mongoose's id virtual do. Every other
|
|
2243
|
+
// member is stored under its storage name.
|
|
2244
|
+
const isReferencedCollection = (member) => {
|
|
2245
|
+
const relation = member.extensions?.relation;
|
|
2246
|
+
return Boolean(relation && !relation.embedded && getListShape(member.type));
|
|
2247
|
+
};
|
|
2248
|
+
const storedMemberValue = (stored, memberName, member) => {
|
|
2249
|
+
if (isReferencedCollection(member)) return undefined;
|
|
2250
|
+
const identity = memberName === 'id' ? ownStoredValue(stored, '_id') : undefined;
|
|
2251
|
+
return identity ?? ownStoredValue(stored, getFieldStorageName(memberName, member));
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
// Whether stored members hold no data: every member is null or undefined, an empty list, or a
|
|
2255
|
+
// singular embedded object that holds no data. Any other value, a promise or an accessor included,
|
|
2256
|
+
// is data. An object that contains itself, which only a custom result can, holds data.
|
|
2257
|
+
const holdsNoData = (stored, type, ancestors = []) => !ancestors.includes(stored)
|
|
2258
|
+
&& Object.entries(type.getFields()).every(([memberName, member]) => {
|
|
2259
|
+
const value = storedMemberValue(stored, memberName, member);
|
|
2260
|
+
if (value === null || value === undefined) return true;
|
|
2261
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
2262
|
+
const memberType = unwrapNonNull(member.type);
|
|
2263
|
+
if (member.extensions?.relation?.embedded !== true || !(memberType instanceof GraphQLObjectType)) return false;
|
|
2264
|
+
const nested = storedMembersOf(value);
|
|
2265
|
+
return nested !== null && holdsNoData(nested, memberType, [...ancestors, stored]);
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
// The members that count: non-null members that generated inputs accept. As on writes
|
|
2269
|
+
// (completeEmbeddedValue), `id` and readOnly members do not; nor do interface and union members,
|
|
2270
|
+
// which buildInputType leaves out; nor do referenced collections or members with an application
|
|
2271
|
+
// resolver, which may compute a value.
|
|
2272
|
+
const countsAsRequired = (type, memberName, member) => member.type instanceof GraphQLNonNull
|
|
2273
|
+
&& !isReferencedCollection(member) && memberName !== 'id' && !member.extensions?.readOnly
|
|
2274
|
+
&& !isAbstractOutputType(unwrapListAndNonNull(member.type)) && !hasApplicationResolver(type, memberName);
|
|
2275
|
+
const hasRequiredMember = (type) => Object.entries(type.getFields())
|
|
2276
|
+
.some(([memberName, member]) => countsAsRequired(type, memberName, member));
|
|
2277
|
+
|
|
2278
|
+
// Whether rendering such an object fails: a member that counts is null or undefined, or is a singular
|
|
2279
|
+
// embedded object that misses one of its own. A promise or an accessor counts as present.
|
|
2280
|
+
const missesRequiredMember = (stored, type) => Object.entries(type.getFields()).some(([memberName, member]) => {
|
|
2281
|
+
if (!countsAsRequired(type, memberName, member)) return false;
|
|
2282
|
+
const value = storedMemberValue(stored, memberName, member);
|
|
2283
|
+
if (value === null || value === undefined) return true;
|
|
2284
|
+
if (member.extensions?.relation?.embedded !== true || !(member.type.ofType instanceof GraphQLObjectType)) return false;
|
|
2285
|
+
const nested = storedMembersOf(value);
|
|
2286
|
+
return nested !== null && missesRequiredMember(nested, member.type.ofType);
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
// A type without a member that counts never reads as null, so its values are returned untouched,
|
|
2290
|
+
// with no member read. Reading stored members never changes the result otherwise: a value whose
|
|
2291
|
+
// members cannot be read, because an adapter hook or a property read throws, is returned as it is.
|
|
2292
|
+
const readEmbeddedObject = (value, type) => {
|
|
2293
|
+
if (!(type instanceof GraphQLObjectType) || !hasRequiredMember(type)) return value;
|
|
2294
|
+
try {
|
|
2295
|
+
const stored = storedMembersOf(value);
|
|
2296
|
+
return stored !== null && holdsNoData(stored, type) && missesRequiredMember(stored, type) ? null : value;
|
|
2297
|
+
} catch {
|
|
2298
|
+
return value;
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
|
|
1970
2302
|
// An adapter that hydrates stored records may render an explicitly null singular embedded object as
|
|
1971
2303
|
// an object (MongoDB nested paths do); readEmbeddedValue returns null for it. Nullable singular
|
|
1972
2304
|
// embedded fields of registered types, and of the embedded types they reach, read through it. The
|
|
@@ -1975,11 +2307,24 @@ const installIdResolver = (gqltype) => {
|
|
|
1975
2307
|
// The value is read as graphql's default resolver reads it, so a method on the parent, such as one a
|
|
1976
2308
|
// custom mutation result supplies, is still called; a promise it returns is read once it settles.
|
|
1977
2309
|
// Only a field named like an Object.prototype member that reads as that member reads as null.
|
|
2310
|
+
// Such an adapter may also render an absent object, or store an omitted one, as an object that holds
|
|
2311
|
+
// nothing but its list defaults (MongoDB nested paths do). An object that holds no data and misses a
|
|
2312
|
+
// required member (see missesRequiredMember) cannot be rendered, so it reads as null, as an absent
|
|
2313
|
+
// object does on other read paths, whatever the selection. The rule reads the stored members
|
|
2314
|
+
// (storedMembersOf), never running a getter or virtual, and depends only on the value and its type,
|
|
2315
|
+
// so runtimes that share the type, with or without the hook, read the same. An object a
|
|
2316
|
+
// generated create or update writes does not read as null, since those writes require the members
|
|
2317
|
+
// counted, with one exception: Mongoose minimizes empty objects when it saves a new document, so a
|
|
2318
|
+
// create that writes an empty object for a required embedded member (`geo: {}`) stores that member
|
|
2319
|
+
// as absent. An object that holds nothing else then reads as null, as an omitted one does, except
|
|
2320
|
+
// where Mongoose hydrates the document (by-ID and reference reads, update responses) and the member is
|
|
2321
|
+
// a nested path, which it renders as `{}`. Generated models declare every embedded member as a nested
|
|
2322
|
+
// path except one named `type`, a single nested subdocument. PostgreSQL rejects such a create
|
|
2323
|
+
// (REQUIRED_VALUE).
|
|
1978
2324
|
const readEmbeddedField = markGenerated((parent, args, context, info) => {
|
|
1979
2325
|
const value = readOwnField(parent, args, context, info);
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
: adapter.readEmbeddedValue(value);
|
|
2326
|
+
const read = (resolved) => readEmbeddedObject(adapter.readEmbeddedValue(resolved), getNamedType(info.returnType));
|
|
2327
|
+
return isThenable(value) ? value.then(read) : read(value);
|
|
1983
2328
|
});
|
|
1984
2329
|
|
|
1985
2330
|
const installEmbeddedValueResolvers = (gqltype, visited) => {
|
|
@@ -2042,6 +2387,20 @@ const assertCollectionConnections = () => {
|
|
|
2042
2387
|
|
|
2043
2388
|
const getRegistrations = () => Object.values(typesDict.types);
|
|
2044
2389
|
|
|
2390
|
+
// Application types that a schema with these allowlists contains, whatever else it reaches: the
|
|
2391
|
+
// endpoint types of its queries and mutations, and the input and output types of its registered
|
|
2392
|
+
// mutations.
|
|
2393
|
+
const includedApplicationTypes = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => [
|
|
2394
|
+
...Object.values(typesDict.types)
|
|
2395
|
+
.filter((type) => type.endpoint
|
|
2396
|
+
&& ((type.simpleEntityEndpointName && !shouldNotBeIncludedInSchema(includedQueryTypes, type.gqltype))
|
|
2397
|
+
|| !shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)))
|
|
2398
|
+
.map((type) => type.gqltype),
|
|
2399
|
+
...Object.entries(registeredMutations)
|
|
2400
|
+
.filter(([entry]) => !shouldNotBeIncludedInSchema(includedCustomMutations, entry))
|
|
2401
|
+
.flatMap(([, { inputModel, outputModel }]) => [inputModel, outputModel].filter(Boolean)),
|
|
2402
|
+
];
|
|
2403
|
+
|
|
2045
2404
|
const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => {
|
|
2046
2405
|
// Reject before models, resolvers or roots are built, so a rejected schema changes nothing.
|
|
2047
2406
|
Object.values(typesDict.types).forEach(({ gqltype }) => validateScope(gqltype));
|
|
@@ -2049,6 +2408,11 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
|
|
|
2049
2408
|
schemaTypes.forEach(assertNotBoundElsewhere);
|
|
2050
2409
|
schemaTypes.forEach(assertFieldsNotBoundElsewhere);
|
|
2051
2410
|
assertCollectionConnections();
|
|
2411
|
+
// An application type with the name of a field metadata type that an earlier schema fixed; one that
|
|
2412
|
+
// only generated types or the fields of an input object reach is found when the schema is
|
|
2413
|
+
// constructed.
|
|
2414
|
+
assertFieldMetadataNamesFree(includedApplicationTypes(includedQueryTypes, includedMutationTypes,
|
|
2415
|
+
includedCustomMutations));
|
|
2052
2416
|
const referencedTypes = markReferencedTypesForModelGeneration();
|
|
2053
2417
|
// Entities, which keep their own stored identity: connect() types and relation targets.
|
|
2054
2418
|
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
@@ -2110,7 +2474,7 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
|
|
|
2110
2474
|
query = buildRootQuery('RootQueryType', includedQueryTypes, aggregationResultTypeFor(applicationJSON));
|
|
2111
2475
|
}
|
|
2112
2476
|
|
|
2113
|
-
const schema =
|
|
2477
|
+
const schema = constructSchema(query, mutation);
|
|
2114
2478
|
// Reserve reachable types whose relation fields still have no resolver, such as an unregistered
|
|
2115
2479
|
// custom mutation result, so another runtime cannot later generate resolvers this schema executes.
|
|
2116
2480
|
schemaTypes.forEach((type) => {
|
package/types/auth/errors.d.ts
CHANGED
|
@@ -4,12 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { auth } from '../index.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
export type UnauthenticatedError = InstanceType<typeof auth.UnauthenticatedError>;
|
|
10
|
-
/** Authorization error: code `FORBIDDEN`, status 403. */
|
|
11
|
-
export const ForbiddenError: typeof auth.ForbiddenError;
|
|
12
|
-
export type ForbiddenError = InstanceType<typeof auth.ForbiddenError>;
|
|
7
|
+
// The root classes themselves, which `auth.UnauthenticatedError` and `auth.ForbiddenError` also are.
|
|
8
|
+
export { UnauthenticatedError, ForbiddenError } from '../index.js';
|
|
13
9
|
export const createAuthError: typeof auth.createAuthError;
|
|
14
10
|
|
|
15
11
|
declare const errors: Pick<typeof auth, 'UnauthenticatedError' | 'ForbiddenError' | 'createAuthError'>;
|
package/types/index.d.ts
CHANGED
|
@@ -227,11 +227,30 @@ export interface Runtime<Model = any, Session = any> {
|
|
|
227
227
|
* reserves reachable types that still have a
|
|
228
228
|
* non-embedded relation field without a resolver, such as an unregistered custom mutation result.
|
|
229
229
|
* Every entity type resolves `id` to `_id ?? id`, whatever the allowlists include.
|
|
230
|
+
*
|
|
231
|
+
* A generated input that would have no fields is left out, with a `Configuration issue`
|
|
232
|
+
* warning: the `added` or `updated` operation of a collection whose child has no writable fields
|
|
233
|
+
* for it, an embedded field whose embedded type has none, `add<type>` for a type without
|
|
234
|
+
* writable fields, and `update<type>` and its state actions for a type without writable fields
|
|
235
|
+
* and without a writable `id`. A non-null embedded field whose embedded type has no writable
|
|
236
|
+
* fields throws `INVALID_MODEL` (400) while inputs are built when the generated mutations reach
|
|
237
|
+
* its owner's create input. When the process's first Simfinity schema has an application type
|
|
238
|
+
* named `FieldExtensionsType` or `RelationType`, Simfinity renames its `__Field.extensions`
|
|
239
|
+
* metadata types `SimfinityFieldExtensionsType` and `SimfinityRelationType` (numbered when
|
|
240
|
+
* taken) for every later schema and logs a warning; once those names are fixed, a type with one
|
|
241
|
+
* of them throws `RESERVED_TYPE_NAME` (500), before `adapter.prepare()` when it is an included
|
|
242
|
+
* endpoint type, a registered mutation's input or output type, an output type they reach or a
|
|
243
|
+
* field argument's type, and when the schema is constructed otherwise, such as for a type inside
|
|
244
|
+
* an input object's fields. The check before `adapter.prepare()` never reads an input object's
|
|
245
|
+
* fields, so a registered input's `fields` function may call `getInputType()`.
|
|
230
246
|
*/
|
|
231
247
|
createSchema(includedQueryTypes?: GraphQLObjectType[] | null, includedMutationTypes?: GraphQLObjectType[] | null, includedCustomMutations?: string[] | null): GraphQLSchema;
|
|
232
248
|
getModel(type: GraphQLObjectType | { name: string }): Model | null | undefined;
|
|
233
249
|
getType(name: string | { name: string }): GraphQLObjectType | null | undefined;
|
|
234
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* Available after createSchema has built input types. It has no fields for a type without
|
|
252
|
+
* writable fields; such inputs are not part of generated schemas.
|
|
253
|
+
*/
|
|
235
254
|
getInputType(type: GraphQLObjectType | { name: string }): GraphQLInputObjectType | undefined;
|
|
236
255
|
getRegistrations(): RuntimeRegistration<Model, Session>[];
|
|
237
256
|
/**
|
|
@@ -276,11 +295,44 @@ export interface DatabaseAdapter<Model = any, Session = any> {
|
|
|
276
295
|
* record renders as an object although the stored value is an explicit null; return any other
|
|
277
296
|
* value unchanged. It must depend only on the value: these resolvers read no data and do not bind
|
|
278
297
|
* their types, so a runtime that reaches a shared type may read through another runtime's hook.
|
|
298
|
+
* Simfinity then reads as null an object that holds no data when a non-null member would read as
|
|
299
|
+
* null, whatever the selection. These members do not count: `id`, readOnly members, interface and
|
|
300
|
+
* union members, members with their own resolver and referenced collections. An object whose type
|
|
301
|
+
* has no member that counts is returned untouched. Otherwise Simfinity reads the stored members
|
|
302
|
+
* (see `rawEmbeddedValue`), never calling a getter, and counts a stored `_id` as `id`; an error
|
|
303
|
+
* while reading them returns the object unchanged. Runtimes that share the type read through the
|
|
304
|
+
* same resolver, with or without the hook. On MongoDB, a create that writes an empty object for a
|
|
305
|
+
* required embedded member stores it as absent (Mongoose minimizes it), so an object that holds
|
|
306
|
+
* nothing else reads as null too, except in by-ID and reference reads and update responses where
|
|
307
|
+
* the member is a nested path, as generated models declare every embedded member except one named
|
|
308
|
+
* `type`.
|
|
279
309
|
*/
|
|
280
310
|
readEmbeddedValue?(value: any): any;
|
|
311
|
+
/**
|
|
312
|
+
* Optional, used with `readEmbeddedValue`. Returns the stored data of a hydrated embedded value
|
|
313
|
+
* as a plain object, without running getters or virtuals, such as a document's raw data; returns
|
|
314
|
+
* any other value unchanged. Return a hydrated value unchanged when its stored data does not show
|
|
315
|
+
* what it renders, such as one whose schema supplies members through getters, virtuals, methods
|
|
316
|
+
* or aliases: a value that is not a plain object counts as data, so Simfinity keeps it as it is.
|
|
317
|
+
* Simfinity calls it on the embedded objects it checks for data, including those it finds inside
|
|
318
|
+
* the returned data, and reads only own data properties of the result; a null or undefined result
|
|
319
|
+
* has no members. Without it, a plain object is read as it is, an accessor property counts as
|
|
320
|
+
* present without being called, and a value that is not a plain object counts as data.
|
|
321
|
+
*/
|
|
322
|
+
rawEmbeddedValue?(value: any): any;
|
|
281
323
|
getById(model: Model, id: any, session?: Session | null, options?: { projection?: Record<string, number>; plain?: boolean; lock?: boolean; requiredId?: any; context?: any }): any;
|
|
282
324
|
/** Optional batch read: the records found for `ids`, in getById shape and any order. After a failure, the runtime reads each ID with getById. */
|
|
283
325
|
getByIds?(model: Model, ids: any[], options?: { context?: any }): any;
|
|
326
|
+
/**
|
|
327
|
+
* Optional. Return true when a `getById` of `model` has no side effects (no query middleware or
|
|
328
|
+
* hooks that guard, count or change reads) and cannot fail where `update` or `delete` by the same
|
|
329
|
+
* ID would succeed: both reject a malformed ID with `NOT_VALID_ID` (400) as the read does, and
|
|
330
|
+
* return null for a missing record. The runtime may then skip a read whose result it does not use:
|
|
331
|
+
* the read before a delete without an `onDelete` hook, and, for a type with embedded fields, the
|
|
332
|
+
* read before an update that merges no embedded value and has neither `onUpdating` nor nested
|
|
333
|
+
* collection items. When it is absent or returns false, those reads run.
|
|
334
|
+
*/
|
|
335
|
+
readByIdIsInert?(model: Model): boolean;
|
|
284
336
|
prepareUpdate(set: Record<string, any>, unset: Record<string, string>): any;
|
|
285
337
|
update(model: Model, id: any, changes: any, session?: Session): any;
|
|
286
338
|
delete(model: Model, id: any, session?: Session): any;
|
|
@@ -430,11 +482,19 @@ export interface AuthPluginOptions {
|
|
|
430
482
|
debug?: boolean;
|
|
431
483
|
}
|
|
432
484
|
|
|
433
|
-
|
|
485
|
+
/**
|
|
486
|
+
* Authentication error: code `UNAUTHENTICATED`, status 401, default message `Authentication required`.
|
|
487
|
+
* The same class as `auth.UnauthenticatedError` and the `/auth` and `/auth/errors` subpath exports.
|
|
488
|
+
*/
|
|
489
|
+
export class UnauthenticatedError extends SimfinityError {
|
|
434
490
|
constructor(message?: string);
|
|
435
491
|
}
|
|
436
492
|
|
|
437
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Authorization error: code `FORBIDDEN`, status 403, default message `Access denied`.
|
|
495
|
+
* The same class as `auth.ForbiddenError` and the `/auth` and `/auth/errors` subpath exports.
|
|
496
|
+
*/
|
|
497
|
+
export class ForbiddenError extends SimfinityError {
|
|
438
498
|
constructor(message?: string);
|
|
439
499
|
}
|
|
440
500
|
|
package/types/plugins.d.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import { plugins } from './index.js';
|
|
6
6
|
|
|
7
7
|
export type { AuthPluginOptions, EnvelopSchemaPlugin, PermissionSchema } from './index.js';
|
|
8
|
-
|
|
8
|
+
// The /auth declaration itself, as the runtime re-exports that binding, so the two subpaths can be
|
|
9
|
+
// re-exported together.
|
|
10
|
+
export { createAuthPlugin } from './auth/index.js';
|
|
9
11
|
export const apolloCountPlugin: typeof plugins.apolloCountPlugin;
|
|
10
12
|
export const envelopCountPlugin: typeof plugins.envelopCountPlugin;
|
|
11
13
|
|