@kubun/graphql 0.17.1 → 0.18.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/lib/schema.js CHANGED
@@ -1,7 +1,7 @@
1
- import { AttachmentID, DocumentID, DocumentModelID, decodeID, PluginNodeID } from '@kubun/id';
1
+ import { AttachmentID, CatalogID, DocumentID, DocumentModelID, decodeID, PluginNodeID } from '@kubun/id';
2
2
  import { REFERENCE_PREFIX } from '@kubun/protocol';
3
3
  import { pascalCase } from 'change-case';
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';
4
+ import { extendSchema, GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, isEnumType, isListType, isNonNullType, isObjectType, isScalarType, Kind, parse as parseGraphQL } from 'graphql';
5
5
  import { GraphQLDateTimeISO, GraphQLDID, GraphQLDuration, GraphQLEmailAddress, GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
6
6
  import { GraphQLAttachmentID, GraphQLCredentialEntryID, GraphQLDocID } from './identifiers.js';
7
7
  import { connectionArgs, connectionDefinitions, mutationWithClientMutationId, nodeDefinitions } from './relay.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,
@@ -356,6 +358,411 @@ const ModelAccessDefaults = new GraphQLObjectType({
356
358
  }
357
359
  })
358
360
  });
361
+ const CatalogRuleInput = new GraphQLInputObjectType({
362
+ name: 'CatalogRuleInput',
363
+ fields: {
364
+ id: {
365
+ type: GraphQLID
366
+ },
367
+ models: {
368
+ type: toList(GraphQLString)
369
+ },
370
+ owners: {
371
+ type: toList(GraphQLString)
372
+ },
373
+ maxAge: {
374
+ type: GraphQLString
375
+ },
376
+ maxAhead: {
377
+ type: GraphQLString
378
+ },
379
+ dateField: {
380
+ type: GraphQLString
381
+ }
382
+ }
383
+ });
384
+ const CatalogRule = new GraphQLObjectType({
385
+ name: 'CatalogRule',
386
+ fields: {
387
+ id: {
388
+ type: new GraphQLNonNull(GraphQLID)
389
+ },
390
+ models: {
391
+ type: toOuputList(GraphQLString)
392
+ },
393
+ owners: {
394
+ type: toOuputList(GraphQLString)
395
+ },
396
+ maxAge: {
397
+ type: GraphQLString
398
+ },
399
+ maxAhead: {
400
+ type: GraphQLString
401
+ },
402
+ dateField: {
403
+ type: GraphQLString
404
+ }
405
+ }
406
+ });
407
+ const CatalogVersionRef = new GraphQLObjectType({
408
+ name: 'CatalogVersionRef',
409
+ fields: {
410
+ catalogID: {
411
+ type: new GraphQLNonNull(GraphQLID)
412
+ },
413
+ versionID: {
414
+ type: new GraphQLNonNull(GraphQLID)
415
+ }
416
+ }
417
+ });
418
+ const CatalogUpdateInput = new GraphQLInputObjectType({
419
+ name: 'CatalogUpdateInput',
420
+ fields: {
421
+ name: {
422
+ type: GraphQLString
423
+ },
424
+ description: {
425
+ type: GraphQLString
426
+ },
427
+ rules: {
428
+ type: toList(CatalogRuleInput)
429
+ }
430
+ }
431
+ });
432
+ const Catalog = new GraphQLObjectType({
433
+ name: 'Catalog',
434
+ fields: {
435
+ headVersionID: {
436
+ type: new GraphQLNonNull(GraphQLID)
437
+ },
438
+ upstream: {
439
+ type: CatalogVersionRef
440
+ },
441
+ id: {
442
+ type: new GraphQLNonNull(GraphQLID)
443
+ },
444
+ name: {
445
+ type: new GraphQLNonNull(GraphQLString)
446
+ },
447
+ description: {
448
+ type: GraphQLString
449
+ },
450
+ rules: {
451
+ type: new GraphQLNonNull(toOuputList(CatalogRule))
452
+ },
453
+ app: {
454
+ type: GraphQLString
455
+ }
456
+ }
457
+ });
458
+ const CatalogListing = new GraphQLObjectType({
459
+ name: 'CatalogListing',
460
+ fields: {
461
+ id: {
462
+ type: new GraphQLNonNull(GraphQLID)
463
+ },
464
+ app: {
465
+ type: GraphQLString
466
+ },
467
+ name: {
468
+ type: new GraphQLNonNull(GraphQLString)
469
+ },
470
+ deletedAt: {
471
+ type: GraphQLString
472
+ },
473
+ catalog: {
474
+ type: Catalog
475
+ }
476
+ }
477
+ });
478
+ const CatalogVersion = new GraphQLObjectType({
479
+ name: 'CatalogVersion',
480
+ fields: {
481
+ id: {
482
+ type: new GraphQLNonNull(GraphQLID)
483
+ },
484
+ catalogID: {
485
+ type: new GraphQLNonNull(GraphQLID)
486
+ },
487
+ parents: {
488
+ type: new GraphQLNonNull(toOuputList(CatalogVersionRef))
489
+ },
490
+ catalog: {
491
+ type: new GraphQLNonNull(Catalog)
492
+ }
493
+ }
494
+ });
495
+ const CatalogVersionEdge = new GraphQLObjectType({
496
+ name: 'CatalogVersionEdge',
497
+ fields: {
498
+ cursor: {
499
+ type: new GraphQLNonNull(GraphQLString)
500
+ },
501
+ node: {
502
+ type: new GraphQLNonNull(CatalogVersion)
503
+ }
504
+ }
505
+ });
506
+ const CatalogVersionPageInfo = new GraphQLObjectType({
507
+ name: 'CatalogVersionPageInfo',
508
+ fields: {
509
+ endCursor: {
510
+ type: GraphQLString
511
+ },
512
+ hasNextPage: {
513
+ type: new GraphQLNonNull(GraphQLBoolean)
514
+ }
515
+ }
516
+ });
517
+ const CatalogVersionConnection = new GraphQLObjectType({
518
+ name: 'CatalogVersionConnection',
519
+ fields: {
520
+ edges: {
521
+ type: new GraphQLNonNull(toOuputList(CatalogVersionEdge))
522
+ },
523
+ pageInfo: {
524
+ type: new GraphQLNonNull(CatalogVersionPageInfo)
525
+ }
526
+ }
527
+ });
528
+ const CatalogDraft = new GraphQLObjectType({
529
+ name: 'CatalogDraft',
530
+ fields: {
531
+ catalogID: {
532
+ type: new GraphQLNonNull(GraphQLID)
533
+ },
534
+ baseVersionID: {
535
+ type: new GraphQLNonNull(GraphQLID)
536
+ },
537
+ dirty: {
538
+ type: new GraphQLNonNull(GraphQLBoolean)
539
+ },
540
+ updateAvailable: {
541
+ type: new GraphQLNonNull(GraphQLBoolean)
542
+ },
543
+ catalog: {
544
+ type: new GraphQLNonNull(Catalog)
545
+ }
546
+ }
547
+ });
548
+ const CatalogDeviceLevel = new GraphQLEnumType({
549
+ name: 'CatalogDeviceLevel',
550
+ values: {
551
+ KNOWN: {},
552
+ OFFLINE: {},
553
+ SERVE_ONLY: {}
554
+ }
555
+ });
556
+ const CatalogLevelState = new GraphQLEnumType({
557
+ name: 'CatalogLevelState',
558
+ values: {
559
+ READY: {},
560
+ NEEDS_ROUTE: {}
561
+ }
562
+ });
563
+ const CatalogDeviceStorage = new GraphQLObjectType({
564
+ name: 'CatalogDeviceStorage',
565
+ fields: {
566
+ catalogID: {
567
+ type: new GraphQLNonNull(GraphQLID)
568
+ },
569
+ level: {
570
+ type: CatalogDeviceLevel
571
+ },
572
+ state: {
573
+ type: new GraphQLNonNull(CatalogLevelState)
574
+ },
575
+ matchingLocalDocuments: {
576
+ type: new GraphQLNonNull(GraphQLInt)
577
+ }
578
+ }
579
+ });
580
+ const CustodyStatus = new GraphQLEnumType({
581
+ name: 'CustodyStatus',
582
+ values: {
583
+ NONE: {},
584
+ PENDING: {},
585
+ PARTIAL: {},
586
+ READY: {}
587
+ }
588
+ });
589
+ const CatalogCustody = new GraphQLObjectType({
590
+ name: 'CatalogCustody',
591
+ fields: {
592
+ catalogID: {
593
+ type: new GraphQLNonNull(GraphQLID)
594
+ },
595
+ custodians: {
596
+ type: new GraphQLNonNull(toOuputList(GraphQLString))
597
+ },
598
+ accepted: {
599
+ type: new GraphQLNonNull(toOuputList(GraphQLString))
600
+ },
601
+ declined: {
602
+ type: new GraphQLNonNull(toOuputList(GraphQLString))
603
+ },
604
+ status: {
605
+ type: new GraphQLNonNull(CustodyStatus)
606
+ }
607
+ }
608
+ });
609
+ const DocumentRetainHold = new GraphQLObjectType({
610
+ name: 'DocumentRetainHold',
611
+ fields: {
612
+ id: {
613
+ type: new GraphQLNonNull(GraphQLID)
614
+ },
615
+ documentID: {
616
+ type: new GraphQLNonNull(GraphQLID)
617
+ },
618
+ reason: {
619
+ type: GraphQLString
620
+ },
621
+ expiresAt: {
622
+ type: GraphQLDateTimeISO,
623
+ resolve: (hold)=>hold.expiresAt == null ? null : new Date(hold.expiresAt)
624
+ }
625
+ }
626
+ });
627
+ const DocumentAvailability = new GraphQLEnumType({
628
+ name: 'DocumentAvailability',
629
+ values: {
630
+ LOCAL: {},
631
+ UNAVAILABLE: {}
632
+ }
633
+ });
634
+ const DocumentScope = new GraphQLObjectType({
635
+ name: 'DocumentScope',
636
+ fields: {
637
+ scope: {
638
+ type: new GraphQLNonNull(GraphQLString)
639
+ },
640
+ availability: {
641
+ type: DocumentAvailability
642
+ },
643
+ retained: {
644
+ type: new GraphQLNonNull(GraphQLBoolean)
645
+ },
646
+ refs: {
647
+ type: new GraphQLNonNull(toOuputList(GraphQLString))
648
+ }
649
+ }
650
+ });
651
+ const CatalogUpstreamUnavailableReason = new GraphQLEnumType({
652
+ name: 'CatalogUpstreamUnavailableReason',
653
+ values: {
654
+ NO_LIVE_OFFER: {},
655
+ UPSTREAM_DELETED: {},
656
+ BASE_MISSING: {},
657
+ VERSION_PENDING: {}
658
+ }
659
+ });
660
+ const CatalogUpstreamStatus = new GraphQLObjectType({
661
+ name: 'CatalogUpstreamStatus',
662
+ fields: {
663
+ upstream: {
664
+ type: CatalogVersionRef
665
+ },
666
+ latestVersionID: {
667
+ type: GraphQLID
668
+ },
669
+ updateAvailable: {
670
+ type: new GraphQLNonNull(GraphQLBoolean)
671
+ },
672
+ unavailableReason: {
673
+ type: CatalogUpstreamUnavailableReason
674
+ }
675
+ }
676
+ });
677
+ const CatalogRuleConflict = new GraphQLObjectType({
678
+ name: 'CatalogRuleConflict',
679
+ fields: {
680
+ ruleID: {
681
+ type: new GraphQLNonNull(GraphQLID)
682
+ },
683
+ base: {
684
+ type: CatalogRule
685
+ },
686
+ local: {
687
+ type: CatalogRule
688
+ },
689
+ upstream: {
690
+ type: CatalogRule
691
+ }
692
+ }
693
+ });
694
+ const CatalogPullPreview = new GraphQLObjectType({
695
+ name: 'CatalogPullPreview',
696
+ fields: {
697
+ baseVersionID: {
698
+ type: new GraphQLNonNull(GraphQLID)
699
+ },
700
+ localRevisionID: {
701
+ type: new GraphQLNonNull(GraphQLID)
702
+ },
703
+ upstreamVersionID: {
704
+ type: new GraphQLNonNull(GraphQLID)
705
+ },
706
+ merged: {
707
+ type: new GraphQLNonNull(Catalog)
708
+ },
709
+ conflicts: {
710
+ type: new GraphQLNonNull(toOuputList(CatalogRuleConflict))
711
+ }
712
+ }
713
+ });
714
+ const CatalogPullResult = new GraphQLObjectType({
715
+ name: 'CatalogPullResult',
716
+ fields: {
717
+ catalog: {
718
+ type: new GraphQLNonNull(CatalogListing)
719
+ },
720
+ draft: {
721
+ type: CatalogDraft
722
+ },
723
+ published: {
724
+ type: new GraphQLNonNull(GraphQLBoolean)
725
+ }
726
+ }
727
+ });
728
+ const CatalogPullSide = new GraphQLEnumType({
729
+ name: 'CatalogPullSide',
730
+ values: {
731
+ LOCAL: {},
732
+ UPSTREAM: {}
733
+ }
734
+ });
735
+ const CatalogRuleResolutionInput = new GraphQLInputObjectType({
736
+ name: 'CatalogRuleResolutionInput',
737
+ fields: {
738
+ ruleID: {
739
+ type: new GraphQLNonNull(GraphQLID)
740
+ },
741
+ side: {
742
+ type: new GraphQLNonNull(CatalogPullSide)
743
+ }
744
+ }
745
+ });
746
+ function requireCatalog(ctx) {
747
+ if (ctx.catalog === undefined) throw new Error('Catalog context is unavailable');
748
+ return ctx.catalog;
749
+ }
750
+ async function catalogOperation(operation) {
751
+ try {
752
+ return await operation();
753
+ } catch (error) {
754
+ if (error instanceof Error && 'code' in error && (error.code === 'CATALOG_PULL_STALE' || error.code === 'CATALOG_RULES_EMPTY' || error.code === 'CATALOG_RULE_LIMIT_EXCEEDED' || error.code === 'INVALID_CATALOG_RULE' || error.code === 'CATALOG_DEVICE_LEVEL_DENIED' || error.code === 'CUSTODIAN_UNAVAILABLE' || error.code === 'CUSTODY_SIGNER_REQUIRED' || error.code === 'CUSTODY_ONLY_COPY' || error.code === 'DEVICE_OWNER_REQUIRED' || error.code === 'DOCUMENT_HOLD_DENIED' || error.code === 'DOCUMENT_HOLD_INVALID_EXPIRY' || error.code === 'DOCUMENT_NOT_HOLDABLE')) {
755
+ // biome-ignore lint/style/useErrorCause: GraphQLError retains the cause as originalError.
756
+ throw new GraphQLError(error.message, {
757
+ originalError: error,
758
+ extensions: {
759
+ code: error.code
760
+ }
761
+ });
762
+ }
763
+ throw error;
764
+ }
765
+ }
359
766
  export class SchemaBuilder {
360
767
  // Mapping of model or references IDs to their GraphQL object names
361
768
  _aliases;
@@ -388,6 +795,9 @@ export class SchemaBuilder {
388
795
  _relationsTo = {};
389
796
  _extensionSDL;
390
797
  _extensionResolvers;
798
+ // Types whose `@ifTypeExists` extension was left out: their `typeFields`
799
+ // are skipped rather than rejected.
800
+ _skippedTypeExtensions = new Set();
391
801
  _stubExtensionResolvers;
392
802
  _pluginNodeResolvers = {};
393
803
  _subscriptions = {};
@@ -535,7 +945,90 @@ export class SchemaBuilder {
535
945
  this._definitions = definitions;
536
946
  const queryFields = {
537
947
  nodes: definitions.nodesField,
538
- ...definitions.queryFields
948
+ ...definitions.queryFields,
949
+ previewCatalogPull: {
950
+ type: new GraphQLNonNull(CatalogPullPreview),
951
+ args: {
952
+ id: {
953
+ type: new GraphQLNonNull(GraphQLID)
954
+ }
955
+ },
956
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).previewCatalogPull(args.id))
957
+ },
958
+ catalogVersions: {
959
+ type: new GraphQLNonNull(CatalogVersionConnection),
960
+ args: {
961
+ id: {
962
+ type: new GraphQLNonNull(GraphQLID)
963
+ },
964
+ first: {
965
+ type: new GraphQLNonNull(GraphQLInt)
966
+ },
967
+ after: {
968
+ type: GraphQLString
969
+ }
970
+ },
971
+ resolve: async (_root, args, ctx)=>await requireCatalog(ctx).catalogVersions({
972
+ id: args.id,
973
+ first: args.first,
974
+ after: args.after ?? undefined
975
+ })
976
+ },
977
+ catalogUpstreamStatus: {
978
+ type: new GraphQLNonNull(CatalogUpstreamStatus),
979
+ args: {
980
+ id: {
981
+ type: new GraphQLNonNull(GraphQLID)
982
+ }
983
+ },
984
+ resolve: async (_root, args, ctx)=>await requireCatalog(ctx).catalogUpstreamStatus(args.id)
985
+ },
986
+ catalogDraft: {
987
+ type: CatalogDraft,
988
+ args: {
989
+ id: {
990
+ type: new GraphQLNonNull(GraphQLID)
991
+ }
992
+ },
993
+ resolve: async (_root, args, ctx)=>await requireCatalog(ctx).catalogDraft(args.id)
994
+ },
995
+ catalogs: {
996
+ type: new GraphQLNonNull(toOuputList(CatalogListing)),
997
+ args: {
998
+ includeDeleted: {
999
+ type: GraphQLBoolean,
1000
+ defaultValue: false
1001
+ }
1002
+ },
1003
+ resolve: async (_root, args, ctx)=>await requireCatalog(ctx).catalogs(args.includeDeleted === true)
1004
+ },
1005
+ catalogDeviceStorage: {
1006
+ type: new GraphQLNonNull(CatalogDeviceStorage),
1007
+ args: {
1008
+ catalogID: {
1009
+ type: new GraphQLNonNull(GraphQLID)
1010
+ }
1011
+ },
1012
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).catalogDeviceStorage(args.catalogID))
1013
+ },
1014
+ catalogCustody: {
1015
+ type: new GraphQLNonNull(CatalogCustody),
1016
+ args: {
1017
+ catalogID: {
1018
+ type: new GraphQLNonNull(GraphQLID)
1019
+ }
1020
+ },
1021
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).catalogCustody(args.catalogID))
1022
+ },
1023
+ documentScope: {
1024
+ type: DocumentScope,
1025
+ args: {
1026
+ id: {
1027
+ type: new GraphQLNonNull(GraphQLID)
1028
+ }
1029
+ },
1030
+ resolve: async (_root, args, ctx)=>await requireCatalog(ctx).documentScope(args.id)
1031
+ }
539
1032
  };
