@kubun/graphql 0.17.1 → 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 CHANGED
@@ -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/schema.d.ts CHANGED
@@ -94,6 +94,7 @@ 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;
package/lib/schema.js CHANGED
@@ -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 = {};
@@ -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
- schema = extendSchema(schema, parseGraphQL(this._extensionSDL));
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.resolve = (source)=>source[field.name] ?? createDefaultValue(fieldType);
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
- if (node != null && typeof node === 'object' && 'model' in node) {
1166
- return this._aliases[node.model];
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';
1
+ import type { DocumentData, DocumentFilter, DocumentOrderBy, PatchOperation } from '@kubun/protocol';
2
2
  import type { ConnectionArguments } from './relay.js';
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;
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.1",
3
+ "version": "0.17.2",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -18,7 +18,7 @@
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.0",
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",
@@ -34,7 +34,7 @@
34
34
  "@kubun/db": "^0.17.0",
35
35
  "@kubun/db-better-sqlite": "^0.17.0",
36
36
  "@kubun/scalars": "^0.17.0",
37
- "@kubun/store-graph": "^0.17.0",
37
+ "@kubun/store-graph": "^0.17.1",
38
38
  "@kubun/test-utils": "^0.0.0"
39
39
  },
40
40
  "publishConfig": {