@simtlix/simfinity-core 3.12.0 → 3.14.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 +15 -3
- package/package.json +2 -1
- package/src/const/QLQuantifier.js +21 -0
- package/src/index.js +2 -0
- package/src/metadata.js +24 -0
- package/src/query-emptiness.js +2 -2
- package/src/query-frames.js +510 -0
- package/src/query-limits.js +9 -2
- package/src/query-operators.js +23 -9
- package/src/query-plan.js +236 -155
- package/src/record-version.js +235 -0
- package/src/runtime.js +664 -168
- package/types/index.d.ts +181 -23
package/README.md
CHANGED
|
@@ -16,13 +16,17 @@ For relational storage, `@simtlix/simfinity-sql` builds on core and delegates ph
|
|
|
16
16
|
- `registerCustomQuery` for reads whose results are not stored records, such as previews, other sources or permission evaluations;
|
|
17
17
|
- `registerView` for a named list read of one registered type, with params and a fixed predicate, run through the type's scope;
|
|
18
18
|
- `preventCreatingCollection` to pass validation-only storage setup to an adapter;
|
|
19
|
-
- `configureQueryLimits` and `configureMutationLimits` for the process-wide page-size,
|
|
19
|
+
- `configureQueryLimits` and `configureMutationLimits` for the process-wide page-size, compiled-frame, frame-nesting and nested-operation limits.
|
|
20
20
|
|
|
21
21
|
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. The GraphQL types that `prepare()` and `createModel()` receive still contain virtual fields, which must never be stored: an adapter that builds storage from a type's fields skips each field for which `isVirtualField(field)`, exported by this package with `virtualFieldOptions(field)`, returns true, on entities and embedded types alike, or builds it from `describeModels()`, which leaves them out. Generated writes, filters, sorts and aggregations never name a virtual field, but a controller hook can still assign one. 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` and virtual members, 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.
|
|
22
22
|
|
|
23
23
|
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.
|
|
24
24
|
|
|
25
|
-
An adapter declares the filter operators it compiles as `queryFeatures: { operators }`, a list of `QLOperator` values that includes `EQ`, which a condition without an operator means; names this version does not know are ignored. `createRuntime()` reads it once and throws `INVALID_QUERY_FEATURES` (500) unless it is `undefined`, `null` or an object whose `operators` is an array of strings and whose optional `referenceEmptiness` is a boolean, `undefined` or `null`. Without a declaration the adapter supports the operators of 3.7.0, `EQ`, `NE`, `LT`, `LTE`, `GT`, `GTE`, `BTW`, `IN`, `NIN` and `LIKE`, so it never receives a text operator such as `ILIKE`, or an emptiness operator, `IS_EMPTY` or `IS_NOT_EMPTY`, that it would drop or misread. A list, count, aggregate or collection read, or a by-ID read through a `get_by_id` scope, that uses another `QLOperator` value fails with `UNSUPPORTED_QUERY` (400): the runtime checks the client's arguments before middleware runs and the final arguments, with what middleware, scopes and joined scopes added, before it calls the adapter.
|
|
25
|
+
An adapter declares the filter operators it compiles as `queryFeatures: { operators }`, a list of `QLOperator` values that includes `EQ`, which a condition without an operator means; names this version does not know are ignored. `createRuntime()` reads it once and throws `INVALID_QUERY_FEATURES` (500) unless it is `undefined`, `null` or an object whose `operators` is an array of strings and whose optional `referenceEmptiness` is a boolean, `undefined` or `null`. Without a declaration the adapter supports the operators of 3.7.0, `EQ`, `NE`, `LT`, `LTE`, `GT`, `GTE`, `BTW`, `IN`, `NIN` and `LIKE`, so it never receives a text operator such as `ILIKE`, or an emptiness operator, `IS_EMPTY` or `IS_NOT_EMPTY`, that it would drop or misread. A list, count, aggregate or collection read, or a by-ID read through a `get_by_id` scope, that uses another `QLOperator` value fails with `UNSUPPORTED_QUERY` (400): the runtime checks the client's arguments before middleware runs and the final arguments, with what middleware, scopes and joined scopes added, before it calls the adapter. Unknown operators on actual filter predicates fail with `INVALID_FILTER_OPERATOR` (400) before the adapter is called. An adapter that receives the text operators must apply their rules: literal values, a `String` field or list, and `INVALID_FILTER_VALUE` (400) for a value that is not a string or contains U+0000. `createQueryPlan(models, entity, args, { operators })` validates them and rejects operators outside `operators` with `UNSUPPORTED_QUERY`. An adapter that declares `IS_EMPTY` and `IS_NOT_EMPTY` must compile their semantics on the queried record's own fields, where `IS_NOT_EMPTY` is the exact complement of `IS_EMPTY`: a `String` is empty when missing, `null` or `""`, any other scalar or enum when missing or `null`, a list when missing, `null` or `[]`, and an embedded object when missing, `null` or every stored member is empty. The runtime rejects a value other than `null` and a path inside a list before it calls the adapter, and also emptiness through references and collections unless the adapter declares `referenceEmptiness: true`, so the adapter receives conditions whose `value` may be absent and whose term or group `path` may be omitted, `null` or `""` to name the field itself; `createQueryPlan` plans them as `{ kind: 'predicate', path, operator, value: null }`, whose `path` may end at a scalar list, an embedded list or an embedded object. See [empty values](https://simtlix.github.io/simfinity.js/guide/queries.html#empty-values). An adapter that declares `referenceEmptiness: true` also receives emptiness conditions whose path crosses single references and ends at a reference or a collection, and embedded objects with reference and collection members. A client's such term or group condition carries the key `__scope: { id, scopes }`, where `scopes` holds, for each reference it crosses and each reference, collection or member it ends at or reads, keyed by the dotted path relative to the containing frame, the arguments that the target type's `find` scope produced, or `null` when it added nothing. The adapter must join a crossed reference only where that scope admits the target, and count at an end or member only the records that the scope admits, so that a record the caller may not see reads as a missing one, also on a later hop with a `null` scope; a condition without `__scope` reads stored data, as server conditions and unmarked clients on unscoped chains do. `createQueryPlan(models, entity, args, { operators, referenceEmptiness: true })` lifts each filled `__scope` into the predicate's `scopes`, planning each scope on its target type, and throws `DETACHED_CLIENT_FILTER` (500) for a `__scope` that lacks a path; its `referenceEmptiness` option defaults to `true` when `operators` is omitted and to `false` otherwise. See [references and collections](https://simtlix.github.io/simfinity.js/guide/queries.html#references-and-collections).
|
|
26
|
+
|
|
27
|
+
An adapter opts into collection frames with `queryFeatures.quantifiers: true` and `referenceEmptiness: true`; omission/null means false and invalid combinations give `INVALID_QUERY_FEATURES`. Every generated schema exposes `QLQuantifier`, `QLQuantifiedGroup`, group `ANY`/`ALL`/`NONE` lists and relation `quantifier`, with request-time capability gates. Collection shorthand becomes correlated `ANY`, with one child per group/prefix and no duplicated roots; explicit `ALL`/`NONE` use the same element-relative frame. Client scope keys are relative to their containing frame and apply `find` visibility before quantification; server conditions range over stored elements. `get_by_id` scopes are trusted, so `NE`, `NIN` and `EQ null` collection terms no longer admit childless records.
|
|
28
|
+
|
|
29
|
+
`createQueryPlan()` enables quantifiers by default without `operators`; with `operators`, declare capabilities explicitly. Its `QueryPredicate` includes `quantified`, element `scope`/`where`, scoped-reference `joins` and aggregate `anchor`. Compilers must honour these members and fail closed on incomplete metadata. `configureQueryLimits` adds `maxFrameNesting` (default 3), while `maxReferenceFrames` (10) counts compiled client units, even when unmarked, after all client 400 checks. Named views preserve separate client-argument and `where` budgets of that size. Every adapter rejects list sorts through collections, including count mode. See the [plugin contract](https://simtlix.github.io/simfinity.js/guide/sql-plugins.html#query-features) and [upgrade notes](https://simtlix.github.io/simfinity.js/resources/compatibility.html#collection-frames).
|
|
26
30
|
|
|
27
31
|
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()`.
|
|
28
32
|
|
|
@@ -42,6 +46,14 @@ Importing the runtime installs Simfinity's `__Field.extensions` introspection fi
|
|
|
42
46
|
|
|
43
47
|
`isVirtualField(field)` tells whether a GraphQL field is virtual, and `virtualFieldOptions(field)` returns its normalized options, `{ mcp }`, or `null` for any other field. Adapters that build storage from GraphQL types use them to skip virtual fields.
|
|
44
48
|
|
|
45
|
-
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.
|
|
49
|
+
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`, `QLQuantifier`, `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.
|
|
46
50
|
|
|
47
51
|
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.
|
|
52
|
+
|
|
53
|
+
## Optimistic concurrency
|
|
54
|
+
|
|
55
|
+
`extensions: { concurrency: { mode: 'optimistic' } }` opts an entity into the managed `version: RecordVersion!` field. Core exports `RecordVersion` and `MAX_RECORD_VERSION` (`9007199254740991`); import from the application's facade to share their identity. See the [concurrency guide](https://simtlix.github.io/simfinity.js/guide/concurrency.html).
|
|
56
|
+
|
|
57
|
+
An adapter advertises `writeFeatures: { optimisticConcurrency: true }`; `createRuntime()` snapshots/freezes this declaration and rejects invalid shapes with `INVALID_WRITE_FEATURES` (500). A protected schema rejects an adapter without support. `update(model, id, changes, session, { expectedVersion })` must atomically match ID/version, apply changes and set `expectedVersion + 1`, returning the updated record; `delete(model, id, session, { expectedVersion })` must atomically match both and return the deleted record. An update at the maximum skips the write. After no match, read the same cast key in the same session: return `null` if absent, report `VERSION_CONFLICT` if stale or `VERSION_EXHAUSTED` for an equal maximum on update, otherwise return `null`. MongoDB missing/null versions count as 1. Without `expectedVersion`, existing writes and call paths remain unchanged.
|
|
58
|
+
|
|
59
|
+
Support also requires `find(model, type, args, session, { requiredId, context })` to honor the session, required key and context for scoped visibility checks. Wrappers must forward every argument, including options. Core's update result diagnostic requires `expected + 1` and advancement beyond its immutable pre-read version; delete results must carry the expected version. These diagnostics raise `DATABASE_ERROR` for inconsistent results but cannot prove arbitrary wrappers kept the atomic predicate. They retain supplied-session ownership; adapters must preserve their own guarded-write abort semantics. The `get_by_id` scope defers stale/exhausted errors for hidden callers until write hooks authorize the operation, but malformed stored versions can fail before hooks. Protected writes always perform their required pre-read, regardless of `readByIdIsInert`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"description": "Driver-free GraphQL runtime and model metadata for Simfinity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"./internal/object-id": "./src/auth/object-id.js",
|
|
55
55
|
"./internal/query-operators": "./src/query-operators.js",
|
|
56
|
+
"./internal/record-version": "./src/record-version.js",
|
|
56
57
|
"./internal/relation-storage": "./src/relation-storage.js",
|
|
57
58
|
"./plugins": {
|
|
58
59
|
"types": "./types/plugins.d.ts",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GraphQLEnumType } from 'graphql';
|
|
2
|
+
|
|
3
|
+
const QLQuantifier = new GraphQLEnumType({
|
|
4
|
+
name: 'QLQuantifier',
|
|
5
|
+
values: {
|
|
6
|
+
ANY: { value: 'ANY' },
|
|
7
|
+
ALL: { value: 'ALL' },
|
|
8
|
+
NONE: { value: 'NONE' },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const QUANTIFIED_LISTS = Object.freeze(QLQuantifier.getValues().map(({ value }) => value));
|
|
13
|
+
export const QUANTIFIERS = new Set(QUANTIFIED_LISTS);
|
|
14
|
+
|
|
15
|
+
// Shared by the collector and the quantified-group validator. `path` is allowed only on a
|
|
16
|
+
// quantified member; reserved metadata is interpreted by its owning phase.
|
|
17
|
+
export const GROUP_KEYS = new Set([
|
|
18
|
+
'conditions', 'AND', 'OR', ...QUANTIFIED_LISTS, 'path', '__scope', '__implicit', '__anchor',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export default QLQuantifier;
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { describeModels } from './metadata.js';
|
|
2
2
|
export { isVirtualField, virtualFieldOptions } from './relation-storage.js';
|
|
3
|
+
export { MAX_RECORD_VERSION, RecordVersion } from './record-version.js';
|
|
3
4
|
export { default as SimfinityError } from './errors/simfinity.error.js';
|
|
4
5
|
export { createValidatedScalar } from './scalars/factory.js';
|
|
5
6
|
export {
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
} from './runtime.js';
|
|
10
11
|
export { createQueryPlan, resolveModelPath } from './query-plan.js';
|
|
11
12
|
export { default as QLOperator } from './const/QLOperator.js';
|
|
13
|
+
export { default as QLQuantifier } from './const/QLQuantifier.js';
|
|
12
14
|
export { default as QLSort } from './const/QLSort.js';
|
|
13
15
|
export { default as QLValue } from './const/QLValue.js';
|
|
14
16
|
export { configureQueryLimits, paginationStages } from './query-limits.js';
|
package/src/metadata.js
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
assertVirtualConnection, assertVirtualDeclaration, assertVirtualIndexes, assertVirtualStateField,
|
|
7
7
|
} from './virtual-fields.js';
|
|
8
8
|
import { assertPlainConnectionField } from './relation-storage.js';
|
|
9
|
+
import {
|
|
10
|
+
isProtectedType, isVersionField, recordVersionFieldMisuse, versionConnectionCollision, versionEmbeddedMisuse,
|
|
11
|
+
versionFieldCollision,
|
|
12
|
+
} from './record-version.js';
|
|
9
13
|
|
|
10
14
|
const invalid = (message) => {
|
|
11
15
|
throw new SimfinityError(message, 'INVALID_MODEL', 400);
|
|
@@ -62,10 +66,20 @@ const scalarDescription = (type) => {
|
|
|
62
66
|
return { scalar };
|
|
63
67
|
};
|
|
64
68
|
|
|
69
|
+
// The version field of a protected entity, described the same way whether the type carries Simfinity's
|
|
70
|
+
// injected field or only declares the extension and createSchema() has not injected it yet.
|
|
71
|
+
const versionDescription = () => ({
|
|
72
|
+
name: 'version', storageName: 'version', kind: 'scalar', scalar: 'Version', required: true,
|
|
73
|
+
list: false, itemRequired: false, unique: false, readOnly: true, version: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
65
76
|
/**
|
|
66
77
|
* Describe persistent entities without importing a database driver or mutating GraphQL types.
|
|
67
78
|
* Virtual fields (`extensions.virtual`) store nothing, so they are left out of every entity and
|
|
68
79
|
* embedded type, and their result types are neither described nor required to declare a relation.
|
|
80
|
+
* An entity that declares optimistic concurrency (`extensions.concurrency`), or already carries its
|
|
81
|
+
* injected field, ends with the version field. A field that would collide with it, any other field
|
|
82
|
+
* typed RecordVersion, and a protected type used as an embedded value throw INVALID_MODEL.
|
|
69
83
|
*/
|
|
70
84
|
export const describeModels = (registrations) => {
|
|
71
85
|
if (!Array.isArray(registrations)) invalid('registrations must be an array');
|
|
@@ -91,10 +105,17 @@ export const describeModels = (registrations) => {
|
|
|
91
105
|
const fieldsOf = (type, ancestors, path, embedded = false) => {
|
|
92
106
|
if (ancestors.includes(type)) invalid(`Embedded cycle at ${path}`);
|
|
93
107
|
registerType(type);
|
|
108
|
+
const isProtected = isProtectedType(type);
|
|
109
|
+
if (isProtected && embedded) invalid(versionEmbeddedMisuse(type.name, path));
|
|
110
|
+
const collision = isProtected ? versionFieldCollision(type) : null;
|
|
111
|
+
if (collision) invalid(collision);
|
|
112
|
+
const misuse = recordVersionFieldMisuse(type);
|
|
113
|
+
if (misuse) invalid(misuse);
|
|
94
114
|
const stored = Object.values(type.getFields()).filter((field) => !assertVirtualDeclaration(
|
|
95
115
|
type.name, field.name, field, () => typeof field.resolve === 'function',
|
|
96
116
|
));
|
|
97
117
|
const fields = stored.map((field) => {
|
|
118
|
+
if (isVersionField(field)) return versionDescription();
|
|
98
119
|
const { named, ...wrappers } = unwrap(field.type);
|
|
99
120
|
const extensions = field.extensions || {};
|
|
100
121
|
const descriptor = {
|
|
@@ -117,12 +138,15 @@ export const describeModels = (registrations) => {
|
|
|
117
138
|
assertPlainConnectionField(type.name, field.name, field);
|
|
118
139
|
addEntity(named);
|
|
119
140
|
if (wrappers.list) {
|
|
141
|
+
const connectionCollision = versionConnectionCollision(type, field.name, field);
|
|
142
|
+
if (connectionCollision) invalid(connectionCollision);
|
|
120
143
|
if (!relation.connectionField) invalid(`${path}.${field.name} requires a child connectionField`);
|
|
121
144
|
if (embedded) invalid(`Non-embedded collections inside embedded objects are not supported: ${path}.${field.name}`);
|
|
122
145
|
return { ...descriptor, kind: 'collection', target: named.name, connectionField: relation.connectionField };
|
|
123
146
|
}
|
|
124
147
|
return { ...descriptor, kind: 'reference', target: named.name, storageName: relation.connectionField || field.name };
|
|
125
148
|
});
|
|
149
|
+
if (isProtected && !fields.some((item) => item.version)) fields.push(versionDescription());
|
|
126
150
|
const storage = new Set();
|
|
127
151
|
for (const field of fields.filter((item) => item.kind !== 'collection')) {
|
|
128
152
|
if (storage.has(field.storageName)) invalid(`Storage collision at ${path}.${field.storageName}`);
|
package/src/query-emptiness.js
CHANGED
|
@@ -45,7 +45,7 @@ const LISTS = { embeddedList: 'an embedded list', collection: 'a collection' };
|
|
|
45
45
|
// collection names its `target` type, and a collection the child's storage column that links it,
|
|
46
46
|
// `connectionField`, derived as MongoDB derives it; without a declared connectionField a collection
|
|
47
47
|
// reads through its own field name.
|
|
48
|
-
const graphqlStep = (name, field) => {
|
|
48
|
+
export const graphqlStep = (name, field) => {
|
|
49
49
|
const type = getNamedType(field.type);
|
|
50
50
|
const list = !!getListShape(field.type);
|
|
51
51
|
if (isInterfaceType(type) || isUnionType(type)) return { name, kind: 'abstract' };
|
|
@@ -69,7 +69,7 @@ const graphqlMembers = (type) => Object.entries(type.getFields())
|
|
|
69
69
|
// The same classification of a describeModels field description: inferred inverse references are ID
|
|
70
70
|
// scalars, as GraphQL exposes them. Descriptions hold no interface or union fields, and describeModels
|
|
71
71
|
// has already normalized a collection's connectionField to the child's storage column.
|
|
72
|
-
const modelStep = (field) => {
|
|
72
|
+
export const modelStep = (field) => {
|
|
73
73
|
if (field.kind === 'embedded') return { name: field.name, kind: field.list ? 'embeddedList' : 'embedded', node: field.fields ?? [] };
|
|
74
74
|
if (field.kind === 'collection') return { name: field.name, kind: 'collection', target: field.target, connectionField: field.connectionField };
|
|
75
75
|
if (field.kind === 'reference' && !field.inferred) return { name: field.name, kind: 'reference', target: field.target };
|