540
1033
  for (const id of this._includedModelIDs){
541
1034
  this._buildDocument(id);
@@ -553,6 +1046,9 @@ export class SchemaBuilder {
553
1046
  },
554
1047
  search: {
555
1048
  type: GraphQLString
1049
+ },
1050
+ catalogID: {
1051
+ type: GraphQLID
556
1052
  }
557
1053
  },
558
1054
  resolve: async (_, args, ctx)=>{
@@ -611,10 +1107,13 @@ export class SchemaBuilder {
611
1107
  },
612
1108
  after: {
613
1109
  type: GraphQLString
1110
+ },
1111
+ catalogID: {
1112
+ type: GraphQLID
614
1113
  }
615
1114
  },
616
1115
  resolve: async (_root, args, ctx)=>{
617
- const { query, models, first, after } = args;
1116
+ const { query, models, first, after, catalogID } = args;
618
1117
  if (query.trim() === '') {
619
1118
  throw new Error('Search query must not be empty');
620
1119
  }
@@ -632,7 +1131,8 @@ export class SchemaBuilder {
632
1131
  query,
633
1132
  modelIDs,
634
1133
  first: pageSize + 1,
635
- offset
1134
+ offset,
1135
+ catalogRef: catalogID == null ? undefined : CatalogID.fromString(catalogID).toRecordKey()
636
1136
  });
