@simtlix/simfinity-core 3.5.7 → 3.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +54 -8
- package/src/const/QLValue.js +11 -3
- package/src/errors/internal-server.error.js +5 -1
- package/src/index.js +3 -0
- package/src/introspection.js +42 -8
- package/src/runtime.js +691 -177
- package/src/scalars.js +53 -33
- package/src/validators.js +159 -47
- package/types/auth/errors.d.ts +12 -0
- package/types/auth/expressions.d.ts +25 -0
- package/types/auth/index.d.ts +36 -0
- package/types/auth/rules.d.ts +30 -0
- package/types/index.d.ts +108 -15
- package/types/plugins.d.ts +14 -0
- package/types/scalars.d.ts +16 -0
- package/types/validators.d.ts +20 -0
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
|
|
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.
|
|
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,4 +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
|
+
|
|
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
|
@@ -1,24 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-core",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.9",
|
|
4
4
|
"description": "Driver-free GraphQL runtime and model metadata for Simfinity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"auth": [
|
|
11
|
+
"types/auth/index.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"auth/errors": [
|
|
14
|
+
"types/auth/errors.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"auth/expressions": [
|
|
17
|
+
"types/auth/expressions.d.ts"
|
|
18
|
+
],
|
|
19
|
+
"auth/rules": [
|
|
20
|
+
"types/auth/rules.d.ts"
|
|
21
|
+
],
|
|
22
|
+
"plugins": [
|
|
23
|
+
"types/plugins.d.ts"
|
|
24
|
+
],
|
|
25
|
+
"scalars": [
|
|
26
|
+
"types/scalars.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"validators": [
|
|
29
|
+
"types/validators.d.ts"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
8
33
|
"exports": {
|
|
9
34
|
".": {
|
|
10
35
|
"types": "./types/index.d.ts",
|
|
11
36
|
"default": "./src/index.js"
|
|
12
37
|
},
|
|
13
|
-
"./auth":
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
"./auth": {
|
|
39
|
+
"types": "./types/auth/index.d.ts",
|
|
40
|
+
"default": "./src/auth/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./auth/errors": {
|
|
43
|
+
"types": "./types/auth/errors.d.ts",
|
|
44
|
+
"default": "./src/auth/errors.js"
|
|
45
|
+
},
|
|
46
|
+
"./auth/expressions": {
|
|
47
|
+
"types": "./types/auth/expressions.d.ts",
|
|
48
|
+
"default": "./src/auth/expressions.js"
|
|
49
|
+
},
|
|
50
|
+
"./auth/rules": {
|
|
51
|
+
"types": "./types/auth/rules.d.ts",
|
|
52
|
+
"default": "./src/auth/rules.js"
|
|
53
|
+
},
|
|
17
54
|
"./internal/object-id": "./src/auth/object-id.js",
|
|
18
55
|
"./internal/relation-storage": "./src/relation-storage.js",
|
|
19
|
-
"./plugins":
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
"./plugins": {
|
|
57
|
+
"types": "./types/plugins.d.ts",
|
|
58
|
+
"default": "./src/plugins.js"
|
|
59
|
+
},
|
|
60
|
+
"./scalars": {
|
|
61
|
+
"types": "./types/scalars.d.ts",
|
|
62
|
+
"default": "./src/scalars.js"
|
|
63
|
+
},
|
|
64
|
+
"./validators": {
|
|
65
|
+
"types": "./types/validators.d.ts",
|
|
66
|
+
"default": "./src/validators.js"
|
|
67
|
+
}
|
|
22
68
|
},
|
|
23
69
|
"files": [
|
|
24
70
|
"src",
|
package/src/const/QLValue.js
CHANGED
|
@@ -3,19 +3,27 @@ import SimfinityError from '../errors/simfinity.error.js';
|
|
|
3
3
|
|
|
4
4
|
const parseQLValue = (value) => value;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// graphql-js resolves a variable that is the whole value itself, but passes a list literal such as
|
|
7
|
+
// [$from, $to] here with the operation's variables (undefined during validation). A variable item
|
|
8
|
+
// the operation leaves unset reads as null, as graphql-js reads list items of other types. An enum
|
|
9
|
+
// literal reads as its name, like the quoted name of an enum filter value.
|
|
10
|
+
const parseLiteral = (ast, variables) => {
|
|
7
11
|
switch (ast.kind) {
|
|
8
12
|
case Kind.INT: return parseInt(ast.value, 10);
|
|
9
13
|
case Kind.FLOAT: return parseFloat(ast.value);
|
|
10
14
|
case Kind.BOOLEAN: return ast.value === true || ast.value === 'true';
|
|
11
|
-
case Kind.STRING:
|
|
15
|
+
case Kind.STRING:
|
|
16
|
+
case Kind.ENUM: return ast.value;
|
|
12
17
|
case Kind.NULL: return null;
|
|
18
|
+
case Kind.VARIABLE:
|
|
19
|
+
// Object.hasOwn: a variable named like an Object.prototype member is not inherited.
|
|
20
|
+
return variables != null && Object.hasOwn(variables, ast.name.value) ? variables[ast.name.value] : null;
|
|
13
21
|
case Kind.LIST:
|
|
14
22
|
return ast.values.map((item) => {
|
|
15
23
|
if (item.kind === Kind.LIST) {
|
|
16
24
|
throw new SimfinityError('Nested filter value lists are not supported', 'INVALID_FILTER_VALUE', 400);
|
|
17
25
|
}
|
|
18
|
-
return parseLiteral(item);
|
|
26
|
+
return parseLiteral(item, variables);
|
|
19
27
|
});
|
|
20
28
|
default:
|
|
21
29
|
throw new SimfinityError('Filter values must be scalars or flat scalar lists', 'INVALID_FILTER_VALUE', 400);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import SimfinityError from './simfinity.error.js';
|
|
2
2
|
|
|
3
|
+
// An unexpected failure: code INTERNAL_SERVER_ERROR and status 500. `cause` keeps the original error.
|
|
4
|
+
// The cause is never included in GraphQL responses, or in the errors that buildErrorFormatter returns
|
|
5
|
+
// when they are serialized. It is an enumerable own property of this error, so JSON.stringify(error),
|
|
6
|
+
// or a logger that copies enumerable properties, includes it.
|
|
3
7
|
class InternalServerError extends SimfinityError {
|
|
4
8
|
constructor(message, cause) {
|
|
5
|
-
super(message, 'INTERNAL_SERVER_ERROR');
|
|
9
|
+
super(message, 'INTERNAL_SERVER_ERROR', 500);
|
|
6
10
|
this.cause = cause;
|
|
7
11
|
this.getCause = () => this.cause;
|
|
8
12
|
}
|
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
|
+
};
|