@simtlix/simfinity-core 3.5.9 → 3.7.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 -1
- package/package.json +1 -1
- package/src/runtime.js +373 -24
- package/types/auth/index.d.ts +1 -0
- package/types/index.d.ts +68 -4
- package/types/plugins.d.ts +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ 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. 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.
|
|
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
|
+
|
|
21
|
+
An adapter can also define `findChildrenBatch(model, type, connectionField, parentIds, args, { context })`, the batch form of `findChildren` for generated collection fields, and a synchronous `batchesChildren(model, type, connectionField)` predicate. The runtime calls both as adapter methods and checks them on every read. When the method is missing or the predicate returns `false`, each field calls `findChildren` at once with the arguments object it built, as before. Otherwise, within one request (a context object), the runtime groups the collection reads of one related type and connection storage field whose effective arguments (after middleware, the scope and joined scopes) hold the same plain data and are requested in the same tick, that is before the promise jobs queued so far have run. It calls the method with 2 to 100 distinct parent IDs, each a raw ID that `castId` accepts without changing its string form, and with a copy of the arguments that the method may change; a group with one distinct parent calls `findChildren`. Return one array per parent ID, in order, each holding exactly the records, order and page that `findChildren` returns for that parent; records should show their parent under the connection field, or not show one. Fields of one parent share its records, each with its own array. After a rejection, or a result of another shape or with records of another parent, each field reads alone with `findChildren`; failures other than a `SimfinityError`, and results of another shape, are logged once per type. Pairing rule: the batch must reproduce `this.findChildren`, so `batchesChildren` must return `false` when `this.findChildren` is not the adapter's own function, as with a spread copy or an `Object.assign` override that changes how children are read. The bundled adapters follow this rule: both define the two members only with `batchedCollections: true`: the SQL adapter when the schema is prepared and its plugin's `compileChildrenQuery` is paired with its `compileQuery`, and the MongoDB adapter at creation. To opt out on one adapter instance, set `adapter.findChildrenBatch = undefined` (deleting it does nothing for a class whose method is on the prototype), or make `batchesChildren` return `false`. A copy that overrides `getById` still inherits the base `getByIds`, as before.
|
|
20
22
|
|
|
21
23
|
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
24
|
|
package/package.json
CHANGED
package/src/runtime.js
CHANGED
|
@@ -427,6 +427,21 @@ const cloneInput = (value, type) => {
|
|
|
427
427
|
return value;
|
|
428
428
|
};
|
|
429
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
|
+
|
|
430
445
|
export const createRuntime = (adapter) => {
|
|
431
446
|
if (!adapter) {
|
|
432
447
|
throw new SimfinityError('A database adapter is required', 'ADAPTER_REQUIRED', 500);
|
|
@@ -1246,10 +1261,10 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
1246
1261
|
};
|
|
1247
1262
|
|
|
1248
1263
|
const executeRegisteredMutation = (args, callback, context, session, inputModel) => {
|
|
1249
|
-
const
|
|
1264
|
+
const nextInput = attemptInputs(args, inputModel);
|
|
1250
1265
|
return adapter.withTransaction(
|
|
1251
1266
|
session,
|
|
1252
|
-
(mySession) => callback(
|
|
1267
|
+
(mySession) => callback(nextInput(), mySession, context),
|
|
1253
1268
|
);
|
|
1254
1269
|
};
|
|
1255
1270
|
|
|
@@ -1270,10 +1285,23 @@ const iterateOnCollectionFields = async (materializedModel, gqltype, objectId, s
|
|
|
1270
1285
|
}
|
|
1271
1286
|
};
|
|
1272
1287
|
|
|
1273
|
-
|
|
1274
|
-
|
|
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;
|
|
1293
|
+
|
|
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
|
+
}
|
|
1275
1303
|
|
|
1276
|
-
if (
|
|
1304
|
+
if (hasHook) {
|
|
1277
1305
|
await controller.onDelete(currentObject, session, context);
|
|
1278
1306
|
}
|
|
1279
1307
|
|
|
@@ -1346,13 +1374,35 @@ const clearNullEmbeddedMembers = (merged, fieldEntry, patch) => {
|
|
|
1346
1374
|
}
|
|
1347
1375
|
};
|
|
1348
1376
|
|
|
1349
|
-
|
|
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) => {
|
|
1350
1399
|
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
|
|
1351
1400
|
const objectId = args.id;
|
|
1352
1401
|
const argTypes = gqltype.getFields();
|
|
1353
1402
|
const embeddedFieldNames = getEmbeddedFieldNames(gqltype);
|
|
1354
1403
|
|
|
1355
|
-
if (embeddedFieldNames.length > 0
|
|
1404
|
+
if (embeddedFieldNames.length > 0
|
|
1405
|
+
&& readsStoredEmbedded(Model, controller, materializedModel, embeddedFieldNames, recordChecked)) {
|
|
1356
1406
|
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
1357
1407
|
const currentObject = await adapter.getById(Model, objectId, session, {
|
|
1358
1408
|
projection,
|
|
@@ -1365,7 +1415,6 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
1365
1415
|
const newObjectData = ownValue(materializedModel.modelArgs, fieldEntryName);
|
|
1366
1416
|
if (newObjectData) {
|
|
1367
1417
|
if (Array.isArray(newObjectData)) {
|
|
1368
|
-
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
1369
1418
|
completeEmbeddedValue(fieldEntryName, argTypes[fieldEntryName], newObjectData);
|
|
1370
1419
|
} else {
|
|
1371
1420
|
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
@@ -1427,7 +1476,7 @@ const onStateChanged = async (Model, gqltype, controller, args, session, actionF
|
|
|
1427
1476
|
}
|
|
1428
1477
|
|
|
1429
1478
|
args.state = stateValue(actionField.to);
|
|
1430
|
-
let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context);
|
|
1479
|
+
let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context, true);
|
|
1431
1480
|
result = adapter.toObject(result);
|
|
1432
1481
|
result.state = actionField.to.value;
|
|
1433
1482
|
return result;
|
|
@@ -1543,13 +1592,11 @@ const executeOperation = (Model, gqltype, controller, args, operation, actionFie
|
|
|
1543
1592
|
const limit = getMaxNestedOperations();
|
|
1544
1593
|
if (limit !== null && operation !== operations.DELETE) assertNestedOperations(gqltype, args, limit);
|
|
1545
1594
|
if (operation !== operations.DELETE) assertCollectionWrites(gqltype, args);
|
|
1546
|
-
const
|
|
1595
|
+
const nextInput = operation === operations.DELETE ? () => args : attemptInputs(args, inputType);
|
|
1547
1596
|
return adapter.withTransaction(
|
|
1548
1597
|
session,
|
|
1549
1598
|
async (mySession) => {
|
|
1550
|
-
const attemptArgs =
|
|
1551
|
-
? inputSnapshot
|
|
1552
|
-
: cloneInput(inputSnapshot, inputType);
|
|
1599
|
+
const attemptArgs = nextInput();
|
|
1553
1600
|
switch (operation) {
|
|
1554
1601
|
case operations.SAVE:
|
|
1555
1602
|
return onSaveObject(Model, gqltype, controller, attemptArgs, mySession, null, context);
|
|
@@ -1594,9 +1641,12 @@ const executeItemFunction = async (gqltype, collectionField, parentKey, session,
|
|
|
1594
1641
|
const args = operationType === operations.DELETE ? { id: element } : { input: element };
|
|
1595
1642
|
await executeMiddleware({ type, args, operation: operationType, context });
|
|
1596
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;
|
|
1597
1647
|
if (operationType === operations.UPDATE || operationType === operations.DELETE) {
|
|
1598
1648
|
const id = operationType === operations.DELETE ? args.id : args.input.id;
|
|
1599
|
-
|
|
1649
|
+
child = await adapter.getById(type.model, id, session, { plain: true, lock: true });
|
|
1600
1650
|
if (!child) {
|
|
1601
1651
|
throw new SimfinityError(`${collectionGQLType.name} ${id} is not valid`, 'NOT_VALID_ID', 404);
|
|
1602
1652
|
}
|
|
@@ -1612,10 +1662,10 @@ const executeItemFunction = async (gqltype, collectionField, parentKey, session,
|
|
|
1612
1662
|
break;
|
|
1613
1663
|
case operations.UPDATE:
|
|
1614
1664
|
await onUpdateSubject(type.model, collectionGQLType, type.controller,
|
|
1615
|
-
args.input, session, linkToParent, context);
|
|
1665
|
+
args.input, session, linkToParent, context, true);
|
|
1616
1666
|
break;
|
|
1617
1667
|
case operations.DELETE:
|
|
1618
|
-
await onDelete(type.model, type.controller, args.id, session, context);
|
|
1668
|
+
await onDelete(type.model, type.controller, args.id, session, context, child);
|
|
1619
1669
|
}
|
|
1620
1670
|
}
|
|
1621
1671
|
};
|
|
@@ -1866,6 +1916,312 @@ const resolveById = async (type, args, context, requiredId) => {
|
|
|
1866
1916
|
return results[0] || null;
|
|
1867
1917
|
};
|
|
1868
1918
|
|
|
1919
|
+
// Batched collection reads. A generated collection field reads its children with one
|
|
1920
|
+
// adapter.findChildren call per parent unless the adapter defines findChildrenBatch and its
|
|
1921
|
+
// batchesChildren predicate does not return false. Then, within one request (a context object), the
|
|
1922
|
+
// reads of one related type and connection storage field whose effective arguments (after
|
|
1923
|
+
// middleware, the scope and joined scopes) hold the same plain data wait until the promise jobs
|
|
1924
|
+
// queued so far have run, as references do, and are read together, at most CHILD_BATCH_SIZE
|
|
1925
|
+
// distinct parents per call.
|
|
1926
|
+
const CHILD_BATCH_SIZE = 100;
|
|
1927
|
+
// Arguments whose canonical key would exceed this many characters, such as a long IN list, read
|
|
1928
|
+
// alone, so a field never spends more than this on canonicalization.
|
|
1929
|
+
const CHILD_ARGS_KEY_BUDGET = 64 * 1024;
|
|
1930
|
+
const NOT_PLAIN = Symbol('not plain data');
|
|
1931
|
+
|
|
1932
|
+
// The canonical key of a collection read's effective arguments: two reads share it only when their
|
|
1933
|
+
// arguments hold the same data. Only plain data qualifies: strings, numbers (-0, NaN and Infinity
|
|
1934
|
+
// kept distinct), booleans, undefined, null, bigints, dates (by time, with no own properties), dense
|
|
1935
|
+
// arrays, and objects with the Object or a null prototype whose own string-keyed properties are
|
|
1936
|
+
// enumerable data properties, all writable and configurable, or all fixed by freezing. The key keeps
|
|
1937
|
+
// property order, null prototypes, frozen objects and shared references. Anything else, such as a
|
|
1938
|
+
// function, a symbol, a class instance (an ObjectId included), an accessor, a non-enumerable,
|
|
1939
|
+
// sealed or non-extensible member, a sparse or decorated array, a cycle, an exotic object that fails
|
|
1940
|
+
// while it is read, or a key over the budget, returns null, and the read runs alone.
|
|
1941
|
+
const childArgsKey = (args) => {
|
|
1942
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) return null;
|
|
1943
|
+
const parts = [];
|
|
1944
|
+
let size = 0;
|
|
1945
|
+
const push = (part) => {
|
|
1946
|
+
size += part.length;
|
|
1947
|
+
if (size > CHILD_ARGS_KEY_BUDGET) throw NOT_PLAIN;
|
|
1948
|
+
parts.push(part);
|
|
1949
|
+
};
|
|
1950
|
+
const seen = new Map();
|
|
1951
|
+
const visit = (value) => {
|
|
1952
|
+
switch (typeof value) {
|
|
1953
|
+
case 'string':
|
|
1954
|
+
if (value.length > CHILD_ARGS_KEY_BUDGET) throw NOT_PLAIN;
|
|
1955
|
+
push(JSON.stringify(value));
|
|
1956
|
+
return;
|
|
1957
|
+
case 'number': push(Object.is(value, -0) ? '-0' : String(value)); return;
|
|
1958
|
+
case 'boolean':
|
|
1959
|
+
case 'undefined': push(String(value)); return;
|
|
1960
|
+
case 'bigint': push(`${value}n`); return;
|
|
1961
|
+
case 'object': if (value !== null) break; push('null'); return;
|
|
1962
|
+
default: throw NOT_PLAIN;
|
|
1963
|
+
}
|
|
1964
|
+
const known = seen.get(value);
|
|
1965
|
+
if (known) {
|
|
1966
|
+
// A shared reference repeats the index of its first visit; a cycle is not plain data.
|
|
1967
|
+
if (!known.done) throw NOT_PLAIN;
|
|
1968
|
+
push(`@${known.index}`);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const entry = { index: seen.size, done: false };
|
|
1972
|
+
seen.set(value, entry);
|
|
1973
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1974
|
+
const frozen = Object.isFrozen(value);
|
|
1975
|
+
const keys = Reflect.ownKeys(value);
|
|
1976
|
+
if (prototype === Date.prototype) {
|
|
1977
|
+
if (keys.length) throw NOT_PLAIN;
|
|
1978
|
+
// Throws a TypeError, caught below, for an object that only inherits from Date.prototype.
|
|
1979
|
+
push(`D${frozen ? 'F' : ''}${Date.prototype.getTime.call(value)}`);
|
|
1980
|
+
} else {
|
|
1981
|
+
const array = Array.isArray(value);
|
|
1982
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) throw NOT_PLAIN;
|
|
1983
|
+
// A dense array owns its indices, in order, then its length, and nothing else.
|
|
1984
|
+
if (array && (keys.length !== value.length + 1
|
|
1985
|
+
|| keys.some((key, index) => key !== (index < value.length ? String(index) : 'length'))
|
|
1986
|
+
|| Reflect.getOwnPropertyDescriptor(value, 'length').writable === frozen)) throw NOT_PLAIN;
|
|
1987
|
+
if (!frozen && !Object.isExtensible(value)) throw NOT_PLAIN;
|
|
1988
|
+
push(`${array ? '[' : '{'}${prototype === null ? '0' : ''}${frozen ? 'F' : ''}`);
|
|
1989
|
+
for (const key of keys) {
|
|
1990
|
+
if (typeof key === 'symbol') throw NOT_PLAIN;
|
|
1991
|
+
if (array && key === 'length') continue;
|
|
1992
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
1993
|
+
if (!descriptor || !('value' in descriptor) || !descriptor.enumerable
|
|
1994
|
+
|| descriptor.writable === frozen || descriptor.configurable === frozen) throw NOT_PLAIN;
|
|
1995
|
+
if (!array) push(`${JSON.stringify(key)}:`);
|
|
1996
|
+
visit(descriptor.value);
|
|
1997
|
+
push(',');
|
|
1998
|
+
}
|
|
1999
|
+
push(array ? ']' : '}');
|
|
2000
|
+
}
|
|
2001
|
+
entry.done = true;
|
|
2002
|
+
};
|
|
2003
|
+
try {
|
|
2004
|
+
visit(args);
|
|
2005
|
+
return parts.join('');
|
|
2006
|
+
} catch {
|
|
2007
|
+
// Not plain data, too large, or an exotic object (such as a proxy) that failed while it was read.
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
// A copy of arguments that childArgsKey accepted, with the same prototypes, frozen objects, dates
|
|
2013
|
+
// and shared references, so the adapter may change it without reaching the request's objects.
|
|
2014
|
+
const clonePlainArgs = (value, copies = new Map()) => {
|
|
2015
|
+
if (value === null || typeof value !== 'object') return value;
|
|
2016
|
+
if (copies.has(value)) return copies.get(value);
|
|
2017
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2018
|
+
if (prototype === Date.prototype) {
|
|
2019
|
+
const date = new Date(Date.prototype.getTime.call(value));
|
|
2020
|
+
copies.set(value, date);
|
|
2021
|
+
return Object.isFrozen(value) ? Object.freeze(date) : date;
|
|
2022
|
+
}
|
|
2023
|
+
const copy = Array.isArray(value) ? [] : Object.create(prototype);
|
|
2024
|
+
copies.set(value, copy);
|
|
2025
|
+
for (const key of Object.keys(value)) {
|
|
2026
|
+
Object.defineProperty(copy, key, {
|
|
2027
|
+
value: clonePlainArgs(value[key], copies), writable: true, enumerable: true, configurable: true,
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
return Object.isFrozen(value) ? Object.freeze(copy) : copy;
|
|
2031
|
+
};
|
|
2032
|
+
|
|
2033
|
+
// The batch key of a parent ID: its string form, when the adapter casts the ID without changing
|
|
2034
|
+
// that form. A missing ID (which keeps the adapter's own semantics, such as MongoDB matching
|
|
2035
|
+
// children without a parent), an object whose string form says nothing of its value, an ID castId
|
|
2036
|
+
// rejects and one it normalizes have none, and read alone.
|
|
2037
|
+
const childParentKey = (parentId) => {
|
|
2038
|
+
try {
|
|
2039
|
+
if (parentId == null) return null;
|
|
2040
|
+
const key = String(parentId);
|
|
2041
|
+
if (typeof parentId === 'object' && key === Object.prototype.toString.call(parentId)) return null;
|
|
2042
|
+
return String(adapter.castId(parentId)) === key ? key : null;
|
|
2043
|
+
} catch {
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
// Collection reads of one request that wait for a batch: context -> related type registration ->
|
|
2049
|
+
// (connection storage field + canonical arguments) -> group. Nothing is cached: each group is read
|
|
2050
|
+
// once and dropped.
|
|
2051
|
+
const childBatches = new WeakMap();
|
|
2052
|
+
|
|
2053
|
+
const readChildrenAlone = (type, connection, parentId, args) => adapter.findChildren(
|
|
2054
|
+
type.model, type.gqltype, connection.storageFieldName, parentId, args, null,
|
|
2055
|
+
);
|
|
2056
|
+
|
|
2057
|
+
// Whether the adapter reads this type's children in batches, checked on every read as an adapter
|
|
2058
|
+
// method call. A predicate that throws keeps per-parent reads; an error other than a SimfinityError
|
|
2059
|
+
// is reported once per type.
|
|
2060
|
+
const batchesChildren = (type, connection) => {
|
|
2061
|
+
if (adapter.batchesChildren == null) return true;
|
|
2062
|
+
try {
|
|
2063
|
+
return adapter.batchesChildren(type.model, type.gqltype, connection.storageFieldName) !== false;
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
if (!(error instanceof SimfinityError)) {
|
|
2066
|
+
warnOnce(type.gqltype, 'batchesChildren', 'Configuration issue: adapter.batchesChildren failed for '
|
|
2067
|
+
+ `${type.gqltype.name} (${error?.message ?? String(error)}); its collection fields read one parent at a time.`);
|
|
2068
|
+
}
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
// Whether a batched child record may belong to the parent with this key: it is an object, and the
|
|
2074
|
+
// parent it stores under the connection storage field (or its GraphQL name), when it shows one, has
|
|
2075
|
+
// that key by string form or by castId. A stored list (a multikey connection) needs one such entry.
|
|
2076
|
+
const belongsToParent = (record, connection, key) => {
|
|
2077
|
+
if (record === null || typeof record !== 'object') return false;
|
|
2078
|
+
let stored = ownValue(record, connection.storageFieldName);
|
|
2079
|
+
if (stored === undefined && connection.graphqlFieldName) stored = ownValue(record, connection.graphqlFieldName);
|
|
2080
|
+
if (stored === undefined) return true;
|
|
2081
|
+
const matches = (value) => {
|
|
2082
|
+
const id = value?._id ?? value;
|
|
2083
|
+
if (id == null) return false;
|
|
2084
|
+
try {
|
|
2085
|
+
return String(id) === key || String(adapter.castId(id)) === key;
|
|
2086
|
+
} catch {
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
return Array.isArray(stored) ? stored.some(matches) : matches(stored);
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
// The fields of one parent share its records, as references do, but each gets its own list.
|
|
2094
|
+
const settleChildren = (waiters, list) => waiters.forEach(({ resolve }, index) => {
|
|
2095
|
+
resolve(index === 0 ? list : [...list]);
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
// After a failed read, each field reads alone, in its own promise job, with its own copy of the
|
|
2099
|
+
// arguments and its own parent ID, so an error fails only the fields it would fail alone.
|
|
2100
|
+
const readEachChildAlone = (type, group, waiters) => {
|
|
2101
|
+
for (const { parentId, resolve, reject } of waiters) {
|
|
2102
|
+
Promise.resolve()
|
|
2103
|
+
.then(() => readChildrenAlone(type, group.connection, parentId, clonePlainArgs(group.args)))
|
|
2104
|
+
.then(resolve, reject);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
// Whether a batch result holds one list per parent, in order, of records of that parent. Every index is
|
|
2109
|
+
// checked, holes included, since Array.prototype.every skips empty slots.
|
|
2110
|
+
const listsMatchParents = (lists, parents, connection) => {
|
|
2111
|
+
if (!Array.isArray(lists) || lists.length !== parents.length) return false;
|
|
2112
|
+
for (let index = 0; index < lists.length; index++) {
|
|
2113
|
+
const records = Object.hasOwn(lists, index) ? lists[index] : undefined;
|
|
2114
|
+
if (!Array.isArray(records)) return false;
|
|
2115
|
+
for (let position = 0; position < records.length; position++) {
|
|
2116
|
+
if (!Object.hasOwn(records, position) || !belongsToParent(records[position], connection, parents[index].key)) return false;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
return true;
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
const readChildren = async (type, group, parents, context) => {
|
|
2123
|
+
const { connection } = group;
|
|
2124
|
+
if (parents.length === 1) {
|
|
2125
|
+
// One distinct parent reads with findChildren, as before, shared by the fields that read it.
|
|
2126
|
+
const { waiters } = parents[0];
|
|
2127
|
+
let list;
|
|
2128
|
+
try {
|
|
2129
|
+
list = await readChildrenAlone(type, connection, waiters[0].parentId, clonePlainArgs(group.args));
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
if (waiters.length === 1) waiters[0].reject(error);
|
|
2132
|
+
else readEachChildAlone(type, group, waiters);
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
if (waiters.length > 1 && !Array.isArray(list)) readEachChildAlone(type, group, waiters);
|
|
2136
|
+
else settleChildren(waiters, list);
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
let lists;
|
|
2140
|
+
try {
|
|
2141
|
+
lists = await adapter.findChildrenBatch(type.model, type.gqltype, connection.storageFieldName,
|
|
2142
|
+
parents.map(({ waiters }) => waiters[0].parentId), clonePlainArgs(group.args), { context });
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
if (!(error instanceof SimfinityError)) {
|
|
2145
|
+
warnOnce(type.gqltype, 'findChildrenBatch', `adapter.findChildrenBatch failed for ${type.gqltype.name} `
|
|
2146
|
+
+ `(${error?.message ?? String(error)}); the fields it read were read one parent at a time. Later failures `
|
|
2147
|
+
+ 'for this type are not reported.');
|
|
2148
|
+
}
|
|
2149
|
+
parents.forEach(({ waiters }) => readEachChildAlone(type, group, waiters));
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
if (!listsMatchParents(lists, parents, connection)) {
|
|
2153
|
+
warnOnce(type.gqltype, 'findChildrenBatch', 'Configuration issue: adapter.findChildrenBatch did not return one '
|
|
2154
|
+
+ `list of ${type.gqltype.name} records per parent ID, in parentIds order; the fields it read were read one `
|
|
2155
|
+
+ 'parent at a time.');
|
|
2156
|
+
parents.forEach(({ waiters }) => readEachChildAlone(type, group, waiters));
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
parents.forEach(({ waiters }, index) => settleChildren(waiters, lists[index]));
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
const flushChildren = (type, group, context) => {
|
|
2163
|
+
const parents = [...group.parents].map(([key, { waiters }]) => ({ key, waiters }));
|
|
2164
|
+
for (let start = 0; start < parents.length; start += CHILD_BATCH_SIZE) {
|
|
2165
|
+
const chunk = parents.slice(start, start + CHILD_BATCH_SIZE);
|
|
2166
|
+
// An unexpected error settles every field of the chunk that is still waiting.
|
|
2167
|
+
readChildren(type, group, chunk, context).catch((error) => {
|
|
2168
|
+
for (const { waiters } of chunk) waiters.forEach(({ reject }) => reject(error));
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
// A generated collection read. It runs at once, with the arguments object it received, as before,
|
|
2174
|
+
// unless the adapter batches this type's children, the request has a context object, the parent ID
|
|
2175
|
+
// has a batch key, and the effective arguments are plain data with valid pagination and a key within
|
|
2176
|
+
// the budget. Then it joins the request's group for its related type, connection storage field and
|
|
2177
|
+
// arguments: the first read of a group copies the arguments, and later reads compute only the key.
|
|
2178
|
+
const loadChildren = (type, connection, parentId, args, context) => {
|
|
2179
|
+
if (typeof adapter.findChildrenBatch !== 'function' || typeof context !== 'object' || context === null
|
|
2180
|
+
|| !batchesChildren(type, connection)) return readChildrenAlone(type, connection, parentId, args);
|
|
2181
|
+
const key = childParentKey(parentId);
|
|
2182
|
+
const argsKey = key === null ? null : childArgsKey(args);
|
|
2183
|
+
if (argsKey === null) return readChildrenAlone(type, connection, parentId, args);
|
|
2184
|
+
try {
|
|
2185
|
+
// Invalid pagination fails each field alone, as findChildren reports it.
|
|
2186
|
+
paginationStages(args.pagination, true);
|
|
2187
|
+
} catch {
|
|
2188
|
+
return readChildrenAlone(type, connection, parentId, args);
|
|
2189
|
+
}
|
|
2190
|
+
let byType = childBatches.get(context);
|
|
2191
|
+
if (!byType) {
|
|
2192
|
+
byType = new Map();
|
|
2193
|
+
childBatches.set(context, byType);
|
|
2194
|
+
}
|
|
2195
|
+
let groups = byType.get(type);
|
|
2196
|
+
if (!groups) {
|
|
2197
|
+
groups = new Map();
|
|
2198
|
+
byType.set(type, groups);
|
|
2199
|
+
}
|
|
2200
|
+
const groupKey = `${JSON.stringify(connection.storageFieldName)}${argsKey}`;
|
|
2201
|
+
let group = groups.get(groupKey);
|
|
2202
|
+
if (!group) {
|
|
2203
|
+
let snapshot;
|
|
2204
|
+
try {
|
|
2205
|
+
snapshot = clonePlainArgs(args);
|
|
2206
|
+
} catch {
|
|
2207
|
+
return readChildrenAlone(type, connection, parentId, args);
|
|
2208
|
+
}
|
|
2209
|
+
group = { connection, args: snapshot, parents: new Map() };
|
|
2210
|
+
groups.set(groupKey, group);
|
|
2211
|
+
const created = group;
|
|
2212
|
+
// As for references, flush once the promise jobs queued so far have run.
|
|
2213
|
+
Promise.resolve().then(() => process.nextTick(() => {
|
|
2214
|
+
groups.delete(groupKey);
|
|
2215
|
+
if (!groups.size && byType.get(type) === groups) byType.delete(type);
|
|
2216
|
+
flushChildren(type, created, context);
|
|
2217
|
+
}));
|
|
2218
|
+
}
|
|
2219
|
+
return new Promise((resolve, reject) => {
|
|
2220
|
+
if (!group.parents.has(key)) group.parents.set(key, { waiters: [] });
|
|
2221
|
+
group.parents.get(key).waiters.push({ parentId, resolve, reject });
|
|
2222
|
+
});
|
|
2223
|
+
};
|
|
2224
|
+
|
|
1869
2225
|
// Why a type's generated create or update input has no fields, for the warnings of buildMutation. An
|
|
1870
2226
|
// update input also needs a writable id.
|
|
1871
2227
|
const withoutWritableFields = (type, { noWritableId = false } = {}) => {
|
|
@@ -2505,14 +2861,7 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2505
2861
|
await executeMiddleware(params);
|
|
2506
2862
|
await executeScope(params);
|
|
2507
2863
|
await applyJoinedScopes(joinedScopes, params);
|
|
2508
|
-
return
|
|
2509
|
-
relatedTypeInfo.model,
|
|
2510
|
-
relatedTypeInfo.gqltype,
|
|
2511
|
-
connection.storageFieldName,
|
|
2512
|
-
parent._id ?? parent.id,
|
|
2513
|
-
params.args,
|
|
2514
|
-
null,
|
|
2515
|
-
);
|
|
2864
|
+
return loadChildren(relatedTypeInfo, connection, parent._id ?? parent.id, params.args, context);
|
|
2516
2865
|
}, { simfinityQuery: { typeName: relatedType.name, operation: 'find' } });
|
|
2517
2866
|
runtimeBoundTypes.set(gqltype, runtimeIdentity);
|
|
2518
2867
|
} else if (fieldEntry.type instanceof GraphQLObjectType
|
package/types/auth/index.d.ts
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -323,6 +323,16 @@ export interface DatabaseAdapter<Model = any, Session = any> {
|
|
|
323
323
|
getById(model: Model, id: any, session?: Session | null, options?: { projection?: Record<string, number>; plain?: boolean; lock?: boolean; requiredId?: any; context?: any }): any;
|
|
324
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. */
|
|
325
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;
|
|
326
336
|
prepareUpdate(set: Record<string, any>, unset: Record<string, string>): any;
|
|
327
337
|
update(model: Model, id: any, changes: any, session?: Session): any;
|
|
328
338
|
delete(model: Model, id: any, session?: Session): any;
|
|
@@ -330,6 +340,41 @@ export interface DatabaseAdapter<Model = any, Session = any> {
|
|
|
330
340
|
count(model: Model, type: GraphQLObjectType, args: any, session?: Session | null): Promise<number> | number;
|
|
331
341
|
aggregate(model: Model, type: GraphQLObjectType, args: any, session?: Session | null): any;
|
|
332
342
|
findChildren(model: Model, type: GraphQLObjectType, connectionField: string, parentId: any, args: any, session?: Session | null): any;
|
|
343
|
+
/**
|
|
344
|
+
* Optional batch form of `findChildren` for generated collection fields. The runtime calls it as
|
|
345
|
+
* an adapter method (`this` is the adapter) only when `batchesChildren` does not return false, and
|
|
346
|
+
* checks both members on every read. Within one request (a GraphQL context object), it groups the
|
|
347
|
+
* collection reads of one related type and connection storage field whose effective arguments
|
|
348
|
+
* (after middleware, the scope and joined scopes) hold the same plain data and are requested
|
|
349
|
+
* before the promise jobs queued so far have run, and calls this method with 2 to 100 distinct
|
|
350
|
+
* parent IDs: each one the raw ID the first of its fields read, never null, and one that `castId`
|
|
351
|
+
* accepts without changing its string form. A group with one distinct parent calls `findChildren`
|
|
352
|
+
* instead. `args` is a copy the method may change; it holds only plain data and dates, and its
|
|
353
|
+
* pagination is valid. `options.context` is the request context; the runtime passes no session.
|
|
354
|
+
*
|
|
355
|
+
* Return, or resolve to, one array per parent ID, in `parentIds` order, each holding exactly the
|
|
356
|
+
* records, order and page that `findChildren(model, type, connectionField, parentIds[i], args, null)`
|
|
357
|
+
* returns for that parent; the result must not depend on `options.context`. Records should show
|
|
358
|
+
* their parent under `connectionField` (or the connection field's GraphQL name), as `findChildren`
|
|
359
|
+
* records do, or not show one. Fields that read the same parent share its records, each with its
|
|
360
|
+
* own array. After a rejection or throw, or a result of another shape (not an array, another
|
|
361
|
+
* length, an entry that is not an array, or a record that shows another parent), each field reads
|
|
362
|
+
* alone with `findChildren`; a failure other than a `SimfinityError` and a result of another shape
|
|
363
|
+
* are reported once per type with `console.warn`. To keep per-parent reads for a model or field,
|
|
364
|
+
* return false from `batchesChildren`.
|
|
365
|
+
*/
|
|
366
|
+
findChildrenBatch?: ((model: Model, type: GraphQLObjectType, connectionField: string, parentIds: any[], args: any, options?: { context?: any }) => any[][] | PromiseLike<any[][]>) | undefined;
|
|
367
|
+
/**
|
|
368
|
+
* Optional, synchronous. Return false to read the children of `model` through `connectionField`
|
|
369
|
+
* with one `findChildren` call per field, as an adapter without `findChildrenBatch` does: at once,
|
|
370
|
+
* with the arguments object the field built. The runtime calls it as an adapter method on every
|
|
371
|
+
* read that could join a batch. Return false whenever `findChildrenBatch` would not reproduce
|
|
372
|
+
* `this.findChildren`, such as when `this.findChildren` is not the adapter's own function (a copy
|
|
373
|
+
* or an `Object.assign` override that changes how children are read), or for a model whose reads
|
|
374
|
+
* have hooks or settings that a batched read would bypass. When it is absent, the runtime batches
|
|
375
|
+
* every read it can.
|
|
376
|
+
*/
|
|
377
|
+
batchesChildren?: ((model: Model, type: GraphQLObjectType, connectionField: string) => boolean) | undefined;
|
|
333
378
|
}
|
|
334
379
|
export function createRuntime<Model = any, Session = any>(adapter: DatabaseAdapter<Model, Session>): Runtime<Model, Session>;
|
|
335
380
|
|
|
@@ -353,15 +398,34 @@ export function configureQueryLimits(options?: { maxPageSize?: number }): void;
|
|
|
353
398
|
export function configureMutationLimits(options?: MutationLimitsOptions): void;
|
|
354
399
|
export function paginationStages(pagination: { page: number; size: number } | null | undefined, withDefault: boolean): Array<{ $skip: number } | { $limit: number }>;
|
|
355
400
|
|
|
356
|
-
/**
|
|
401
|
+
/**
|
|
402
|
+
* Envelop-style schema plugin, as MCP `schemaPlugins` accepts it; `createAuthPlugin` returns the
|
|
403
|
+
* narrower {@link AuthSchemaPlugin}. `@simtlix/simfinity-mcp` re-exports this declaration, so
|
|
404
|
+
* `export *` of both packages exports one `EnvelopSchemaPlugin`.
|
|
405
|
+
*/
|
|
357
406
|
export interface EnvelopSchemaPlugin {
|
|
407
|
+
/**
|
|
408
|
+
* Wraps resolvers in place. MCP `schemaPlugins` awaits a returned promise; Envelop and Yoga do
|
|
409
|
+
* not, so plugins shared with them should install synchronously.
|
|
410
|
+
*/
|
|
358
411
|
onSchemaChange?: (payload: {
|
|
359
412
|
schema: GraphQLSchema;
|
|
360
413
|
replaceSchema: (schema: GraphQLSchema) => void;
|
|
361
|
-
}) => void
|
|
414
|
+
}) => void | Promise<void>;
|
|
362
415
|
[key: string]: unknown;
|
|
363
416
|
}
|
|
364
417
|
|
|
418
|
+
/**
|
|
419
|
+
* What `createAuthPlugin` returns: an {@link EnvelopSchemaPlugin} whose hook wraps the resolvers
|
|
420
|
+
* synchronously and returns nothing.
|
|
421
|
+
*/
|
|
422
|
+
export interface AuthSchemaPlugin extends EnvelopSchemaPlugin {
|
|
423
|
+
onSchemaChange?: (payload: {
|
|
424
|
+
schema: GraphQLSchema;
|
|
425
|
+
replaceSchema: (schema: GraphQLSchema) => void;
|
|
426
|
+
}) => void;
|
|
427
|
+
}
|
|
428
|
+
|
|
365
429
|
/** A single field validator (throws SimfinityError VALIDATION_ERROR on failure). */
|
|
366
430
|
export interface FieldValidator {
|
|
367
431
|
validate(typeName: string, fieldName: string, value: unknown, session?: unknown): Promise<void>;
|
|
@@ -495,7 +559,7 @@ export const auth: {
|
|
|
495
559
|
* `onSchemaChange` throws TypeError, before wrapping any field, when a schema type was created
|
|
496
560
|
* by another copy of the graphql module.
|
|
497
561
|
*/
|
|
498
|
-
createAuthPlugin(permissions: PermissionSchema, options?: AuthPluginOptions):
|
|
562
|
+
createAuthPlugin(permissions: PermissionSchema, options?: AuthPluginOptions): AuthSchemaPlugin;
|
|
499
563
|
/** @deprecated Use createAuthPlugin instead. graphql-middleware compatible middleware. */
|
|
500
564
|
createAuthMiddleware(
|
|
501
565
|
permissions: PermissionSchema,
|
|
@@ -565,7 +629,7 @@ export const auth: {
|
|
|
565
629
|
|
|
566
630
|
/** GraphQL server plugins: auth plugin factory plus count-extension plugins. */
|
|
567
631
|
export const plugins: {
|
|
568
|
-
createAuthPlugin(permissions: PermissionSchema, options?: AuthPluginOptions):
|
|
632
|
+
createAuthPlugin(permissions: PermissionSchema, options?: AuthPluginOptions): AuthSchemaPlugin;
|
|
569
633
|
/** Apollo Server plugin that copies `contextValue.count` into `extensions.count`. */
|
|
570
634
|
apolloCountPlugin(): {
|
|
571
635
|
requestDidStart(): Promise<{
|
package/types/plugins.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { plugins } from './index.js';
|
|
6
6
|
|
|
7
|
-
export type { AuthPluginOptions, EnvelopSchemaPlugin, PermissionSchema } from './index.js';
|
|
7
|
+
export type { AuthPluginOptions, AuthSchemaPlugin, 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
9
|
// re-exported together.
|
|
10
10
|
export { createAuthPlugin } from './auth/index.js';
|