637
1137
  const hasNextPage = hits.length > pageSize;
638
1138
  const page = hasNextPage ? hits.slice(0, pageSize) : hits;
@@ -881,6 +1381,219 @@ export class SchemaBuilder {
881
1381
  }
882
1382
  };
883
1383
  }
1384
+ if (this._includeMutations) {
1385
+ this._mutations.editCatalogDraft = {
1386
+ type: new GraphQLNonNull(CatalogDraft),
1387
+ args: {
1388
+ id: {
1389
+ type: new GraphQLNonNull(GraphQLID)
1390
+ },
1391
+ input: {
1392
+ type: new GraphQLNonNull(CatalogUpdateInput)
1393
+ }
1394
+ },
1395
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).editCatalogDraft(args.id, args.input))
1396
+ };
1397
+ this._mutations.discardCatalogDraft = {
1398
+ type: new GraphQLNonNull(GraphQLBoolean),
1399
+ args: {
1400
+ id: {
1401
+ type: new GraphQLNonNull(GraphQLID)
1402
+ }
1403
+ },
1404
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).discardCatalogDraft(args.id))
1405
+ };
1406
+ this._mutations.publishCatalogDraft = {
1407
+ type: new GraphQLNonNull(CatalogListing),
1408
+ args: {
1409
+ id: {
1410
+ type: new GraphQLNonNull(GraphQLID)
1411
+ },
1412
+ expectedHeadVersionID: {
1413
+ type: new GraphQLNonNull(GraphQLID)
1414
+ }
1415
+ },
1416
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).publishCatalogDraft(args.id, args.expectedHeadVersionID))
1417
+ };
1418
+ this._mutations.resolveCatalogPull = {
1419
+ type: new GraphQLNonNull(CatalogPullResult),
1420
+ args: {
1421
+ id: {
1422
+ type: new GraphQLNonNull(GraphQLID)
1423
+ },
1424
+ baseVersionID: {
1425
+ type: new GraphQLNonNull(GraphQLID)
1426
+ },
1427
+ localRevisionID: {
1428
+ type: new GraphQLNonNull(GraphQLID)
1429
+ },
1430
+ upstreamVersionID: {
1431
+ type: new GraphQLNonNull(GraphQLID)
1432
+ },
1433
+ resolutions: {
1434
+ type: new GraphQLNonNull(toList(CatalogRuleResolutionInput))
1435
+ },
1436
+ publish: {
1437
+ type: new GraphQLNonNull(GraphQLBoolean)
1438
+ }
1439
+ },
1440
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).resolveCatalogPull(args))
1441
+ };
1442
+ this._mutations.cloneCatalog = {
1443
+ type: new GraphQLNonNull(CatalogListing),
1444
+ args: {
1445
+ id: {
1446
+ type: new GraphQLNonNull(GraphQLID)
1447
+ }
1448
+ },
1449
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).cloneCatalog(args.id))
1450
+ };
1451
+ this._mutations.createCatalog = {
1452
+ type: Catalog,
1453
+ args: {
1454
+ name: {
1455
+ type: new GraphQLNonNull(GraphQLString)
1456
+ },
1457
+ description: {
1458
+ type: GraphQLString
1459
+ },
1460
+ rules: {
1461
+ type: new GraphQLNonNull(toList(CatalogRuleInput))
1462
+ },
1463
+ app: {
1464
+ type: GraphQLString
1465
+ }
1466
+ },
1467
+ resolve: async (_root, args, ctx)=>await catalogOperation(async ()=>{
1468
+ const listing = await requireCatalog(ctx).createCatalog({
1469
+ name: args.name,
1470
+ description: args.description ?? undefined,
1471
+ rules: args.rules,
1472
+ app: args.app ?? undefined
1473
+ });
1474
+ return listing.catalog;
1475
+ })
1476
+ };
1477
+ this._mutations.updateCatalog = {
1478
+ type: Catalog,
1479
+ args: {
1480
+ id: {
1481
+ type: new GraphQLNonNull(GraphQLID)
1482
+ },
1483
+ name: {
1484
+ type: GraphQLString
1485
+ },
1486
+ description: {
1487
+ type: GraphQLString
1488
+ },
1489
+ rules: {
1490
+ type: toList(CatalogRuleInput)
1491
+ }
1492
+ },
1493
+ resolve: async (_root, args, ctx)=>await catalogOperation(async ()=>{
1494
+ const listing = await requireCatalog(ctx).updateCatalog(args.id, {
1495
+ name: args.name ?? undefined,
1496
+ description: args.description ?? undefined,
1497
+ rules: args.rules ?? undefined
1498
+ });
1499
+ return listing.catalog;
1500
+ })
1501
+ };
1502
+ this._mutations.deleteCatalog = {
1503
+ type: GraphQLBoolean,
1504
+ args: {
1505
+ id: {
1506
+ type: new GraphQLNonNull(GraphQLID)
1507
+ }
1508
+ },
1509
+ resolve: async (_root, args, ctx)=>await requireCatalog(ctx).deleteCatalog(args.id)
1510
+ };
1511
+ this._mutations.deduplicateAppCatalogs = {
1512
+ type: Catalog,
1513
+ args: {
1514
+ app: {
1515
+ type: new GraphQLNonNull(GraphQLString)
1516
+ },
1517
+ name: {
1518
+ type: new GraphQLNonNull(GraphQLString)
1519
+ }
1520
+ },
1521
+ resolve: async (_root, args, ctx)=>(await requireCatalog(ctx).deduplicateAppCatalogs(args.app, args.name))?.catalog ?? null
1522
+ };
1523
+ this._mutations.setCatalogDeviceLevel = {
1524
+ type: new GraphQLNonNull(CatalogDeviceStorage),
1525
+ args: {
1526
+ catalogID: {
1527
+ type: new GraphQLNonNull(GraphQLID)
1528
+ },
1529
+ level: {
1530
+ type: new GraphQLNonNull(CatalogDeviceLevel)
1531
+ }
1532
+ },
1533
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).setCatalogDeviceLevel(args.catalogID, args.level))
1534
+ };
1535
+ this._mutations.setCatalogCustodians = {
1536
+ type: new GraphQLNonNull(CatalogCustody),
1537
+ args: {
1538
+ catalogID: {
1539
+ type: new GraphQLNonNull(GraphQLID)
1540
+ },
1541
+ deviceDIDs: {
1542
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString)))
1543
+ }
1544
+ },
1545
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).setCatalogCustodians(args.catalogID, args.deviceDIDs))
1546
+ };
1547
+ this._mutations.setDeviceCustodyEligible = {
1548
+ type: new GraphQLNonNull(GraphQLBoolean),
1549
+ args: {
1550
+ eligible: {
1551
+ type: new GraphQLNonNull(GraphQLBoolean)
1552
+ }
1553
+ },
1554
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).setDeviceCustodyEligible(args.eligible))
1555
+ };
1556
+ this._mutations.leaveCatalogCustody = {
1557
+ type: new GraphQLNonNull(CatalogCustody),
1558
+ args: {
1559
+ catalogID: {
1560
+ type: new GraphQLNonNull(GraphQLID)
1561
+ },
1562
+ force: {
1563
+ type: GraphQLBoolean,
1564
+ defaultValue: false
1565
+ }
1566
+ },
1567
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).leaveCatalogCustody(args.catalogID, args.force === true))
1568
+ };
1569
+ this._mutations.retainDocument = {
1570
+ type: new GraphQLNonNull(DocumentRetainHold),
1571
+ args: {
1572
+ documentID: {
1573
+ type: new GraphQLNonNull(GraphQLID)
1574
+ },
1575
+ reason: {
1576
+ type: GraphQLString
1577
+ },
1578
+ expiresAt: {
1579
+ type: GraphQLDateTimeISO
1580
+ }
1581
+ },
1582
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).retainDocument(args.documentID, {
1583
+ reason: args.reason ?? undefined,
1584
+ expiresAt: args.expiresAt?.toISOString()
1585
+ }))
1586
+ };
1587
+ this._mutations.releaseDocumentHold = {
1588
+ type: new GraphQLNonNull(GraphQLBoolean),
1589
+ args: {
1590
+ holdID: {
1591
+ type: new GraphQLNonNull(GraphQLID)
1592
+ }
1593
+ },
1594
+ resolve: async (_root, args, ctx)=>await catalogOperation(()=>requireCatalog(ctx).releaseDocumentHold(args.holdID))
1595
+ };
1596
+ }
884
1597
  const config = {
885
1598
  query: new GraphQLObjectType({
886
1599
  name: 'Query',
@@ -968,10 +1681,14 @@ export class SchemaBuilder {
968
1681
  // Apply SDL extensions
969
1682
  if (this._extensionSDL != null) {
970
1683
  const typesBefore = new Set(Object.keys(schema.getTypeMap()));
971
- schema = extendSchema(schema, parseGraphQL(this._extensionSDL));
1684
+ const fieldsBefore = new Map();
1685
+ for (const [typeName, type] of Object.entries(schema.getTypeMap())){
1686
+ if (isObjectType(type)) fieldsBefore.set(typeName, new Set(Object.keys(type.getFields())));
1687
+ }
1688
+ schema = extendSchema(schema, this.#applicableExtensions(schema, this._extensionSDL));
972
1689
  // Stub extension resolvers for client-side pre-execution
973
1690
  if (this._stubExtensionResolvers) {
974
- this.#stubExtensionFields(schema, typesBefore);
1691
+ this.#stubExtensionFields(schema, typesBefore, fieldsBefore);
975
1692
  }
976
1693
  }
977
1694
  // Attach resolvers to extended fields
@@ -980,6 +1697,35 @@ export class SchemaBuilder {
980
1697
  }
981
1698
  return schema;
982
1699
  }
1700
+ /**
1701
+ * The extension SDL to apply. `extend type T @ifTypeExists { ... }` extends
1702
+ * `T` only when the schema built from the models has it — a plugin can add
1703
+ * fields to model types a graph may or may not include — and is otherwise
1704
+ * left out, along with the `typeFields` given for `T`. The directive is a
1705
+ * build instruction only and never reaches the schema.
1706
+ */ #applicableExtensions(schema, sdl) {
1707
+ const document = parseGraphQL(sdl);
1708
+ const definitions = [];
1709
+ for (const definition of document.definitions){
1710
+ const conditional = definition.kind === Kind.OBJECT_TYPE_EXTENSION && definition.directives?.some((directive)=>directive.name.value === IF_TYPE_EXISTS);
1711
+ if (!conditional) {
1712
+ definitions.push(definition);
1713
+ continue;
1714
+ }
1715
+ if (schema.getType(definition.name.value) == null) {
1716
+ this._skippedTypeExtensions.add(definition.name.value);
1717
+ continue;
1718
+ }
1719
+ definitions.push({
1720
+ ...definition,
1721
+ directives: definition.directives?.filter((directive)=>directive.name.value !== IF_TYPE_EXISTS)
1722
+ });
1723
+ }
1724
+ return {
1725
+ ...document,
1726
+ definitions
1727
+ };
1728
+ }
983
1729
  #attachExtensionResolvers(schema, resolvers) {
984
1730
  const queryType = schema.getQueryType();
985
1731
  if (queryType != null && resolvers.queryFields != null) {
@@ -1014,6 +1760,7 @@ export class SchemaBuilder {
1014
1760
  }
1015
1761
  if (resolvers.typeFields != null) {
1016
1762
  for (const [typeName, fieldResolvers] of Object.entries(resolvers.typeFields)){
1763
+ if (this._skippedTypeExtensions.has(typeName)) continue;
1017
1764
  const type = schema.getType(typeName);
1018
1765
  if (type == null) {
1019
1766
  throw new Error(`Extension resolver references type "${typeName}" which does not exist in the schema`);
@@ -1036,7 +1783,7 @@ export class SchemaBuilder {
1036
1783
  }
1037
1784
  }
1038
1785
  }
1039
- #stubExtensionFields(schema, typesBefore) {
1786
+ #stubExtensionFields(schema, typesBefore, fieldsBefore) {
1040
1787
  // Stub root Query/Mutation fields added by extension SDL
1041
1788
  for (const rootType of [
1042
1789
  schema.getQueryType(),
@@ -1050,15 +1797,22 @@ export class SchemaBuilder {
1050
1797
  }
1051
1798
  }
1052
1799
  }
1053
- // Stub fields on types added by extension SDL
1800
+ // Stub fields on types added by extension SDL, and fields an extension
1801
+ // added to an existing type
1054
1802
  for (const [typeName, type] of Object.entries(schema.getTypeMap())){
1055
- if (typesBefore.has(typeName)) continue;
1056
1803
  if (!isObjectType(type)) continue;
1804
+ const existing = typesBefore.has(typeName) ? fieldsBefore.get(typeName) ?? null : null;
1805
+ if (typesBefore.has(typeName) && existing == null) continue;
1057
1806
  const fields = type.getFields();
1058
1807
  for (const field of Object.values(fields)){
1808
+ if (existing?.has(field.name) === true) continue;
1059
1809
  if (field.resolve == null) {
1060
1810
  const fieldType = field.type;
1061
- field.resolve = (source)=>source[field.name] ?? createDefaultValue(fieldType);
1811
+ // A nullable field added to an existing type (a model's) stubs as
1812
+ // null: a made-up value, such as an enum's first member, would read
1813
+ // as a real one.
1814
+ const stubsNull = existing != null && !isNonNullType(fieldType);
1815
+ field.resolve = (source)=>source[field.name] ?? (stubsNull ? null : createDefaultValue(fieldType));
1062
1816
  }
1063
1817
  }
1064
1818
  }
@@ -1162,12 +1916,15 @@ export class SchemaBuilder {
1162
1916
  }, (node)=>{
1163
1917
  if (typeof node === 'string') return 'AccountNode';
1164
1918
  if (node instanceof AttachmentID) return 'AttachmentNode';
1165
- if (node != null && typeof node === 'object' && 'model' in node) {
1166
- return this._aliases[node.model];
1167
- }
1919
+ // An explicit type (set on a plugin node above) wins over the document
1920
+ // heuristic, so a plugin node whose source has a `model` key is not
1921
+ // taken for a document.
1168
1922
  if (node != null && typeof node === 'object' && '__typename' in node) {
1169
1923
  return node.__typename;
1170
1924
  }
1925
+ if (node != null && typeof node === 'object' && 'model' in node) {
1926
+ return this._aliases[node.model];
1927
+ }
1171
1928
  throw new Error('Unknown node type');
1172
1929
  });
1173
1930
  const accountObject = new GraphQLObjectType({
@@ -1351,7 +2108,7 @@ export class SchemaBuilder {
1351
2108
  return await ctx.resolveAggregate(scope.modelIDs, {
1352
2109
  filter: scope.filter,
1353
2110
  owner: scope.owner,
1354
- catalogID: scope.catalogID,
2111
+ catalogRef: scope.catalogRef,
1355
2112
  count,
1356
2113
  aggregates
1357
2114
  });
@@ -1393,7 +2150,7 @@ export class SchemaBuilder {
1393
2150
  return await ctx.resolveGroupedAggregate(scope.modelIDs, {
1394
2151
  filter: scope.filter,
1395
2152
  owner: scope.owner,
1396
- catalogID: scope.catalogID,
2153
+ catalogRef: scope.catalogRef,
1397
2154
  count,
1398
2155
  aggregates,
1399
2156
  groupBy,