@kubun/graphql 0.17.0 → 0.17.2
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/lib/context.d.ts +22 -2
- package/lib/relay.d.ts +93 -0
- package/lib/relay.js +197 -0
- package/lib/schema.d.ts +9 -8
- package/lib/schema.js +60 -11
- package/lib/types.d.ts +3 -27
- package/package.json +4 -5
package/lib/context.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AggregateHaving, AggregateSpec, AggregateValues, DocumentData, Doc
|
|
|
3
3
|
import type { SearchDocumentResult, SearchDocumentsParams, StoredAccessDefaultRow, ViewerReadAccess } from '@kubun/store-graph';
|
|
4
4
|
import type { Tracer } from '@sozai/otel';
|
|
5
5
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
6
|
-
import type { Connection, Edge } from '
|
|
6
|
+
import type { Connection, Edge } from './relay.js';
|
|
7
7
|
import type { AllConnectionArguments, ModelAccessDefaults, UpdateMutationInput } from './types.js';
|
|
8
8
|
/**
|
|
9
9
|
* Event data for document save notifications used by subscriptions.
|
|
@@ -90,7 +90,7 @@ export type AggregateScope = {
|
|
|
90
90
|
};
|
|
91
91
|
/**
|
|
92
92
|
* Connection value returned by {@link QueryContext.resolveConnection}. Extends
|
|
93
|
-
* the
|
|
93
|
+
* the Relay connection (edges/pageInfo are untouched) with the query
|
|
94
94
|
* scope so the `aggregates` field can resolve consistently with the page.
|
|
95
95
|
*/
|
|
96
96
|
export type ScopedConnection = Connection<DocumentNode> & {
|
|
@@ -216,6 +216,13 @@ export type SetMutationParams = {
|
|
|
216
216
|
* viewer) owns. Omitted means owner = signer (the ops-level owner).
|
|
217
217
|
*/
|
|
218
218
|
owner?: string;
|
|
219
|
+
/**
|
|
220
|
+
* Fetch-order token for a merge-policy-aware SET, forwarded to the engine's
|
|
221
|
+
* outcome-bearing seam. A model that declares a `providerMergePolicy` rejects
|
|
222
|
+
* a tokenless write; ordinary models ignore it. Optional, so non-connector
|
|
223
|
+
* callers stay source-compatible.
|
|
224
|
+
*/
|
|
225
|
+
sourceFetchHLC?: string;
|
|
219
226
|
};
|
|
220
227
|
export type UpdateMutationParams = {
|
|
221
228
|
input: UpdateMutationInput;
|
|
@@ -249,6 +256,19 @@ export type SetDocumentAccessOverrideParams = {
|
|
|
249
256
|
export type MutationContext = {
|
|
250
257
|
executeCreateMutation(params: CreateMutationParams): Promise<DocumentNode>;
|
|
251
258
|
executeSetMutation(params: SetMutationParams): Promise<DocumentNode>;
|
|
259
|
+
/**
|
|
260
|
+
* Outcome-bearing SET, present only on the engine's signing context. The
|
|
261
|
+
* client and service graph contexts do not implement it, so a consumer that
|
|
262
|
+
* needs the merge outcome asserts presence with a runtime null check and
|
|
263
|
+
* fails closed (throws) when it is absent, rather than mis-committing through
|
|
264
|
+
* the plain SET seam. `document` is null when a gate dropped the projection.
|
|
265
|
+
*/
|
|
266
|
+
executeSetMutationWithOutcome?(params: SetMutationParams): Promise<{
|
|
267
|
+
document: DocumentNode | null;
|
|
268
|
+
outcome?: 'applied' | 'dominated' | 'nonconforming';
|
|
269
|
+
candidateDigest?: string;
|
|
270
|
+
winnerDigest?: string;
|
|
271
|
+
}>;
|
|
252
272
|
executeUpdateMutation(params: UpdateMutationParams): Promise<DocumentNode>;
|
|
253
273
|
executeRemoveMutation(params: RemoveMutationParams): Promise<void>;
|
|
254
274
|
executeSetModelAccessDefaults(params: SetModelAccessDefaultsParams): Promise<ModelAccessDefaults>;
|
package/lib/relay.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type GraphQLFieldConfig, type GraphQLFieldConfigArgumentMap, type GraphQLFieldExtensions, type GraphQLFieldResolver, type GraphQLInputFieldConfig, GraphQLInterfaceType, type GraphQLNamedOutputType, GraphQLNonNull, GraphQLObjectType, type GraphQLResolveInfo, type GraphQLTypeResolver, type ThunkObjMap } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Internal reimplementation of the small slice of `graphql-relay` this package
|
|
4
|
+
* uses (connection args/types, `Node` interface, and Relay-style mutations).
|
|
5
|
+
* The emitted GraphQL types are byte-identical to `graphql-relay@0.10`, which
|
|
6
|
+
* the schema snapshot tests verify.
|
|
7
|
+
*/
|
|
8
|
+
/** A type alias for cursors in this implementation. */
|
|
9
|
+
export type ConnectionCursor = string;
|
|
10
|
+
/** A type describing the arguments a connection field receives in GraphQL. */
|
|
11
|
+
export type ConnectionArguments = {
|
|
12
|
+
before?: ConnectionCursor | null;
|
|
13
|
+
after?: ConnectionCursor | null;
|
|
14
|
+
first?: number | null;
|
|
15
|
+
last?: number | null;
|
|
16
|
+
};
|
|
17
|
+
/** A type designed to be exposed as a `Connection` over GraphQL. */
|
|
18
|
+
export type Connection<T> = {
|
|
19
|
+
edges: Array<Edge<T>>;
|
|
20
|
+
pageInfo: PageInfo;
|
|
21
|
+
};
|
|
22
|
+
/** A type designed to be exposed as an `Edge` over GraphQL. */
|
|
23
|
+
export type Edge<T> = {
|
|
24
|
+
node: T;
|
|
25
|
+
cursor: ConnectionCursor;
|
|
26
|
+
};
|
|
27
|
+
/** A type designed to be exposed as `PageInfo` over GraphQL. */
|
|
28
|
+
export type PageInfo = {
|
|
29
|
+
startCursor: ConnectionCursor | null;
|
|
30
|
+
endCursor: ConnectionCursor | null;
|
|
31
|
+
hasPreviousPage: boolean;
|
|
32
|
+
hasNextPage: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Args to include on a field whose return type is a connection type with
|
|
36
|
+
* forward pagination.
|
|
37
|
+
*/
|
|
38
|
+
export declare const forwardConnectionArgs: GraphQLFieldConfigArgumentMap;
|
|
39
|
+
/**
|
|
40
|
+
* Args to include on a field whose return type is a connection type with
|
|
41
|
+
* backward pagination.
|
|
42
|
+
*/
|
|
43
|
+
export declare const backwardConnectionArgs: GraphQLFieldConfigArgumentMap;
|
|
44
|
+
/**
|
|
45
|
+
* Args to include on a field whose return type is a connection type with
|
|
46
|
+
* bidirectional pagination.
|
|
47
|
+
*/
|
|
48
|
+
export declare const connectionArgs: GraphQLFieldConfigArgumentMap;
|
|
49
|
+
export type ConnectionConfig = {
|
|
50
|
+
name?: string;
|
|
51
|
+
nodeType: GraphQLNamedOutputType | GraphQLNonNull<GraphQLNamedOutputType>;
|
|
52
|
+
resolveNode?: GraphQLFieldResolver<any, any>;
|
|
53
|
+
resolveCursor?: GraphQLFieldResolver<any, any>;
|
|
54
|
+
edgeFields?: ThunkObjMap<GraphQLFieldConfig<any, any>>;
|
|
55
|
+
connectionFields?: ThunkObjMap<GraphQLFieldConfig<any, any>>;
|
|
56
|
+
};
|
|
57
|
+
export type GraphQLConnectionDefinitions = {
|
|
58
|
+
edgeType: GraphQLObjectType;
|
|
59
|
+
connectionType: GraphQLObjectType;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Returns a GraphQLObjectType for a connection with the given name, and whose
|
|
63
|
+
* nodes are of the specified type.
|
|
64
|
+
*/
|
|
65
|
+
export declare function connectionDefinitions(config: ConnectionConfig): GraphQLConnectionDefinitions;
|
|
66
|
+
export type GraphQLNodeDefinitions<TContext> = {
|
|
67
|
+
nodeInterface: GraphQLInterfaceType;
|
|
68
|
+
nodeField: GraphQLFieldConfig<unknown, TContext>;
|
|
69
|
+
nodesField: GraphQLFieldConfig<unknown, TContext>;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Given a function to map from an ID to an underlying object, and a function to
|
|
73
|
+
* map from an underlying object to its concrete GraphQLObjectType, constructs a
|
|
74
|
+
* `Node` interface plus `node`/`nodes` root field configs.
|
|
75
|
+
*/
|
|
76
|
+
export declare function nodeDefinitions<TContext>(fetchById: (id: string, context: TContext, info: GraphQLResolveInfo) => unknown, typeResolver?: GraphQLTypeResolver<unknown, TContext>): GraphQLNodeDefinitions<TContext>;
|
|
77
|
+
type MutationFn<TInput, TOutput, TContext> = (object: TInput, ctx: TContext, info: GraphQLResolveInfo) => TOutput;
|
|
78
|
+
export type MutationConfig<TInput = any, TOutput = unknown, TContext = unknown> = {
|
|
79
|
+
name: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
deprecationReason?: string;
|
|
82
|
+
extensions?: GraphQLFieldExtensions<unknown, unknown>;
|
|
83
|
+
inputFields: ThunkObjMap<GraphQLInputFieldConfig>;
|
|
84
|
+
outputFields: ThunkObjMap<GraphQLFieldConfig<TOutput, TContext>>;
|
|
85
|
+
mutateAndGetPayload: MutationFn<TInput, Promise<TOutput> | TOutput, TContext>;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Returns a GraphQLFieldConfig for the mutation described by the provided
|
|
89
|
+
* config, wiring the Relay `clientMutationId` round-trip through a generated
|
|
90
|
+
* `${name}Input`/`${name}Payload` pair.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mutationWithClientMutationId<TInput = any, TOutput = unknown, TContext = unknown>(config: MutationConfig<TInput, TOutput, TContext>): GraphQLFieldConfig<unknown, TContext>;
|
|
93
|
+
export {};
|
package/lib/relay.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { GraphQLBoolean, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, getNamedType, resolveObjMapThunk } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Args to include on a field whose return type is a connection type with
|
|
4
|
+
* forward pagination.
|
|
5
|
+
*/ export const forwardConnectionArgs = Object.freeze({
|
|
6
|
+
after: {
|
|
7
|
+
type: GraphQLString,
|
|
8
|
+
description: 'Returns the items in the list that come after the specified cursor.'
|
|
9
|
+
},
|
|
10
|
+
first: {
|
|
11
|
+
type: GraphQLInt,
|
|
12
|
+
description: 'Returns the first n items from the list.'
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Args to include on a field whose return type is a connection type with
|
|
17
|
+
* backward pagination.
|
|
18
|
+
*/ export const backwardConnectionArgs = Object.freeze({
|
|
19
|
+
before: {
|
|
20
|
+
type: GraphQLString,
|
|
21
|
+
description: 'Returns the items in the list that come before the specified cursor.'
|
|
22
|
+
},
|
|
23
|
+
last: {
|
|
24
|
+
type: GraphQLInt,
|
|
25
|
+
description: 'Returns the last n items from the list.'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Args to include on a field whose return type is a connection type with
|
|
30
|
+
* bidirectional pagination.
|
|
31
|
+
*/ export const connectionArgs = {
|
|
32
|
+
...forwardConnectionArgs,
|
|
33
|
+
...backwardConnectionArgs
|
|
34
|
+
};
|
|
35
|
+
/** The common page info type shared by every connection. */ const pageInfoType = new GraphQLObjectType({
|
|
36
|
+
name: 'PageInfo',
|
|
37
|
+
description: 'Information about pagination in a connection.',
|
|
38
|
+
fields: ()=>({
|
|
39
|
+
hasNextPage: {
|
|
40
|
+
type: new GraphQLNonNull(GraphQLBoolean),
|
|
41
|
+
description: 'When paginating forwards, are there more items?'
|
|
42
|
+
},
|
|
43
|
+
hasPreviousPage: {
|
|
44
|
+
type: new GraphQLNonNull(GraphQLBoolean),
|
|
45
|
+
description: 'When paginating backwards, are there more items?'
|
|
46
|
+
},
|
|
47
|
+
startCursor: {
|
|
48
|
+
type: GraphQLString,
|
|
49
|
+
description: 'When paginating backwards, the cursor to continue.'
|
|
50
|
+
},
|
|
51
|
+
endCursor: {
|
|
52
|
+
type: GraphQLString,
|
|
53
|
+
description: 'When paginating forwards, the cursor to continue.'
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Returns a GraphQLObjectType for a connection with the given name, and whose
|
|
59
|
+
* nodes are of the specified type.
|
|
60
|
+
*/ export function connectionDefinitions(config) {
|
|
61
|
+
const { nodeType } = config;
|
|
62
|
+
const name = config.name ?? getNamedType(nodeType).name;
|
|
63
|
+
const edgeType = new GraphQLObjectType({
|
|
64
|
+
name: `${name}Edge`,
|
|
65
|
+
description: 'An edge in a connection.',
|
|
66
|
+
fields: ()=>({
|
|
67
|
+
node: {
|
|
68
|
+
type: nodeType,
|
|
69
|
+
resolve: config.resolveNode,
|
|
70
|
+
description: 'The item at the end of the edge'
|
|
71
|
+
},
|
|
72
|
+
cursor: {
|
|
73
|
+
type: new GraphQLNonNull(GraphQLString),
|
|
74
|
+
resolve: config.resolveCursor,
|
|
75
|
+
description: 'A cursor for use in pagination'
|
|
76
|
+
},
|
|
77
|
+
...resolveObjMapThunk(config.edgeFields ?? {})
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
const connectionType = new GraphQLObjectType({
|
|
81
|
+
name: `${name}Connection`,
|
|
82
|
+
description: 'A connection to a list of items.',
|
|
83
|
+
fields: ()=>({
|
|
84
|
+
pageInfo: {
|
|
85
|
+
type: new GraphQLNonNull(pageInfoType),
|
|
86
|
+
description: 'Information to aid in pagination.'
|
|
87
|
+
},
|
|
88
|
+
edges: {
|
|
89
|
+
type: new GraphQLList(edgeType),
|
|
90
|
+
description: 'A list of edges.'
|
|
91
|
+
},
|
|
92
|
+
...resolveObjMapThunk(config.connectionFields ?? {})
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
edgeType,
|
|
97
|
+
connectionType
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Given a function to map from an ID to an underlying object, and a function to
|
|
102
|
+
* map from an underlying object to its concrete GraphQLObjectType, constructs a
|
|
103
|
+
* `Node` interface plus `node`/`nodes` root field configs.
|
|
104
|
+
*/ export function nodeDefinitions(fetchById, typeResolver) {
|
|
105
|
+
const nodeInterface = new GraphQLInterfaceType({
|
|
106
|
+
name: 'Node',
|
|
107
|
+
description: 'An object with an ID',
|
|
108
|
+
fields: ()=>({
|
|
109
|
+
id: {
|
|
110
|
+
type: new GraphQLNonNull(GraphQLID),
|
|
111
|
+
description: 'The id of the object.'
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
resolveType: typeResolver
|
|
115
|
+
});
|
|
116
|
+
const nodeField = {
|
|
117
|
+
description: 'Fetches an object given its ID',
|
|
118
|
+
type: nodeInterface,
|
|
119
|
+
args: {
|
|
120
|
+
id: {
|
|
121
|
+
type: new GraphQLNonNull(GraphQLID),
|
|
122
|
+
description: 'The ID of an object'
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
resolve: (_obj, { id }, context, info)=>fetchById(id, context, info)
|
|
126
|
+
};
|
|
127
|
+
const nodesField = {
|
|
128
|
+
description: 'Fetches objects given their IDs',
|
|
129
|
+
type: new GraphQLNonNull(new GraphQLList(nodeInterface)),
|
|
130
|
+
args: {
|
|
131
|
+
ids: {
|
|
132
|
+
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))),
|
|
133
|
+
description: 'The IDs of objects'
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
resolve: (_obj, { ids }, context, info)=>ids.map((id)=>fetchById(id, context, info))
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
nodeInterface,
|
|
140
|
+
nodeField,
|
|
141
|
+
nodesField
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function isPromiseLike(value) {
|
|
145
|
+
return typeof value?.then === 'function';
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns a GraphQLFieldConfig for the mutation described by the provided
|
|
149
|
+
* config, wiring the Relay `clientMutationId` round-trip through a generated
|
|
150
|
+
* `${name}Input`/`${name}Payload` pair.
|
|
151
|
+
*/ export function mutationWithClientMutationId(config) {
|
|
152
|
+
const { name, inputFields, outputFields, mutateAndGetPayload } = config;
|
|
153
|
+
const outputType = new GraphQLObjectType({
|
|
154
|
+
name: `${name}Payload`,
|
|
155
|
+
fields: ()=>({
|
|
156
|
+
...resolveObjMapThunk(outputFields),
|
|
157
|
+
clientMutationId: {
|
|
158
|
+
type: GraphQLString
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
const inputType = new GraphQLInputObjectType({
|
|
163
|
+
name: `${name}Input`,
|
|
164
|
+
fields: ()=>({
|
|
165
|
+
...resolveObjMapThunk(inputFields),
|
|
166
|
+
clientMutationId: {
|
|
167
|
+
type: GraphQLString
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
type: outputType,
|
|
173
|
+
description: config.description,
|
|
174
|
+
deprecationReason: config.deprecationReason,
|
|
175
|
+
extensions: config.extensions,
|
|
176
|
+
args: {
|
|
177
|
+
input: {
|
|
178
|
+
type: new GraphQLNonNull(inputType)
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
resolve: (_source, { input }, context, info)=>{
|
|
182
|
+
const { clientMutationId } = input;
|
|
183
|
+
const injectClientMutationId = (data)=>{
|
|
184
|
+
if (typeof data === 'object' && data !== null) {
|
|
185
|
+
;
|
|
186
|
+
data.clientMutationId = clientMutationId;
|
|
187
|
+
}
|
|
188
|
+
return data;
|
|
189
|
+
};
|
|
190
|
+
const payload = mutateAndGetPayload(input, context, info);
|
|
191
|
+
if (isPromiseLike(payload)) {
|
|
192
|
+
return payload.then((value)=>injectClientMutationId(value));
|
|
193
|
+
}
|
|
194
|
+
return injectClientMutationId(payload);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
package/lib/schema.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AttachmentID, DocumentModelID } from '@kubun/id';
|
|
2
2
|
import type { AggregateHaving, AggregateSpec, ArrayModel, DocumentData, DocumentModel, DocumentModelSchema, DocumentModelsRecord, DocumentNode, GroupedAggregateOrderTerm, ObjectModel, OrderByDirection as OrderByDirectionValue, ReferencesRecordModel, ScalarModel, StringCustomModel, StringEnumModel, StringFormatModel, StringModel, TypeModel } from '@kubun/protocol';
|
|
3
|
-
import { type FieldNode, GraphQLEnumType, type GraphQLFieldConfig, type GraphQLFieldConfigMap, type GraphQLFieldResolver, GraphQLInputObjectType, type GraphQLInputType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, type GraphQLOutputType, type GraphQLResolveInfo, type GraphQLScalarType, GraphQLSchema, type SelectionNode } from 'graphql';
|
|
3
|
+
import { type FieldNode, GraphQLEnumType, type GraphQLFieldConfig, type GraphQLFieldConfigMap, type GraphQLFieldResolver, GraphQLInputObjectType, type GraphQLInputType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, type GraphQLNullableInputType, type GraphQLNullableOutputType, GraphQLObjectType, type GraphQLOutputType, type GraphQLResolveInfo, type GraphQLScalarType, GraphQLSchema, type SelectionNode } from 'graphql';
|
|
4
4
|
import type { Context } from './context.js';
|
|
5
5
|
export declare function toDocumentModelID(id: DocumentModelID | string): DocumentModelID;
|
|
6
6
|
export declare function getGlobalID(id: DocumentModelID | string, globalID: DocumentModelID | string): DocumentModelID;
|
|
7
|
-
export declare function toList(type:
|
|
8
|
-
export declare function toOuputList(type:
|
|
7
|
+
export declare function toList(type: GraphQLNullableInputType): GraphQLList<GraphQLNonNull<GraphQLNullableInputType>>;
|
|
8
|
+
export declare function toOuputList(type: GraphQLNullableOutputType): GraphQLList<GraphQLNonNull<GraphQLNullableOutputType>>;
|
|
9
9
|
export declare const PatchOperation: GraphQLInputObjectType;
|
|
10
10
|
export type GraphQLNodeDefinitions = {
|
|
11
11
|
nodeInterface: GraphQLInterfaceType;
|
|
@@ -94,23 +94,24 @@ export declare class SchemaBuilder {
|
|
|
94
94
|
_relationsTo: Record<string, RelationFields>;
|
|
95
95
|
_extensionSDL: string | undefined;
|
|
96
96
|
_extensionResolvers: ExtensionResolvers | undefined;
|
|
97
|
+
_skippedTypeExtensions: Set<string>;
|
|
97
98
|
_stubExtensionResolvers: boolean;
|
|
98
99
|
_pluginNodeResolvers: Record<string, {
|
|
99
100
|
resolve: (localID: string, ctx: Context) => Promise<unknown> | unknown;
|
|
100
101
|
}>;
|
|
101
102
|
_subscriptions: GraphQLFieldConfigMap<unknown, Context>;
|
|
102
|
-
_types: Record<string,
|
|
103
|
+
_types: Record<string, GraphQLNullableOutputType>;
|
|
103
104
|
constructor(params: SchemaBuilderParams);
|
|
104
105
|
_resolveModelIdentifier(identifier: string): string;
|
|
105
106
|
_resolveModelIDsToInclude(): Set<string>;
|
|
106
107
|
getModelIDs(id: string | null): Array<string>;
|
|
107
108
|
build(): GraphQLSchema;
|
|
108
109
|
_getDefinitions(): SharedDefinitions;
|
|
109
|
-
_getReference(path: string, parentName?: string):
|
|
110
|
+
_getReference(path: string, parentName?: string): GraphQLNullableOutputType;
|
|
110
111
|
_getReferenceSchema(id: string): TypeModel;
|
|
111
112
|
_getInputObject(key: string): GraphQLInputObjectType;
|
|
112
113
|
_getType(key: string): GraphQLOutputType;
|
|
113
|
-
_getReferenceInput(path: string, isPartial: boolean, parentName?: string):
|
|
114
|
+
_getReferenceInput(path: string, isPartial: boolean, parentName?: string): GraphQLNullableInputType;
|
|
114
115
|
_buildSharedDefinitions(): SharedDefinitions;
|
|
115
116
|
_buildDocument(id: string): GraphQLObjectType<DocumentNode, Context> | GraphQLInterfaceType;
|
|
116
117
|
/**
|
|
@@ -191,9 +192,9 @@ export declare class SchemaBuilder {
|
|
|
191
192
|
_buildObjectFilterInput(id: string, schema: DocumentModelSchema | ObjectModel, parentName?: string, isDocument?: boolean): GraphQLInputObjectType;
|
|
192
193
|
_buildObjectOrderByInput(id: string, schema: DocumentModelSchema | ObjectModel, parentName?: string, isDocument?: boolean): GraphQLInputObjectType;
|
|
193
194
|
_buildSetInputObjectType(id: string, withFields: Array<string>, relationField?: string): GraphQLInputObjectType;
|
|
194
|
-
_buildReference(id: string, schema: TypeModel, parentName?: string):
|
|
195
|
+
_buildReference(id: string, schema: TypeModel, parentName?: string): GraphQLNullableOutputType;
|
|
195
196
|
_buildScalar(id: string, schema: ScalarModel, parentName?: string): GraphQLScalarType;
|
|
196
|
-
_buildArray(schema: ArrayModel, parentName?: string): GraphQLList<GraphQLNonNull<
|
|
197
|
+
_buildArray(schema: ArrayModel, parentName?: string): GraphQLList<GraphQLNonNull<GraphQLNullableOutputType>>;
|
|
197
198
|
_buildObject(id: string, schema: ObjectModel, parentName?: string): GraphQLObjectType;
|
|
198
199
|
_buildObjectFields(id: string, schema: DocumentModelSchema | ObjectModel, parentName?: string): GraphQLFieldConfigMap<Record<string, unknown>, Context>;
|
|
199
200
|
_buildNestedAttachmentField(key: string, fieldRef: string): GraphQLFieldConfig<Record<string, unknown>, Context> | null;
|
package/lib/schema.js
CHANGED
|
@@ -2,9 +2,9 @@ import { AttachmentID, DocumentID, DocumentModelID, decodeID, PluginNodeID } fro
|
|
|
2
2
|
import { REFERENCE_PREFIX } from '@kubun/protocol';
|
|
3
3
|
import { pascalCase } from 'change-case';
|
|
4
4
|
import { extendSchema, GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, isEnumType, isListType, isNonNullType, isObjectType, isScalarType, Kind, parse as parseGraphQL } from 'graphql';
|
|
5
|
-
import { connectionArgs, connectionDefinitions, mutationWithClientMutationId, nodeDefinitions } from 'graphql-relay';
|
|
6
5
|
import { GraphQLDateTimeISO, GraphQLDID, GraphQLDuration, GraphQLEmailAddress, GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
|
|
7
6
|
import { GraphQLAttachmentID, GraphQLCredentialEntryID, GraphQLDocID } from './identifiers.js';
|
|
7
|
+
import { connectionArgs, connectionDefinitions, mutationWithClientMutationId, nodeDefinitions } from './relay.js';
|
|
8
8
|
import { GraphQLURL } from './scalars.js';
|
|
9
9
|
import { decodeSearchCursor, encodeSearchCursor } from './search-cursor.js';
|
|
10
10
|
import { getUniqueDocValue, validateAccessIdentifiers } from './utils.js';
|
|
@@ -35,6 +35,8 @@ import { getUniqueDocValue, validateAccessIdentifiers } from './utils.js';
|
|
|
35
35
|
// Object types: return empty object — sub-fields use their own stub resolvers
|
|
36
36
|
return {};
|
|
37
37
|
}
|
|
38
|
+
// Marks an extension of a model type that applies only when the graph has it.
|
|
39
|
+
const IF_TYPE_EXISTS = 'ifTypeExists';
|
|
38
40
|
const STRING_FORMATS = {
|
|
39
41
|
date: GraphQLString,
|
|
40
42
|
time: GraphQLString,
|
|
@@ -388,6 +390,9 @@ export class SchemaBuilder {
|
|
|
388
390
|
_relationsTo = {};
|
|
389
391
|
_extensionSDL;
|
|
390
392
|
_extensionResolvers;
|
|
393
|
+
// Types whose `@ifTypeExists` extension was left out: their `typeFields`
|
|
394
|
+
// are skipped rather than rejected.
|
|
395
|
+
_skippedTypeExtensions = new Set();
|
|
391
396
|
_stubExtensionResolvers;
|
|
392
397
|
_pluginNodeResolvers = {};
|
|
393
398
|
_subscriptions = {};
|
|
@@ -561,7 +566,7 @@ export class SchemaBuilder {
|
|
|
561
566
|
};
|
|
562
567
|
}
|
|
563
568
|
// Add top-level search query across all models
|
|
564
|
-
// Get the PageInfo type from an existing connection (shared
|
|
569
|
+
// Get the PageInfo type from an existing connection (shared across all connections)
|
|
565
570
|
const firstModelID = [
|
|
566
571
|
...this._includedModelIDs
|
|
567
572
|
][0];
|
|
@@ -968,10 +973,14 @@ export class SchemaBuilder {
|
|
|
968
973
|
// Apply SDL extensions
|
|
969
974
|
if (this._extensionSDL != null) {
|
|
970
975
|
const typesBefore = new Set(Object.keys(schema.getTypeMap()));
|
|
971
|
-
|
|
976
|
+
const fieldsBefore = new Map();
|
|
977
|
+
for (const [typeName, type] of Object.entries(schema.getTypeMap())){
|
|
978
|
+
if (isObjectType(type)) fieldsBefore.set(typeName, new Set(Object.keys(type.getFields())));
|
|
979
|
+
}
|
|
980
|
+
schema = extendSchema(schema, this.#applicableExtensions(schema, this._extensionSDL));
|
|
972
981
|
// Stub extension resolvers for client-side pre-execution
|
|
973
982
|
if (this._stubExtensionResolvers) {
|
|
974
|
-
this.#stubExtensionFields(schema, typesBefore);
|
|
983
|
+
this.#stubExtensionFields(schema, typesBefore, fieldsBefore);
|
|
975
984
|
}
|
|
976
985
|
}
|
|
977
986
|
// Attach resolvers to extended fields
|
|
@@ -980,6 +989,35 @@ export class SchemaBuilder {
|
|
|
980
989
|
}
|
|
981
990
|
return schema;
|
|
982
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* The extension SDL to apply. `extend type T @ifTypeExists { ... }` extends
|
|
994
|
+
* `T` only when the schema built from the models has it — a plugin can add
|
|
995
|
+
* fields to model types a graph may or may not include — and is otherwise
|
|
996
|
+
* left out, along with the `typeFields` given for `T`. The directive is a
|
|
997
|
+
* build instruction only and never reaches the schema.
|
|
998
|
+
*/ #applicableExtensions(schema, sdl) {
|
|
999
|
+
const document = parseGraphQL(sdl);
|
|
1000
|
+
const definitions = [];
|
|
1001
|
+
for (const definition of document.definitions){
|
|
1002
|
+
const conditional = definition.kind === Kind.OBJECT_TYPE_EXTENSION && definition.directives?.some((directive)=>directive.name.value === IF_TYPE_EXISTS);
|
|
1003
|
+
if (!conditional) {
|
|
1004
|
+
definitions.push(definition);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (schema.getType(definition.name.value) == null) {
|
|
1008
|
+
this._skippedTypeExtensions.add(definition.name.value);
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
definitions.push({
|
|
1012
|
+
...definition,
|
|
1013
|
+
directives: definition.directives?.filter((directive)=>directive.name.value !== IF_TYPE_EXISTS)
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
...document,
|
|
1018
|
+
definitions
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
983
1021
|
#attachExtensionResolvers(schema, resolvers) {
|
|
984
1022
|
const queryType = schema.getQueryType();
|
|
985
1023
|
if (queryType != null && resolvers.queryFields != null) {
|
|
@@ -1014,6 +1052,7 @@ export class SchemaBuilder {
|
|
|
1014
1052
|
}
|
|
1015
1053
|
if (resolvers.typeFields != null) {
|
|
1016
1054
|
for (const [typeName, fieldResolvers] of Object.entries(resolvers.typeFields)){
|
|
1055
|
+
if (this._skippedTypeExtensions.has(typeName)) continue;
|
|
1017
1056
|
const type = schema.getType(typeName);
|
|
1018
1057
|
if (type == null) {
|
|
1019
1058
|
throw new Error(`Extension resolver references type "${typeName}" which does not exist in the schema`);
|
|
@@ -1036,7 +1075,7 @@ export class SchemaBuilder {
|
|
|
1036
1075
|
}
|
|
1037
1076
|
}
|
|
1038
1077
|
}
|
|
1039
|
-
#stubExtensionFields(schema, typesBefore) {
|
|
1078
|
+
#stubExtensionFields(schema, typesBefore, fieldsBefore) {
|
|
1040
1079
|
// Stub root Query/Mutation fields added by extension SDL
|
|
1041
1080
|
for (const rootType of [
|
|
1042
1081
|
schema.getQueryType(),
|
|
@@ -1050,15 +1089,22 @@ export class SchemaBuilder {
|
|
|
1050
1089
|
}
|
|
1051
1090
|
}
|
|
1052
1091
|
}
|
|
1053
|
-
// Stub fields on types added by extension SDL
|
|
1092
|
+
// Stub fields on types added by extension SDL, and fields an extension
|
|
1093
|
+
// added to an existing type
|
|
1054
1094
|
for (const [typeName, type] of Object.entries(schema.getTypeMap())){
|
|
1055
|
-
if (typesBefore.has(typeName)) continue;
|
|
1056
1095
|
if (!isObjectType(type)) continue;
|
|
1096
|
+
const existing = typesBefore.has(typeName) ? fieldsBefore.get(typeName) ?? null : null;
|
|
1097
|
+
if (typesBefore.has(typeName) && existing == null) continue;
|
|
1057
1098
|
const fields = type.getFields();
|
|
1058
1099
|
for (const field of Object.values(fields)){
|
|
1100
|
+
if (existing?.has(field.name) === true) continue;
|
|
1059
1101
|
if (field.resolve == null) {
|
|
1060
1102
|
const fieldType = field.type;
|
|
1061
|
-
field
|
|
1103
|
+
// A nullable field added to an existing type (a model's) stubs as
|
|
1104
|
+
// null: a made-up value, such as an enum's first member, would read
|
|
1105
|
+
// as a real one.
|
|
1106
|
+
const stubsNull = existing != null && !isNonNullType(fieldType);
|
|
1107
|
+
field.resolve = (source)=>source[field.name] ?? (stubsNull ? null : createDefaultValue(fieldType));
|
|
1062
1108
|
}
|
|
1063
1109
|
}
|
|
1064
1110
|
}
|
|
@@ -1162,12 +1208,15 @@ export class SchemaBuilder {
|
|
|
1162
1208
|
}, (node)=>{
|
|
1163
1209
|
if (typeof node === 'string') return 'AccountNode';
|
|
1164
1210
|
if (node instanceof AttachmentID) return 'AttachmentNode';
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1211
|
+
// An explicit type (set on a plugin node above) wins over the document
|
|
1212
|
+
// heuristic, so a plugin node whose source has a `model` key is not
|
|
1213
|
+
// taken for a document.
|
|
1168
1214
|
if (node != null && typeof node === 'object' && '__typename' in node) {
|
|
1169
1215
|
return node.__typename;
|
|
1170
1216
|
}
|
|
1217
|
+
if (node != null && typeof node === 'object' && 'model' in node) {
|
|
1218
|
+
return this._aliases[node.model];
|
|
1219
|
+
}
|
|
1171
1220
|
throw new Error('Unknown node type');
|
|
1172
1221
|
});
|
|
1173
1222
|
const accountObject = new GraphQLObjectType({
|
package/lib/types.d.ts
CHANGED
|
@@ -1,30 +1,6 @@
|
|
|
1
|
-
import type { DocumentData, DocumentFilter, DocumentOrderBy } from '@kubun/protocol';
|
|
2
|
-
import type { ConnectionArguments } from '
|
|
3
|
-
export type PatchAddOperation
|
|
4
|
-
op: 'add';
|
|
5
|
-
path: string;
|
|
6
|
-
value: unknown;
|
|
7
|
-
};
|
|
8
|
-
export type PatchRemoveOperation = {
|
|
9
|
-
op: 'remove';
|
|
10
|
-
path: string;
|
|
11
|
-
};
|
|
12
|
-
export type PatchReplaceOperation = {
|
|
13
|
-
op: 'replace';
|
|
14
|
-
path: string;
|
|
15
|
-
value: unknown;
|
|
16
|
-
};
|
|
17
|
-
export type PatchMoveOperation = {
|
|
18
|
-
op: 'move';
|
|
19
|
-
from: string;
|
|
20
|
-
path: string;
|
|
21
|
-
};
|
|
22
|
-
export type PatchCopyOperation = {
|
|
23
|
-
op: 'copy';
|
|
24
|
-
from: string;
|
|
25
|
-
path: string;
|
|
26
|
-
};
|
|
27
|
-
export type PatchOperation = PatchAddOperation | PatchCopyOperation | PatchMoveOperation | PatchRemoveOperation | PatchReplaceOperation;
|
|
1
|
+
import type { DocumentData, DocumentFilter, DocumentOrderBy, PatchOperation } from '@kubun/protocol';
|
|
2
|
+
import type { ConnectionArguments } from './relay.js';
|
|
3
|
+
export type { PatchAddOperation, PatchCopyOperation, PatchMoveOperation, PatchOperation, PatchRemoveOperation, PatchReplaceOperation, } from '@kubun/protocol';
|
|
28
4
|
export type AllConnectionArguments = ConnectionArguments & {
|
|
29
5
|
filter?: DocumentFilter;
|
|
30
6
|
orderBy?: DocumentOrderBy;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/graphql",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,14 +18,13 @@
|
|
|
18
18
|
"@kubun/hlc": "^0.17.0",
|
|
19
19
|
"@kubun/id": "^0.17.0",
|
|
20
20
|
"@kubun/logger": "^0.17.0",
|
|
21
|
-
"@kubun/protocol": "^0.17.
|
|
21
|
+
"@kubun/protocol": "^0.17.1",
|
|
22
22
|
"@sozai/async": "^0.2.1",
|
|
23
23
|
"@sozai/codec": "^0.4.0",
|
|
24
24
|
"@sozai/event": "^0.1.3",
|
|
25
25
|
"@sozai/otel": "^0.3.2",
|
|
26
26
|
"change-case": "^5.4.4",
|
|
27
|
-
"graphql": "^
|
|
28
|
-
"graphql-relay": "^0.10.2",
|
|
27
|
+
"graphql": "^17.0.2",
|
|
29
28
|
"graphql-scalars": "^2.0.0",
|
|
30
29
|
"multiformats": "^14.0.5"
|
|
31
30
|
},
|
|
@@ -35,7 +34,7 @@
|
|
|
35
34
|
"@kubun/db": "^0.17.0",
|
|
36
35
|
"@kubun/db-better-sqlite": "^0.17.0",
|
|
37
36
|
"@kubun/scalars": "^0.17.0",
|
|
38
|
-
"@kubun/store-graph": "^0.17.
|
|
37
|
+
"@kubun/store-graph": "^0.17.1",
|
|
39
38
|
"@kubun/test-utils": "^0.0.0"
|
|
40
39
|
},
|
|
41
40
|
"publishConfig": {
|