@kubun/graphql 0.10.1 → 0.12.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 +1,2484 @@
1
- import{AttachmentID as e,DocumentID as t,DocumentModelID as i,decodeID as n,PluginNodeID as s}from"@kubun/id";import{REFERENCE_PREFIX as l}from"@kubun/protocol";import{pascalCase as r}from"change-case";import{extendSchema as o,GraphQLBoolean as a,GraphQLEnumType as u,GraphQLFloat as c,GraphQLID as d,GraphQLInputObjectType as p,GraphQLInt as f,GraphQLInterfaceType as h,GraphQLList as y,GraphQLNonNull as m,GraphQLObjectType as _,GraphQLSchema as g,GraphQLString as b,isEnumType as w,isListType as v,isNonNullType as D,isObjectType as O,isScalarType as I,Kind as $,parse as j}from"graphql";import{connectionArgs as S,connectionDefinitions as F,mutationWithClientMutationId as M,nodeDefinitions as R}from"graphql-relay";import{GraphQLDateTimeISO as T,GraphQLDID as x,GraphQLDuration as A,GraphQLEmailAddress as L,GraphQLJSON as E,GraphQLJSONObject as k}from"graphql-scalars";import{GraphQLAttachmentID as q,GraphQLDocID as B}from"./identifiers.js";import{GraphQLURL as G}from"./scalars.js";import{getUniqueDocValue as N,validateAccessIdentifiers as P}from"./utils.js";function C(e){if(D(e))return C(e.ofType);if(v(e))return[];if(I(e))switch(e.name){case"String":case"ID":return"";case"Int":case"Float":return 0;case"Boolean":return!1;default:return null}if(w(e)){let t=e.getValues();return t[0]?.value??null}return{}}let V={date:b,time:b,"date-time":b,duration:A,email:L,uri:G},Q={attachmentid:q,did:x,docid:B};function H(e){return e.slice(l.length)}function K(e){return v(e)||D(e)&&v(e.ofType)}export function toDocumentModelID(e){return e instanceof i?e:i.fromString(e)}export function getGlobalID(e,t){let i=toDocumentModelID(e);return i.isLocal?i.toGlobal(toDocumentModelID(t)):i}export function toList(e){return new y(new m(e))}export function toOuputList(e){return new y(new m(e))}function U(e){return new p({name:`${e.name}ValueFilter`,isOneOf:!0,fields:{isNull:{type:a},equalTo:{type:e},notEqualTo:{type:e},in:{type:toList(e)},notIn:{type:toList(e)},lessThan:{type:e},lessThanOrEqualTo:{type:e},greaterThan:{type:e},greaterThanOrEqualTo:{type:e}}})}let z={AccountValueFilter:new p({name:"AccountValueFilter",isOneOf:!0,fields:{equalTo:{type:x},notEqualTo:{type:x},in:{type:toList(x)},notIn:{type:toList(x)}}}),BooleanValueFilter:new p({name:"BooleanValueFilter",isOneOf:!0,fields:{isNull:{type:a},equalTo:{type:a}}}),FloatValueFilter:U(c),IntValueFilter:U(f),StringValueFilter:U(b)},W={GraphQLBoolean:"BooleanValueFilter",GraphQLDID:"AccountValueFilter",GraphQLFloat:"FloatValueFilter",GraphQLInt:"IntValueFilter",GraphQLString:"StringValueFilter"},J=new p({name:"PatchSetOperation",fields:{path:{type:new m(b)},value:{type:E}}}),X=new p({name:"PatchRemoveOperation",fields:{path:{type:new m(b)}}}),Y=new p({name:"PatchDestinationOperation",fields:{from:{type:new m(b)},path:{type:new m(b)}}});export const PatchOperation=new p({name:"PatchOperation",isOneOf:!0,fields:{add:{type:J},set:{type:J},remove:{type:X},replace:{type:J},copy:{type:Y},move:{type:Y}}});let Z=new u({name:"OrderByDirection",values:{ASC:{value:"asc"},DESC:{value:"desc"}}}),ee=new _({name:"AccessRule",fields:{level:{type:new m(b)},allowedDIDs:{type:new y(new m(b))},allowedCircles:{type:new y(new m(b))},allowedGroups:{type:new y(new m(b))}}}),et=new _({name:"AccessPermissions",fields:{read:{type:ee},write:{type:ee}}}),ei=new _({name:"ModelAccessDefaults",fields:()=>({ownerDID:{type:new m(b)},modelID:{type:new m(d)},permissions:{type:new m(et)}})});export class SchemaBuilder{_aliases;_aliasToModelID;_aggregateNumericByModel={};_groupKeyNumericByModel={};_definitions;_implemented={};_includedModelIDs;_includeInterfaceDependencies;_includeMutations;_includeRelationDependencies;_includeSubscriptions;_inputObjects={...z,PatchOperation};_mutations={};_nodeModelIDs;_onlyModels;_record;_references={};_relationsTo={};_extensionSDL;_extensionResolvers;_stubExtensionResolvers;_pluginNodeResolvers={};_subscriptions={};_types={OrderByDirection:Z};constructor(e){for(let[t,i]of(this._record=e.record,this._onlyModels=e.onlyModels??null,this._includeInterfaceDependencies=e.includeInterfaceDependencies??!0,this._includeRelationDependencies=e.includeRelationDependencies??!0,this._includeMutations=e.includeMutations??!0,this._includeSubscriptions=e.includeSubscriptions??!0,this._extensionSDL=e.extensionSDL,this._extensionResolvers=e.extensionResolvers,this._stubExtensionResolvers=e.stubExtensionResolvers??!1,this._aliasToModelID=e.aliases??{},this._aliases={},Object.entries(this._aliasToModelID)))this._aliases[i]=t;for(let[t,i]of Object.entries(e.record))if(null==this._aliases[t]){let e=r(i.name);this._aliases[t]=e,this._aliasToModelID[e]??=t}this._includedModelIDs=this._resolveModelIDsToInclude();let t={};for(let[n,s]of Object.entries(e.record)){let e=i.fromString(n);for(let i of(Object.assign(this._references,s.schema.$defs??{}),s.interfaces)){let s=getGlobalID(i,e).toString();t[s]??=new Set,t[s].add(n)}for(let[t,i]of Object.entries(s.fieldsMeta))if("document"===i.type&&null!=i.model){let l=H(s.schema.properties[t]?.$ref),r="array"===this._getReferenceSchema(l).type,o=getGlobalID(i.model,e).toString();this._relationsTo[o]??={},this._relationsTo[o][t]??=[],this._relationsTo[o][t].push({model:n,isList:r})}}let n=new Set;for(let e of Object.keys(t)){let i=new Set;for(let s of(!function e(t,i,n){let s=i[n];if(null==s)throw Error(`Interface ${n} not found in record`);for(let n of s)null!=i[n]?e(t,i,n):t.add(n)}(i,t,e),0!==i.size&&(this._implemented[e]=Array.from(i)),i))n.add(s)}this._nodeModelIDs=Array.from(n)}_resolveModelIdentifier(e){if(null!=this._record[e])return e;let t=this._aliasToModelID[e];if(null!=t&&null!=this._record[t])return t;throw Error(`Model identifier "${e}" not found. Must be a valid model ID or alias.`)}_resolveModelIDsToInclude(){if(null==this._onlyModels||0===this._onlyModels.length)return new Set(Object.keys(this._record));let e=new Set;for(let t of this._onlyModels){let i=this._resolveModelIdentifier(t);e.add(i)}if(!this._includeInterfaceDependencies&&!this._includeRelationDependencies)return e;let t=Array.from(e),i=new Set;for(;t.length>0;){let n=t.pop();if(null==n||i.has(n))continue;i.add(n);let s=this._record[n];if(null!=s){if(this._includeInterfaceDependencies)for(let i of s.interfaces){let s=getGlobalID(i,n).toString();e.has(s)||(e.add(s),t.push(s))}if(this._includeRelationDependencies){for(let i of Object.values(s.fieldsMeta))if("document"===i.type&&null!=i.model&&null!==i.model){let s=getGlobalID(i.model,n).toString();e.has(s)||(e.add(s),t.push(s))}}}}return e}getModelIDs(e){return null===e?this._nodeModelIDs:this._implemented[e]??[e]}build(){let e=this._buildSharedDefinitions();this._definitions=e;let t={nodes:e.nodesField,...e.queryFields};for(let e of this._includedModelIDs){this._buildDocument(e);let i=this._aliases[e],n={type:this._inputObjects[`${e}-filter`]};t[`all${i}`]={type:this._types[`${e}-connection`],args:{...S,filter:n,orderBy:{type:toList(this._inputObjects[`${e}-order`])},search:{type:b}},resolve:async(t,i,n)=>await n.resolveConnection(this.getModelIDs(e),i)}}let i=[...this._includedModelIDs][0];if(null!=i){let n=this._types[`${i}-connection`].getFields().pageInfo.type,s=new _({name:"SearchResultEdge",fields:()=>({cursor:{type:new m(b)},node:{type:new m(e.nodeInterface)},rank:{type:new m(c)}})}),l=new _({name:"SearchResultConnection",fields:()=>({edges:{type:new m(new y(new m(s)))},pageInfo:{type:n}})});t.search={type:new m(l),args:{query:{type:new m(b)},models:{type:new y(new m(b))},first:{type:f},after:{type:b}},resolve:async(e,t,i)=>{let{query:n,models:s,first:l}=t;if(""===n.trim())throw Error("Search query must not be empty");let r=s?s.map(e=>this._aliasToModelID[e]??e):[...this._includedModelIDs],o=await i.searchDocuments(n,r,l??20),a=o.map(e=>e.documentID),u=[...new Set(o.map(e=>e.modelID))],c=new Map((a.length>0?await i.resolveList(u,a):[]).map(e=>[e.id,e])),d=[];for(let e of o){let t=c.get(e.documentID);null!=t&&d.push({cursor:String(d.length),node:t,rank:Math.abs(e.rank)})}return{edges:d,pageInfo:{hasNextPage:!1,hasPreviousPage:!1,startCursor:d.length>0?d[0].cursor:null,endCursor:d.length>0?d[d.length-1].cursor:null}}}}}if(this._includeMutations){this._mutations.setModelAccessDefaults={type:ei,args:{modelID:{type:new m(d)},permissionType:{type:new m(b)},accessLevel:{type:new m(b)},allowedDIDs:{type:toList(b)},allowedCircles:{type:toList(b)},allowedGroups:{type:toList(b)}},resolve:async(e,t,i)=>{let n=t.allowedDIDs??null,s=t.allowedCircles??null,l=t.allowedGroups??null;return P({allowedDIDs:n,allowedCircles:s,allowedGroups:l}),await i.executeSetModelAccessDefaults({modelID:t.modelID,permissionType:t.permissionType,accessLevel:t.accessLevel,allowedDIDs:n,allowedCircles:s,allowedGroups:l})}},this._mutations.removeModelAccessDefaults={type:a,args:{modelID:{type:new m(d)},permissionTypes:{type:new m(toList(b))}},resolve:async(e,t,i)=>(await i.executeRemoveModelAccessDefaults(t.modelID,t.permissionTypes),!0)},this._mutations.setDocumentAccessOverride={type:e.documentInterface,args:{documentID:{type:new m(d)},permissionType:{type:new m(b)},accessLevel:{type:new m(b)},allowedDIDs:{type:toList(b)},allowedCircles:{type:toList(b)},allowedGroups:{type:toList(b)}},resolve:async(e,t,i)=>{let n=t.allowedDIDs??null,s=t.allowedCircles??null,l=t.allowedGroups??null;return P({allowedDIDs:n,allowedCircles:s,allowedGroups:l}),await i.executeSetDocumentAccessOverride({documentID:t.documentID,permissionType:t.permissionType,accessLevel:t.accessLevel,allowedDIDs:n,allowedCircles:s,allowedGroups:l})}},this._mutations.removeDocumentAccessOverride={type:a,args:{documentID:{type:new m(d)},permissionTypes:{type:new m(toList(b))}},resolve:async(e,t,i)=>(await i.executeRemoveDocumentAccessOverride(t.documentID,t.permissionTypes),!0)};let t=new _({name:"BeginTransactionPayload",fields:{transactionID:{type:new m(d)}}}),i=new _({name:"CommitTransactionPayload",fields:{success:{type:new m(a)}}}),n=new _({name:"RollbackTransactionPayload",fields:{success:{type:new m(a)}}});this._mutations.beginTransaction={type:new m(t),args:{},resolve:(e,t,i)=>i.beginTransaction()},this._mutations.commitTransaction={type:new m(i),args:{transactionID:{type:new m(d)}},resolve:async(e,t,i)=>await i.commitTransaction(t.transactionID)},this._mutations.rollbackTransaction={type:new m(n),args:{transactionID:{type:new m(d)}},resolve:(e,t,i)=>i.rollbackTransaction(t.transactionID)}}let n={query:new _({name:"Query",fields:t}),types:[G,x,T]};this._includeMutations&&(n.mutation=new _({name:"Mutation",fields:this._mutations})),this._includeSubscriptions&&(n.subscription=new _({name:"Subscription",fields:()=>{let t={...this._subscriptions};if(this._includeSubscriptions)for(let[i,n]of(t.documentChanged={type:new m(e.documentInterface),args:{id:{type:new m(d)}},resolve:e=>e,subscribe:(e,t,i)=>i.subscribeToDocumentChanged(t.id)},t.documentRemoved={type:new m(d),resolve:e=>e,subscribe:(e,t,i)=>i.subscribeToDocumentRemoved()},Object.entries(this._record))){if(!this._includedModelIDs.has(i))continue;let e=this._aliases[i];for(let s of(t[`new${e}Created`]={type:new m(this._types[i]),args:{owner:{type:d}},resolve:e=>e,subscribe:(e,t,n)=>n.subscribeToDocumentCreated(this.getModelIDs(i),t.owner??void 0)},n.interfaces??[])){let e=getGlobalID(s,i).toString();for(let[i,n]of Object.entries(this._relationsTo[e]??{}))for(let s of n)this._buildRelationSubscriptions(t,e,i,s)}for(let[e,n]of Object.entries(this._relationsTo[i]??{}))for(let s of n)this._includedModelIDs.has(s.model)&&this._buildRelationSubscriptions(t,i,e,s)}return t}}));let s=new g(n);if(null!=this._extensionSDL){let e=new Set(Object.keys(s.getTypeMap()));s=o(s,j(this._extensionSDL)),this._stubExtensionResolvers&&this.#e(s,e)}return null!=this._extensionResolvers&&this.#t(s,this._extensionResolvers),s}#t(e,t){let i=e.getQueryType();if(null!=i&&null!=t.queryFields){let e=i.getFields();for(let[i,n]of Object.entries(t.queryFields)){if(null==e[i])throw Error(`Extension resolver for query field "${i}" has no matching field in SDL`);e[i].resolve=n}}let n=e.getMutationType();if(null!=n&&null!=t.mutationFields){let e=n.getFields();for(let[i,n]of Object.entries(t.mutationFields)){if(null==e[i])throw Error(`Extension resolver for mutation field "${i}" has no matching field in SDL`);e[i].resolve=n}}let s=e.getSubscriptionType();if(null!=s&&null!=t.subscriptionFields){let e=s.getFields();for(let[i,n]of Object.entries(t.subscriptionFields)){if(null==e[i])throw Error(`Extension resolver for subscription field "${i}" has no matching field in SDL`);e[i].resolve=n.resolve,e[i].subscribe=n.subscribe}}if(null!=t.typeFields)for(let[i,n]of Object.entries(t.typeFields)){let t=e.getType(i);if(null==t)throw Error(`Extension resolver references type "${i}" which does not exist in the schema`);if(!O(t))throw Error(`Extension resolver references type "${i}" which is not an object type`);let s=t.getFields();for(let[e,t]of Object.entries(n)){if(null==s[e])throw Error(`Extension resolver for field "${e}" on type "${i}" has no matching field in SDL`);s[e].resolve=t}}if(null!=t.nodeResolvers)for(let[e,i]of Object.entries(t.nodeResolvers))this._pluginNodeResolvers[e]=i}#e(e,t){for(let t of[e.getQueryType(),e.getMutationType()])if(null!=t)for(let e of Object.values(t.getFields()))null==e.resolve&&(e.resolve=()=>C(e.type));for(let[i,n]of Object.entries(e.getTypeMap()))if(!t.has(i)&&O(n)){for(let e of Object.values(n.getFields()))if(null==e.resolve){let t=e.type;e.resolve=i=>i[e.name]??C(t)}}}_getDefinitions(){if(null==this._definitions)throw Error("Definitions must be initialized");return this._definitions}_getReference(e,t){let i=H(e),n=this._types[i];if(null!=n)return n;let s=this._getReferenceSchema(i);return this._buildReference(i,s,t)}_getReferenceSchema(e){let t=this._references[e];if(null==t)throw Error(`Could not find reference: ${e}`);return t}_getReferenceInput(e,t,i=""){let n=H(e),s=this._getReferenceSchema(n);if("array"===s.type)return new y(new m(this._getReferenceInput(s.items.$ref,!1,s.title)));if("object"===s.type){if(s.additionalProperties)return k;let e=t?`${n}-partial`:n,l=this._inputObjects[e];if(null!=l)return l;let o={};for(let[e,i]of Object.entries(s.properties)){let n=this._getReferenceInput(i.$ref,t,s.title);o[e]={type:t||!s.required.includes(e)?n:new m(n)}}let a=this._aliases[n]??r(i+s.title),u=new p({name:t?`Partial${a}Input`:`${a}Input`,fields:o});return this._inputObjects[e]=u,u}return this._buildScalar(n,s,i)}_buildSharedDefinitions(){let i=R(async(i,l)=>{if(i.startsWith("did:"))return i;let r=n(i);if(r instanceof e)return r;if(r instanceof t)return await l.loadDocument(i);if(r instanceof s){let e=this._pluginNodeResolvers[r.typeName];if(null==e)throw Error(`No node resolver registered for plugin type "${r.typeName}"`);let t=await e.resolve(r.localID,l);if(null==t)throw Error(`Node resolver for "${r.typeName}" returned null for localID "${r.localID}"`);return{...t,__typename:r.typeName}}throw Error(`Unsupported node ID: ${i}`)},t=>{if("string"==typeof t)return"AccountNode";if(t instanceof e)return"AttachmentNode";if(null!=t&&"object"==typeof t&&"model"in t)return this._aliases[t.model];if(null!=t&&"object"==typeof t&&"__typename"in t)return t.__typename;throw Error("Unknown node type")}),l=new _({name:"AccountNode",interfaces:[i.nodeInterface],fields:()=>{let e={id:{type:new m(d),description:"Globally unique identifier of the account (DID string)",resolve:e=>e},isViewer:{type:new m(a),description:"Whether the authenticated request issuer is this account or not",resolve:(e,t,i)=>i.getViewer()===e}};for(let[i,n]of Object.entries(this._record))if(this._includedModelIDs.has(i))switch(n.behavior){case"interface":continue;case"default":{let t=this._aliases[i];e[`own${t}Connection`]={type:this._types[`${i}-connection`],args:{...S,filter:{type:this._inputObjects[`${i}-filter`]},orderBy:{type:toList(this._inputObjects[`${i}-order`])}},resolve:async(e,t,n)=>await n.resolveConnection(this.getModelIDs(i),{...t,owner:e})};break}case"unique":{let s=this._aliases[i];e[`own${s}`]={type:this._types[i],args:n.uniqueFields.length?{with:{type:new m(this._buildSetInputObjectType(i,n.uniqueFields))}}:{},resolve:async(e,s,l)=>{let r=N(n.uniqueFields,s.with),o=t.create(i,e,r);return await l.loadDocument(o.toString())}}}}return e}}),r=new _({name:"AttachmentNode",interfaces:[i.nodeInterface],fields:{id:{type:new m(d),resolve:e=>e.toString()},contentLength:{type:new m(f)},mimeType:{type:new m(b)}}}),o={id:{type:new m(d)},model:{type:new m(b)},owner:{type:new m(l)},createdAt:{type:new m(T)},updatedAt:{type:T},accessPermissions:{type:et,resolve:e=>e.data?.accessPermissions??null}},u=new h({name:"DocumentNode",interfaces:[i.nodeInterface],fields:o,resolveType:e=>this._aliases[e.model]}),c={node:i.nodeField,viewer:{type:l,description:"Account issuing the request, if authenticated",resolve:(e,t,i)=>i.getViewer()}};return{...i,accountObject:l,attachmentObject:r,documentInterface:u,documentNodeFields:o,queryFields:c}}_buildDocument(e){if(null!=this._types[e])return this._types[e];let t=this._record[e];if(null==t)throw Error(`Could not find model id: ${e}`);let i=this._getDefinitions();this._aliases[e]??=r(t.name);let n=this._aliases[e],s={name:n,interfaces:()=>[i.documentInterface,i.nodeInterface].concat(t.interfaces.map(t=>this._types[getGlobalID(t,e).toString()]).filter(e=>null!=e)),fields:()=>this._buildDocumentFields(e,t,n)},l="interface"===t.behavior,o=l?new h({...s,resolveType:e=>this._aliases[e.model]}):new _(s),a=this._buildAggregate(e,t,n),u=this._buildGroupedAggregate(e,t,n),{connectionType:c,edgeType:p}=F({nodeType:o,connectionFields:()=>{let t={aggregates:{type:a,resolve:async(t,i,n,s)=>{let l=t.aggregateScope,{count:r,aggregates:o}=this._buildAggregateRequest(e,s);return await n.resolveAggregate(l.modelIDs,{filter:l.filter,owner:l.owner,catalogID:l.catalogID,count:r,aggregates:o})}}};return null!=u&&(t.groupedAggregates={type:new m(toOuputList(u)),args:{groupBy:{type:new m(toList(this._inputObjects[`${e}-group-by`]))},having:{type:this._inputObjects[`${e}-having`]},orderBy:{type:toList(this._inputObjects[`${e}-group-order`])},limit:{type:f}},resolve:async(t,i,n,s)=>{let l=t.aggregateScope,{count:r,aggregates:o}=this._buildGroupedAggregateRequest(e,s),a=(i.groupBy??[]).map(e=>({keys:[e]})),u=this._buildGroupOrderBy(e,i.orderBy);return await n.resolveGroupedAggregate(l.modelIDs,{filter:l.filter,owner:l.owner,catalogID:l.catalogID,count:r,aggregates:o,groupBy:a,having:i.having,orderBy:u.length>0?u:void 0,limit:i.limit??void 0})}}),t}});return this._types[e]=o,this._types[`${e}-connection`]=c,this._types[`${e}-edge`]=p,this._buildDocumentDataObject(e,t,n),this._buildDocumentInput(e,t),this._buildObjectFilterInput(e,t.schema,n,!0),this._buildObjectOrderByInput(e,t.schema,n,!0),!l&&this._includeMutations&&("default"===t.behavior?this._mutations[`create${n}`]=M({name:`Create${n}`,inputFields:()=>({data:{type:new m(this._inputObjects[e])},transactionID:{type:d}}),outputFields:()=>({...i.queryFields,document:{type:this._types[e]}}),mutateAndGetPayload:async(t,i,n)=>({document:await i.executeCreateMutation({modelID:e,data:t.data,info:n,transactionID:t.transactionID})})}):"unique"===t.behavior&&(this._mutations[`set${n}`]=M({name:`Set${n}`,inputFields:()=>({data:{type:new m(this._inputObjects[e])},transactionID:{type:d}}),outputFields:()=>({...i.queryFields,document:{type:this._types[e]}}),mutateAndGetPayload:async(i,n,s)=>{let l=N(t.uniqueFields,i.data);return{document:await n.executeSetMutation({modelID:e,unique:l,data:i.data,info:s,transactionID:i.transactionID})}}})),this._mutations[`update${n}`]=M({name:`Update${n}`,inputFields:()=>({id:{type:new m(d)},patch:{type:new m(toList(PatchOperation))},from:{type:this._inputObjects[`${e}-update`]},transactionID:{type:d}}),outputFields:()=>({...i.queryFields,document:{type:this._types[e]}}),mutateAndGetPayload:async(e,t,i)=>({document:await t.executeUpdateMutation({input:e,info:i,transactionID:e.transactionID})})}),this._mutations[`remove${n}`]=M({name:`Remove${n}`,inputFields:()=>({id:{type:new m(d)},transactionID:{type:d}}),outputFields:()=>({...i.queryFields,id:{type:new m(d)}}),mutateAndGetPayload:async(e,t,i)=>(await t.executeRemoveMutation({id:e.id,info:i,transactionID:e.transactionID}),{id:e.id})})),o}_buildAggregate(e,t,i){let n=this._types[`${e}-aggregate`];if(null!=n)return n;let s={},l={},r=new Map;for(let[e,i]of Object.entries(t.schema.properties??{})){let t=i.$ref;if(null==t)continue;let n=H(t),o=this._getReferenceSchema(n);if("object"===o.type||"array"===o.type)continue;let a=this._buildScalar(n,o),u=a===f||a===c,d=a===b;u&&(s[e]={type:a}),(u||d)&&(l[e]={type:a},r.set(e,u))}this._aggregateNumericByModel[e]=r;let o=Object.keys(s),a={count:{type:new m(f),resolve:e=>e.count??0}};if(o.length>0){let t=new _({name:`${i}SumFields`,fields:s}),n={};for(let e of o)n[e]={type:c};let l=new _({name:`${i}AvgFields`,fields:n});this._types[`${e}-aggregate-sum`]=t,this._types[`${e}-aggregate-avg`]=l,a.sum={type:t,resolve:e=>e.sum??null},a.average={type:l,resolve:e=>e.average??null}}if(Object.keys(l).length>0){let t=new _({name:`${i}MinMaxFields`,fields:l});this._types[`${e}-aggregate-min-max`]=t,a.min={type:t,resolve:e=>e.min??null},a.max={type:t,resolve:e=>e.max??null}}let u=new _({name:`${i}Aggregate`,fields:a});return this._types[`${e}-aggregate`]=u,u}_buildGroupedAggregate(e,t,i){let n=this._types[`${e}-grouped-aggregate`];if(null!=n)return n;let s=new Map,l=new Map;for(let[e,i]of Object.entries(t.schema.properties??{})){let t=i.$ref;if(null==t)continue;let n=H(t),r=this._getReferenceSchema(n);if("object"===r.type||"array"===r.type)continue;let o=this._buildScalar(n,r);s.set(e,o),l.set(e,o===f||o===c)}if(this._groupKeyNumericByModel[e]=l,0===s.size)return null;let r=this._types[`${e}-aggregate-sum`],o=this._types[`${e}-aggregate-avg`],a=this._types[`${e}-aggregate-min-max`],d={};for(let e of s.keys())d[e]={value:e};let h=new u({name:`${i}GroupBy`,values:d});this._types[`${e}-group-by`]=h,this._inputObjects[`${e}-group-by`]=h;let y={};for(let[e,t]of s)y[e]={type:t};let g=new _({name:`${i}GroupKeys`,fields:y});this._types[`${e}-group-keys`]=g;let b={keys:{type:new m(g),resolve:e=>e.keys},count:{type:new m(f),resolve:e=>e.count??0}};null!=r&&null!=o&&(b.sum={type:r,resolve:e=>e.sum??null},b.average={type:o,resolve:e=>e.average??null}),null!=a&&(b.min={type:a,resolve:e=>e.min??null},b.max={type:a,resolve:e=>e.max??null});let w=new _({name:`${i}GroupedAggregate`,fields:b});this._types[`${e}-grouped-aggregate`]=w;let v=e=>this._inputObjects[`${e.name}ValueFilter`],D={count:{type:this._inputObjects.IntValueFilter}};if(null!=r&&null!=o){let t={},n={};for(let[e,i]of Object.entries(r.getFields()))t[e]={type:v(i.type)},n[e]={type:this._inputObjects.FloatValueFilter};let s=new p({name:`${i}HavingSum`,fields:t}),l=new p({name:`${i}HavingAverage`,fields:n});this._inputObjects[`${e}-having-sum`]=s,this._inputObjects[`${e}-having-average`]=l,D.sum={type:s},D.average={type:l}}if(null!=a){let t={},n={};for(let[e,i]of Object.entries(a.getFields()))t[e]={type:v(i.type)},n[e]={type:v(i.type)};let s=new p({name:`${i}HavingMin`,fields:t}),l=new p({name:`${i}HavingMax`,fields:n});this._inputObjects[`${e}-having-min`]=s,this._inputObjects[`${e}-having-max`]=l,D.min={type:s},D.max={type:l}}let O=new p({name:`${i}Having`,fields:D});this._inputObjects[`${e}-having`]=O;let I=(e,t)=>{let i={};for(let e of t)i[e]={type:Z};return new p({name:e,fields:i})},$=I(`${i}GroupKeysOrderBy`,s.keys());this._inputObjects[`${e}-group-keys-order`]=$;let j={keys:{type:$},count:{type:Z}};if(null!=r&&null!=o){let t=I(`${i}SumFieldsOrderBy`,Object.keys(r.getFields())),n=I(`${i}AvgFieldsOrderBy`,Object.keys(o.getFields()));this._inputObjects[`${e}-sum-order`]=t,this._inputObjects[`${e}-avg-order`]=n,j.sum={type:t},j.average={type:n}}if(null!=a){let t=I(`${i}MinFieldsOrderBy`,Object.keys(a.getFields())),n=I(`${i}MaxFieldsOrderBy`,Object.keys(a.getFields()));this._inputObjects[`${e}-min-order`]=t,this._inputObjects[`${e}-max-order`]=n,j.min={type:t},j.max={type:n}}let S=new p({name:`${i}GroupOrderBy`,fields:j});return this._inputObjects[`${e}-group-order`]=S,w}_buildAggregateRequest(e,t){let i=this._aggregateNumericByModel[e]??new Map,n=["sum","average","min","max"],s=!1,l=[];for(let e of t.fieldNodes){let r=e.selectionSet?.selections??[];for(let e of this._expandFieldSelections(r,t,new Set)){let r=e.name.value;if("count"===r){s=!0;continue}if(!n.includes(r))continue;let o=e.selectionSet?.selections??[];for(let e of this._expandFieldSelections(o,t,new Set)){let t=e.name.value;l.push({op:r,keys:[t],numeric:i.get(t)??!1})}}}return{count:s,aggregates:l}}_buildGroupedAggregateRequest(e,t){let i=this._aggregateNumericByModel[e]??new Map,n=["sum","average","min","max"],s=!1,l=[];for(let e of t.fieldNodes){let r=e.selectionSet?.selections??[];for(let e of this._expandFieldSelections(r,t,new Set)){let r=e.name.value;if("count"===r){s=!0;continue}if("keys"===r||!n.includes(r))continue;let o=e.selectionSet?.selections??[];for(let e of this._expandFieldSelections(o,t,new Set)){let t=e.name.value;l.push({op:r,keys:[t],numeric:i.get(t)??!1})}}}return{count:s,aggregates:l}}_buildGroupOrderBy(e,t){if(null==t)return[];let i=this._aggregateNumericByModel[e]??new Map,n=this._groupKeyNumericByModel[e]??new Map,s=[];for(let e of t){for(let[t,i]of(null!=e.count&&s.push({path:["count"],direction:e.count,numeric:!0}),Object.entries(e.keys??{})))s.push({path:["keys",t],direction:i,numeric:n.get(t)??!1});for(let[t,n]of Object.entries(e.sum??{}))s.push({path:["sum",t],direction:n,numeric:i.get(t)??!1});for(let[t,i]of Object.entries(e.average??{}))s.push({path:["average",t],direction:i,numeric:!0});for(let[t,n]of Object.entries(e.min??{}))s.push({path:["min",t],direction:n,numeric:i.get(t)??!1});for(let[t,n]of Object.entries(e.max??{}))s.push({path:["max",t],direction:n,numeric:i.get(t)??!1})}return s}*_expandFieldSelections(e,t,i){for(let n of e)switch(n.kind){case $.FIELD:yield n;break;case $.INLINE_FRAGMENT:yield*this._expandFieldSelections(n.selectionSet.selections,t,i);break;case $.FRAGMENT_SPREAD:{let e=n.name.value;if(i.has(e))break;let s=t.fragments[e];if(null==s)break;i.add(e),yield*this._expandFieldSelections(s.selectionSet.selections,t,i)}}}_buildDocumentFields(t,i,n){let s=this._buildDocumentDataObject(t,i,n),l=this._getDefinitions(),r={...l.documentNodeFields,data:{type:new m(s)}};for(let[n,o]of Object.entries(s.getFields())){let s=i.fieldsMeta[n];if(null!=s)switch(s.type){case"account":K(o.type)?r[`${n}Accounts`]={type:new m(toOuputList(l.accountObject)),resolve:e=>e.data?.[n]??[]}:r[`${n}Account`]={type:l.accountObject,resolve:e=>e.data?.[n]??null};break;case"attachment":K(o.type)?r[`${n}Attachments`]={type:new m(toOuputList(l.attachmentObject)),resolve:t=>(t.data?.[n]??[]).map(e.fromString)}:r[`${n}Attachment`]={type:l.attachmentObject,resolve:t=>{let i=t.data?.[n];return i?e.fromString(i):null}};break;case"document":{let e,i,a=s.model;if(void 0===a)continue;null===a?(e=null,i=l.documentInterface):(e=getGlobalID(a,t).toString(),this._includedModelIDs.has(e)?i=this._buildDocument(e):(i=l.documentInterface,e=null)),K(o.type)?r[`${n}Documents`]={type:new m(toOuputList(i)),resolve:async(t,i,s)=>{let l=t.data?.[n]??[];return l.length?await s.resolveList(this.getModelIDs(e),l):[]}}:r[`${n}Document`]={type:i,resolve:async(e,t,i)=>{let s=e.data?.[n];return s?await i.loadDocument(s):null}}}}}for(let e of i.interfaces??[]){let i=getGlobalID(e,t).toString();for(let[e,t]of Object.entries(this._relationsTo[i]??{}))for(let i of t)this._includedModelIDs.has(i.model)&&this._buildRelationFields(r,e,i)}for(let[e,i]of Object.entries(this._relationsTo[t]??{}))for(let t of i)this._includedModelIDs.has(t.model)&&this._buildRelationFields(r,e,t);return r}_buildDocumentDataObject(e,t,i){let n=`${e}-data`;if(null!=this._types[n])return this._types[n];let s=t.schema.required??[],l={};for(let[i,n]of Object.entries(t.schema.properties??{})){let t=n.$ref;if(null==t)throw Error(`Missing ref for field ${i} of object ${e}`);let r=this._getReference(t);l[i]={type:s.includes(i)?new m(r):r}}let r={name:`${i}Data`,interfaces:()=>t.interfaces.map(t=>{let i=`${getGlobalID(t,e).toString()}-data`;return this._types[i]}).filter(e=>null!=e),fields:l},o="interface"===t.behavior?new h(r):new _(r);return this._types[n]=o,o}_buildDocumentInput(e,t){if("interface"===t.behavior)return;let i={},n={};for(let[e,s]of Object.entries(t.schema.properties)){let l=s.$ref,r=this._getReferenceInput(l,!1);i[e]={type:t.schema.required.includes(e)?new m(r):r};let o=H(l);null==this._getReferenceSchema(o).const&&(n[e]={type:this._getReferenceInput(l,!0)})}let s=this._aliases[e];this._inputObjects[e]=new p({name:`${s}Input`,fields:i}),this._inputObjects[`${e}-update`]=new p({name:`Partial${s}Input`,fields:n})}_buildReferenceFilterInput(e,t=""){let i=H(e),n=this._getReferenceSchema(i),s=t;if("object"===n.type)return n.additionalProperties?null:this._buildObjectFilterInput(i,n,t);if("array"===n.type){let e=this._aliases[i]??r(t+n.title);return s=e,new p({name:`${e}Filter`,isOneOf:!0,fields:{isNull:{type:a}}})}let l=this._buildScalar(i,n,s),o=W[l.name];if(null!=o)return this._inputObjects[o];let u=w(l)?new p({name:`${l.name}ValueFilter`,isOneOf:!0,fields:{isNull:{type:a},equalTo:{type:l},notEqualTo:{type:l},in:{type:toList(l)},notIn:{type:toList(l)}}}):U(l);return null==this._inputObjects[u.name]&&(this._inputObjects[u.name]=u),this._inputObjects[u.name]}_buildReferenceOrderByInput(e,t=""){let i=H(e),n=this._getReferenceSchema(i);switch(n.type){case"array":return null;case"object":return n.additionalProperties?null:this._buildObjectOrderByInput(i,n,t);default:return Z}}_buildObjectFilterInput(e,t,i="",n=!1){let s=`${e}-filter`,l=this._inputObjects[s];if(null!=l)return l;let o=this._aliases[e]??r(i+(t.title??"")),a=n?{_owner:{type:this._inputObjects.AccountValueFilter}}:{};for(let[e,i]of Object.entries(t.properties)){let t=this._buildReferenceFilterInput(i.$ref,o);null!==t&&(a[e]={type:t})}let u=new p({name:`${o}ObjectFilter`,fields:a}),c=new p({name:`${o}Filter`,isOneOf:!0,fields:()=>({where:{type:u},and:{type:toList(this._inputObjects[s])},or:{type:toList(this._inputObjects[s])},not:{type:this._inputObjects[s]}})});return this._inputObjects[s]=c,c}_buildObjectOrderByInput(e,t,i="",n=!1){let s=`${e}-order`,l=this._inputObjects[s];if(null!=l)return l;let o=this._aliases[e]??r(i+(t.title??"")),a=n?{_docOwner:{type:Z},_createdAt:{type:Z}}:{};for(let[e,i]of Object.entries(t.properties)){let t=this._buildReferenceOrderByInput(i.$ref,o);null!=t&&(a[e]={type:t})}let u=new p({name:`${o}OrderBy`,isOneOf:!0,fields:a});return this._inputObjects[s]=u,u}_buildSetInputObjectType(e,t,i){let n=i?r(i):"",s=`${e}-with-${n}`,l=this._inputObjects[s];if(null!=l)return l;let o=this._record[e],a=this._aliases[e];return this._inputObjects[s]=new p({name:`With${n}${a}Input`,fields:()=>{let e={};for(let i of t){let t=o.schema.properties[i];if(null==t)throw Error(`Field ${i} not found on model ${a}`);let n=this._getReferenceInput(t.$ref,!1);e[i]={type:new m(n)}}return e}}),this._inputObjects[s]}_buildReference(e,t,i=""){if(null!=this._types[e])return this._types[e];switch(t.type){case"array":return this._types[e]=this._buildArray(t,i),this._types[e];case"object":return this._types[e]=t.additionalProperties?k:this._buildObject(e,t,i),this._types[e];default:return this._buildScalar(e,t,i)}}_buildScalar(e,t,i=""){if(null!=this._types[e])return this._types[e];switch(t.type){case"boolean":this._types[e]=a;break;case"integer":this._types[e]=f;break;case"number":this._types[e]=c;break;case"string":this._types[e]=this._buildString(t,i)}return this._types[e]}_buildArray(e,t=""){let i=e.items.$ref;return new y(new m(this._getReference(i,r(t+e.title))))}_buildObject(e,t,i=""){let n=r(i+t.title),s=this._buildObjectFields(e,t,n);return new _({name:n,fields:s})}_buildObjectFields(e,t,i=""){let n=t.required??[],s={};for(let[l,r]of Object.entries(t.properties??{})){let t=r.$ref;if(null==t)throw Error(`Missing ref for field ${l} of object ${e}`);let o=this._getReference(t,i);s[l]={type:n.includes(l)?new m(o):o}}return s}_buildRelationFields(e,t,i){if(i.isList)return;let n=i.model,s=`${r(t)}Of${this._aliases[n]}`;e[`in${s}`]={type:new m(this._types[`${n}-connection`]),args:{...S,filter:{type:this._inputObjects[`${n}-filter`]},orderBy:{type:toList(this._inputObjects[`${n}-order`])}},resolve:(e,i,s)=>{let l={where:{[t]:{equalTo:e.id}}},r=i.filter?{and:[i.filter,l]}:l;return s.resolveConnection(this.getModelIDs(n),{...i,filter:r})}}}_buildRelationSubscriptions(e,t,i,n){if(n.isList)return;let s=r(i);e[`edge${this._aliases[n.model]}AddedAs${s}To${this._aliases[t]}`]={type:new m(this._types[`${n.model}-edge`]),args:{id:{type:new m(d)}},resolve:e=>e,subscribe:(e,t,s)=>s.subscribeToDocumentEdgeAdded(this.getModelIDs(n.model),e=>e[i]===t.id)},e[`edge${this._aliases[n.model]}RemovedAs${s}From${this._aliases[t]}`]={type:new m(d),args:{id:{type:new m(d)}},resolve:e=>e,subscribe:(e,t,s)=>s.subscribeToDocumentEdgeRemoved(this.getModelIDs(n.model),e=>e[i]===t.id)}}_buildString(e,t){return null!=e.const?b:null!=e.enum?this._buildEnumString(e,t):null!=e.format?this._buildFormatString(e):this._buildCustomString(e)}_buildCustomString(e){return null==e.title?b:Q[e.title.toLowerCase()]??b}_buildEnumString(e,t=""){let i=r(t+e.title),n=`${i}-enum`;if(null==this._types[n]){let t={};for(let i of e.enum)t[i]={value:i};this._types[n]=new u({name:i,values:t})}return this._types[n]}_buildFormatString(e){let t=V[e.format];if(null==t)throw Error(`Unsupported string format: ${e.format}`);return t}}export function createSchema(e){return new SchemaBuilder(e).build()}
1
+ import { AttachmentID, DocumentID, DocumentModelID, decodeID, PluginNodeID } from '@kubun/id';
2
+ import { REFERENCE_PREFIX } from '@kubun/protocol';
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';
5
+ import { connectionArgs, connectionDefinitions, mutationWithClientMutationId, nodeDefinitions } from 'graphql-relay';
6
+ import { GraphQLDateTimeISO, GraphQLDID, GraphQLDuration, GraphQLEmailAddress, GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
7
+ import { GraphQLAttachmentID, GraphQLDocID } from './identifiers.js';
8
+ import { GraphQLURL } from './scalars.js';
9
+ import { getUniqueDocValue, validateAccessIdentifiers } from './utils.js';
10
+ /**
11
+ * Create a type-appropriate default value for stubbing extension field resolvers.
12
+ * Used during client-side pre-execution where extension fields don't need real resolution.
13
+ */ function createDefaultValue(type) {
14
+ if (isNonNullType(type)) return createDefaultValue(type.ofType);
15
+ if (isListType(type)) return [];
16
+ if (isScalarType(type)) {
17
+ switch(type.name){
18
+ case 'String':
19
+ case 'ID':
20
+ return '';
21
+ case 'Int':
22
+ case 'Float':
23
+ return 0;
24
+ case 'Boolean':
25
+ return false;
26
+ default:
27
+ return null;
28
+ }
29
+ }
30
+ if (isEnumType(type)) {
31
+ const values = type.getValues();
32
+ return values[0]?.value ?? null;
33
+ }
34
+ // Object types: return empty object — sub-fields use their own stub resolvers
35
+ return {};
36
+ }
37
+ const STRING_FORMATS = {
38
+ date: GraphQLString,
39
+ time: GraphQLString,
40
+ 'date-time': GraphQLString,
41
+ duration: GraphQLDuration,
42
+ email: GraphQLEmailAddress,
43
+ uri: GraphQLURL
44
+ };
45
+ const STRING_TITLES = {
46
+ attachmentid: GraphQLAttachmentID,
47
+ did: GraphQLDID,
48
+ docid: GraphQLDocID
49
+ };
50
+ function getReferenceID(path) {
51
+ return path.slice(REFERENCE_PREFIX.length);
52
+ }
53
+ function isList(type) {
54
+ return isListType(type) || isNonNullType(type) && isListType(type.ofType);
55
+ }
56
+ function collectAllModelIDs(collected, record, interfaceID) {
57
+ const implementedBy = record[interfaceID];
58
+ if (implementedBy == null) {
59
+ throw new Error(`Interface ${interfaceID} not found in record`);
60
+ }
61
+ for (const id of implementedBy){
62
+ // Check if this id is itself an interface (exists in record with implementers)
63
+ if (record[id] != null) {
64
+ // The id is an interface that has implementers, resolve recursively
65
+ collectAllModelIDs(collected, record, id);
66
+ } else {
67
+ // The id is a concrete model
68
+ collected.add(id);
69
+ }
70
+ }
71
+ }
72
+ export function toDocumentModelID(id) {
73
+ return id instanceof DocumentModelID ? id : DocumentModelID.fromString(id);
74
+ }
75
+ export function getGlobalID(id, globalID) {
76
+ const docModelID = toDocumentModelID(id);
77
+ return docModelID.isLocal ? docModelID.toGlobal(toDocumentModelID(globalID)) : docModelID;
78
+ }
79
+ export function toList(type) {
80
+ return new GraphQLList(new GraphQLNonNull(type));
81
+ }
82
+ export function toOuputList(type) {
83
+ return new GraphQLList(new GraphQLNonNull(type));
84
+ }
85
+ function createEnumValueFilterInput(type) {
86
+ return new GraphQLInputObjectType({
87
+ name: `${type.name}ValueFilter`,
88
+ isOneOf: true,
89
+ fields: {
90
+ isNull: {
91
+ type: GraphQLBoolean
92
+ },
93
+ equalTo: {
94
+ type
95
+ },
96
+ notEqualTo: {
97
+ type
98
+ },
99
+ in: {
100
+ type: toList(type)
101
+ },
102
+ notIn: {
103
+ type: toList(type)
104
+ }
105
+ }
106
+ });
107
+ }
108
+ function createScalarValueFilterInput(type) {
109
+ return new GraphQLInputObjectType({
110
+ name: `${type.name}ValueFilter`,
111
+ isOneOf: true,
112
+ fields: {
113
+ isNull: {
114
+ type: GraphQLBoolean
115
+ },
116
+ equalTo: {
117
+ type
118
+ },
119
+ notEqualTo: {
120
+ type
121
+ },
122
+ in: {
123
+ type: toList(type)
124
+ },
125
+ notIn: {
126
+ type: toList(type)
127
+ },
128
+ lessThan: {
129
+ type
130
+ },
131
+ lessThanOrEqualTo: {
132
+ type
133
+ },
134
+ greaterThan: {
135
+ type
136
+ },
137
+ greaterThanOrEqualTo: {
138
+ type
139
+ }
140
+ }
141
+ });
142
+ }
143
+ const VALUE_FILTER_INPUTS = {
144
+ AccountValueFilter: new GraphQLInputObjectType({
145
+ name: 'AccountValueFilter',
146
+ isOneOf: true,
147
+ fields: {
148
+ equalTo: {
149
+ type: GraphQLDID
150
+ },
151
+ notEqualTo: {
152
+ type: GraphQLDID
153
+ },
154
+ in: {
155
+ type: toList(GraphQLDID)
156
+ },
157
+ notIn: {
158
+ type: toList(GraphQLDID)
159
+ }
160
+ }
161
+ }),
162
+ BooleanValueFilter: new GraphQLInputObjectType({
163
+ name: 'BooleanValueFilter',
164
+ isOneOf: true,
165
+ fields: {
166
+ isNull: {
167
+ type: GraphQLBoolean
168
+ },
169
+ equalTo: {
170
+ type: GraphQLBoolean
171
+ }
172
+ }
173
+ }),
174
+ FloatValueFilter: createScalarValueFilterInput(GraphQLFloat),
175
+ IntValueFilter: createScalarValueFilterInput(GraphQLInt),
176
+ StringValueFilter: createScalarValueFilterInput(GraphQLString)
177
+ };
178
+ const VALUE_FILTER_INPUT_TYPES = {
179
+ GraphQLBoolean: 'BooleanValueFilter',
180
+ GraphQLDID: 'AccountValueFilter',
181
+ GraphQLFloat: 'FloatValueFilter',
182
+ GraphQLInt: 'IntValueFilter',
183
+ GraphQLString: 'StringValueFilter'
184
+ };
185
+ const PatchSetOperation = new GraphQLInputObjectType({
186
+ name: 'PatchSetOperation',
187
+ fields: {
188
+ path: {
189
+ type: new GraphQLNonNull(GraphQLString)
190
+ },
191
+ value: {
192
+ type: GraphQLJSON
193
+ }
194
+ }
195
+ });
196
+ const PatchRemoveOperation = new GraphQLInputObjectType({
197
+ name: 'PatchRemoveOperation',
198
+ fields: {
199
+ path: {
200
+ type: new GraphQLNonNull(GraphQLString)
201
+ }
202
+ }
203
+ });
204
+ const PatchDestinationOperation = new GraphQLInputObjectType({
205
+ name: 'PatchDestinationOperation',
206
+ fields: {
207
+ from: {
208
+ type: new GraphQLNonNull(GraphQLString)
209
+ },
210
+ path: {
211
+ type: new GraphQLNonNull(GraphQLString)
212
+ }
213
+ }
214
+ });
215
+ export const PatchOperation = new GraphQLInputObjectType({
216
+ name: 'PatchOperation',
217
+ isOneOf: true,
218
+ fields: {
219
+ add: {
220
+ type: PatchSetOperation
221
+ },
222
+ set: {
223
+ type: PatchSetOperation
224
+ },
225
+ remove: {
226
+ type: PatchRemoveOperation
227
+ },
228
+ replace: {
229
+ type: PatchSetOperation
230
+ },
231
+ copy: {
232
+ type: PatchDestinationOperation
233
+ },
234
+ move: {
235
+ type: PatchDestinationOperation
236
+ }
237
+ }
238
+ });
239
+ const OrderByDirection = new GraphQLEnumType({
240
+ name: 'OrderByDirection',
241
+ values: {
242
+ ASC: {
243
+ value: 'asc'
244
+ },
245
+ DESC: {
246
+ value: 'desc'
247
+ }
248
+ }
249
+ });
250
+ const AccessRule = new GraphQLObjectType({
251
+ name: 'AccessRule',
252
+ fields: {
253
+ level: {
254
+ type: new GraphQLNonNull(GraphQLString)
255
+ },
256
+ allowedDIDs: {
257
+ type: new GraphQLList(new GraphQLNonNull(GraphQLString))
258
+ },
259
+ allowedCircles: {
260
+ type: new GraphQLList(new GraphQLNonNull(GraphQLString))
261
+ },
262
+ allowedGroups: {
263
+ type: new GraphQLList(new GraphQLNonNull(GraphQLString))
264
+ }
265
+ }
266
+ });
267
+ const AccessPermissions = new GraphQLObjectType({
268
+ name: 'AccessPermissions',
269
+ fields: {
270
+ read: {
271
+ type: AccessRule
272
+ },
273
+ write: {
274
+ type: AccessRule
275
+ }
276
+ }
277
+ });
278
+ const ModelAccessDefaults = new GraphQLObjectType({
279
+ name: 'ModelAccessDefaults',
280
+ fields: ()=>({
281
+ ownerDID: {
282
+ type: new GraphQLNonNull(GraphQLString)
283
+ },
284
+ modelID: {
285
+ type: new GraphQLNonNull(GraphQLID)
286
+ },
287
+ permissions: {
288
+ type: new GraphQLNonNull(AccessPermissions)
289
+ }
290
+ })
291
+ });
292
+ export class SchemaBuilder {
293
+ // Mapping of model or references IDs to their GraphQL object names
294
+ _aliases;
295
+ _aliasToModelID;
296
+ // Per-model map of aggregatable field key -> whether it is numeric. Drives the
297
+ // SQL numeric cast so JSON-extracted values compare arithmetically ('9' < '10')
298
+ // rather than lexically. Populated by `_buildAggregate`.
299
+ _aggregateNumericByModel = {};
300
+ // Per-model map of groupable field key -> whether it is numeric. Broader than
301
+ // aggregate eligibility (includes boolean and enum fields). Drives the SQL numeric
302
+ // cast on group keys so JSON-extracted values compare arithmetically rather than
303
+ // lexically. Populated by `_buildGroupedAggregate`.
304
+ _groupKeyNumericByModel = {};
305
+ _definitions;
306
+ _implemented = {};
307
+ _includedModelIDs;
308
+ _includeInterfaceDependencies;
309
+ _includeMutations;
310
+ _includeRelationDependencies;
311
+ _includeSubscriptions;
312
+ _inputObjects = {
313
+ ...VALUE_FILTER_INPUTS,
314
+ PatchOperation
315
+ };
316
+ _mutations = {};
317
+ _nodeModelIDs;
318
+ _onlyModels;
319
+ _record;
320
+ _references = {};
321
+ _relationsTo = {};
322
+ _extensionSDL;
323
+ _extensionResolvers;
324
+ _stubExtensionResolvers;
325
+ _pluginNodeResolvers = {};
326
+ _subscriptions = {};
327
+ _types = {
328
+ OrderByDirection
329
+ };
330
+ constructor(params){
331
+ this._record = params.record;
332
+ this._onlyModels = params.onlyModels ?? null;
333
+ this._includeInterfaceDependencies = params.includeInterfaceDependencies ?? true;
334
+ this._includeRelationDependencies = params.includeRelationDependencies ?? true;
335
+ this._includeMutations = params.includeMutations ?? true;
336
+ this._includeSubscriptions = params.includeSubscriptions ?? true;
337
+ this._extensionSDL = params.extensionSDL;
338
+ this._extensionResolvers = params.extensionResolvers;
339
+ this._stubExtensionResolvers = params.stubExtensionResolvers ?? false;
340
+ // params.aliases is { aliasName: modelID }, which is what we need for _aliasToModelID
341
+ this._aliasToModelID = params.aliases ?? {};
342
+ // Build reverse lookup map (modelID -> alias) for internal use
343
+ this._aliases = {};
344
+ for (const [alias, modelID] of Object.entries(this._aliasToModelID)){
345
+ this._aliases[modelID] = alias;
346
+ }
347
+ // Populate fallback aliases from model names for any models without explicit aliases
348
+ for (const [id, model] of Object.entries(params.record)){
349
+ if (this._aliases[id] == null) {
350
+ const name = pascalCase(model.name);
351
+ this._aliases[id] = name;
352
+ this._aliasToModelID[name] ??= id;
353
+ }
354
+ }
355
+ // Compute which models to include
356
+ this._includedModelIDs = this._resolveModelIDsToInclude();
357
+ const referencedInterfaces = {};
358
+ for (const [modelIDstring, model] of Object.entries(params.record)){
359
+ const modelID = DocumentModelID.fromString(modelIDstring);
360
+ Object.assign(this._references, model.schema.$defs ?? {});
361
+ // Extract interfaces
362
+ for (const iid of model.interfaces){
363
+ const interfaceID = getGlobalID(iid, modelID).toString();
364
+ referencedInterfaces[interfaceID] ??= new Set();
365
+ referencedInterfaces[interfaceID].add(modelIDstring);
366
+ }
367
+ // Extract inverse relations
368
+ for (const [fieldName, meta] of Object.entries(model.fieldsMeta)){
369
+ if (meta.type === 'document' && meta.model != null) {
370
+ const relationRefID = getReferenceID(model.schema.properties[fieldName]?.$ref);
371
+ const isList = this._getReferenceSchema(relationRefID).type === 'array';
372
+ const relationID = getGlobalID(meta.model, modelID).toString();
373
+ this._relationsTo[relationID] ??= {};
374
+ this._relationsTo[relationID][fieldName] ??= [];
375
+ this._relationsTo[relationID][fieldName].push({
376
+ model: modelIDstring,
377
+ isList
378
+ });
379
+ }
380
+ }
381
+ }
382
+ // Map interface IDs to all the models implementing them
383
+ const nodeModelIDs = new Set();
384
+ for (const id of Object.keys(referencedInterfaces)){
385
+ const implementedBy = new Set();
386
+ collectAllModelIDs(implementedBy, referencedInterfaces, id);
387
+ if (implementedBy.size !== 0) {
388
+ this._implemented[id] = Array.from(implementedBy);
389
+ }
390
+ for (const modelID of implementedBy){
391
+ nodeModelIDs.add(modelID);
392
+ }
393
+ }
394
+ this._nodeModelIDs = Array.from(nodeModelIDs);
395
+ }
396
+ _resolveModelIdentifier(identifier) {
397
+ // Check if it's a model ID (exists in record)
398
+ if (this._record[identifier] != null) {
399
+ return identifier;
400
+ }
401
+ // Check if it's an alias
402
+ const modelID = this._aliasToModelID[identifier];
403
+ if (modelID != null && this._record[modelID] != null) {
404
+ return modelID;
405
+ }
406
+ throw new Error(`Model identifier "${identifier}" not found. Must be a valid model ID or alias.`);
407
+ }
408
+ _resolveModelIDsToInclude() {
409
+ // If no restriction, include all models
410
+ if (this._onlyModels == null || this._onlyModels.length === 0) {
411
+ return new Set(Object.keys(this._record));
412
+ }
413
+ // Resolve identifiers to model IDs
414
+ const included = new Set();
415
+ for (const identifier of this._onlyModels){
416
+ const modelID = this._resolveModelIdentifier(identifier);
417
+ included.add(modelID);
418
+ }
419
+ // If both dependencies are disabled, return early
420
+ if (!this._includeInterfaceDependencies && !this._includeRelationDependencies) {
421
+ return included;
422
+ }
423
+ // Recursively collect dependencies
424
+ const toProcess = Array.from(included);
425
+ const processed = new Set();
426
+ while(toProcess.length > 0){
427
+ const modelID = toProcess.pop();
428
+ if (modelID == null || processed.has(modelID)) continue;
429
+ processed.add(modelID);
430
+ const model = this._record[modelID];
431
+ if (model == null) continue;
432
+ // Add interface dependencies if enabled
433
+ if (this._includeInterfaceDependencies) {
434
+ for (const iid of model.interfaces){
435
+ const interfaceID = getGlobalID(iid, modelID).toString();
436
+ if (!included.has(interfaceID)) {
437
+ included.add(interfaceID);
438
+ toProcess.push(interfaceID);
439
+ }
440
+ }
441
+ }
442
+ // Add relation dependencies if enabled
443
+ if (this._includeRelationDependencies) {
444
+ for (const meta of Object.values(model.fieldsMeta)){
445
+ if (meta.type === 'document' && meta.model != null && meta.model !== null) {
446
+ const relationID = getGlobalID(meta.model, modelID).toString();
447
+ if (!included.has(relationID)) {
448
+ included.add(relationID);
449
+ toProcess.push(relationID);
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ return included;
456
+ }
457
+ getModelIDs(id) {
458
+ return id === null ? this._nodeModelIDs : this._implemented[id] ?? [
459
+ id
460
+ ];
461
+ }
462
+ build() {
463
+ const definitions = this._buildSharedDefinitions();
464
+ this._definitions = definitions;
465
+ const queryFields = {
466
+ nodes: definitions.nodesField,
467
+ ...definitions.queryFields
468
+ };
469
+ for (const id of this._includedModelIDs){
470
+ this._buildDocument(id);
471
+ const name = this._aliases[id];
472
+ const filterArg = {
473
+ type: this._inputObjects[`${id}-filter`]
474
+ };
475
+ queryFields[`all${name}`] = {
476
+ type: this._types[`${id}-connection`],
477
+ args: {
478
+ ...connectionArgs,
479
+ filter: filterArg,
480
+ orderBy: {
481
+ type: toList(this._inputObjects[`${id}-order`])
482
+ },
483
+ search: {
484
+ type: GraphQLString
485
+ }
486
+ },
487
+ resolve: async (_, args, ctx)=>{
488
+ return await ctx.resolveConnection(this.getModelIDs(id), args);
489
+ }
490
+ };
491
+ }
492
+ // Add top-level search query across all models
493
+ // Get the PageInfo type from an existing connection (shared by graphql-relay)
494
+ const firstModelID = [
495
+ ...this._includedModelIDs
496
+ ][0];
497
+ if (firstModelID != null) {
498
+ const someConnectionType = this._types[`${firstModelID}-connection`];
499
+ const pageInfoType = someConnectionType.getFields().pageInfo.type;
500
+ const searchResultEdge = new GraphQLObjectType({
501
+ name: 'SearchResultEdge',
502
+ fields: ()=>({
503
+ cursor: {
504
+ type: new GraphQLNonNull(GraphQLString)
505
+ },
506
+ node: {
507
+ type: new GraphQLNonNull(definitions.nodeInterface)
508
+ },
509
+ rank: {
510
+ type: new GraphQLNonNull(GraphQLFloat)
511
+ }
512
+ })
513
+ });
514
+ const searchResultConnection = new GraphQLObjectType({
515
+ name: 'SearchResultConnection',
516
+ fields: ()=>({
517
+ edges: {
518
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(searchResultEdge)))
519
+ },
520
+ pageInfo: {
521
+ type: pageInfoType
522
+ }
523
+ })
524
+ });
525
+ queryFields.search = {
526
+ type: new GraphQLNonNull(searchResultConnection),
527
+ args: {
528
+ query: {
529
+ type: new GraphQLNonNull(GraphQLString)
530
+ },
531
+ models: {
532
+ type: new GraphQLList(new GraphQLNonNull(GraphQLString))
533
+ },
534
+ first: {
535
+ type: GraphQLInt
536
+ },
537
+ after: {
538
+ type: GraphQLString
539
+ }
540
+ },
541
+ resolve: async (_root, args, ctx)=>{
542
+ const { query, models, first } = args;
543
+ if (query.trim() === '') {
544
+ throw new Error('Search query must not be empty');
545
+ }
546
+ // Map model names to IDs
547
+ const modelIDs = models ? models.map((name)=>this._aliasToModelID[name] ?? name) : [
548
+ ...this._includedModelIDs
549
+ ];
550
+ const results = await ctx.searchDocuments(query, modelIDs, first ?? 20);
551
+ // Load documents
552
+ const docIDs = results.map((r)=>r.documentID);
553
+ const allModelIDs = [
554
+ ...new Set(results.map((r)=>r.modelID))
555
+ ];
556
+ const documents = docIDs.length > 0 ? await ctx.resolveList(allModelIDs, docIDs) : [];
557
+ const docMap = new Map(documents.map((d)=>[
558
+ d.id,
559
+ d
560
+ ]));
561
+ const edges = [];
562
+ for (const r of results){
563
+ const node = docMap.get(r.documentID);
564
+ if (node != null) {
565
+ edges.push({
566
+ cursor: String(edges.length),
567
+ node,
568
+ rank: Math.abs(r.rank)
569
+ });
570
+ }
571
+ }
572
+ return {
573
+ edges,
574
+ pageInfo: {
575
+ hasNextPage: false,
576
+ hasPreviousPage: false,
577
+ startCursor: edges.length > 0 ? edges[0].cursor : null,
578
+ endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
579
+ }
580
+ };
581
+ }
582
+ };
583
+ }
584
+ // Introspection: the viewer's own model-access-default rows. The stored
585
+ // rows are one-per-(model, permissionType); group them by modelID so each
586
+ // result groups read+write into `permissions`, matching `ModelAccessDefaults`.
587
+ queryFields.modelAccessDefaults = {
588
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ModelAccessDefaults))),
589
+ resolve: async (_root, _args, ctx)=>{
590
+ const viewerDID = ctx.getViewer();
591
+ // The rows are the viewer's OWN defaults and `ownerDID` names the viewer.
592
+ // With no viewer there is no owner to name, and no own rows to return.
593
+ if (viewerDID == null) {
594
+ return [];
595
+ }
596
+ const rows = await ctx.listModelAccessDefaults();
597
+ const byModelID = new Map();
598
+ for (const row of rows){
599
+ let entry = byModelID.get(row.modelID);
600
+ if (entry == null) {
601
+ entry = {
602
+ ownerDID: viewerDID,
603
+ modelID: row.modelID,
604
+ permissions: {}
605
+ };
606
+ byModelID.set(row.modelID, entry);
607
+ }
608
+ entry.permissions[row.permissionType] = {
609
+ level: row.level,
610
+ allowedDIDs: row.allowedDIDs,
611
+ allowedCircles: row.allowedCircles,
612
+ allowedGroups: row.allowedGroups
613
+ };
614
+ }
615
+ return [
616
+ ...byModelID.values()
617
+ ];
618
+ }
619
+ };
620
+ // Add access control mutations if mutations are enabled
621
+ if (this._includeMutations) {
622
+ this._mutations.setModelAccessDefaults = {
623
+ type: ModelAccessDefaults,
624
+ args: {
625
+ modelID: {
626
+ type: new GraphQLNonNull(GraphQLID)
627
+ },
628
+ permissionType: {
629
+ type: new GraphQLNonNull(GraphQLString)
630
+ },
631
+ accessLevel: {
632
+ type: new GraphQLNonNull(GraphQLString)
633
+ },
634
+ allowedDIDs: {
635
+ type: toList(GraphQLString)
636
+ },
637
+ allowedCircles: {
638
+ type: toList(GraphQLString)
639
+ },
640
+ allowedGroups: {
641
+ type: toList(GraphQLString)
642
+ }
643
+ },
644
+ resolve: async (_, args, ctx)=>{
645
+ const allowedDIDs = args.allowedDIDs ?? null;
646
+ const allowedCircles = args.allowedCircles ?? null;
647
+ const allowedGroups = args.allowedGroups ?? null;
648
+ validateAccessIdentifiers({
649
+ allowedDIDs,
650
+ allowedCircles,
651
+ allowedGroups
652
+ });
653
+ return await ctx.executeSetModelAccessDefaults({
654
+ modelID: args.modelID,
655
+ permissionType: args.permissionType,
656
+ accessLevel: args.accessLevel,
657
+ allowedDIDs,
658
+ allowedCircles,
659
+ allowedGroups
660
+ });
661
+ }
662
+ };
663
+ this._mutations.removeModelAccessDefaults = {
664
+ type: GraphQLBoolean,
665
+ args: {
666
+ modelID: {
667
+ type: new GraphQLNonNull(GraphQLID)
668
+ },
669
+ permissionTypes: {
670
+ type: new GraphQLNonNull(toList(GraphQLString))
671
+ }
672
+ },
673
+ resolve: async (_, args, ctx)=>{
674
+ await ctx.executeRemoveModelAccessDefaults(args.modelID, args.permissionTypes);
675
+ return true;
676
+ }
677
+ };
678
+ this._mutations.setDocumentAccessOverride = {
679
+ type: definitions.documentInterface,
680
+ args: {
681
+ documentID: {
682
+ type: new GraphQLNonNull(GraphQLID)
683
+ },
684
+ permissionType: {
685
+ type: new GraphQLNonNull(GraphQLString)
686
+ },
687
+ accessLevel: {
688
+ type: new GraphQLNonNull(GraphQLString)
689
+ },
690
+ allowedDIDs: {
691
+ type: toList(GraphQLString)
692
+ },
693
+ allowedCircles: {
694
+ type: toList(GraphQLString)
695
+ },
696
+ allowedGroups: {
697
+ type: toList(GraphQLString)
698
+ }
699
+ },
700
+ resolve: async (_, args, ctx)=>{
701
+ const allowedDIDs = args.allowedDIDs ?? null;
702
+ const allowedCircles = args.allowedCircles ?? null;
703
+ const allowedGroups = args.allowedGroups ?? null;
704
+ validateAccessIdentifiers({
705
+ allowedDIDs,
706
+ allowedCircles,
707
+ allowedGroups
708
+ });
709
+ return await ctx.executeSetDocumentAccessOverride({
710
+ documentID: args.documentID,
711
+ permissionType: args.permissionType,
712
+ accessLevel: args.accessLevel,
713
+ allowedDIDs,
714
+ allowedCircles,
715
+ allowedGroups
716
+ });
717
+ }
718
+ };
719
+ this._mutations.removeDocumentAccessOverride = {
720
+ type: GraphQLBoolean,
721
+ args: {
722
+ documentID: {
723
+ type: new GraphQLNonNull(GraphQLID)
724
+ },
725
+ permissionTypes: {
726
+ type: new GraphQLNonNull(toList(GraphQLString))
727
+ }
728
+ },
729
+ resolve: async (_, args, ctx)=>{
730
+ await ctx.executeRemoveDocumentAccessOverride(args.documentID, args.permissionTypes);
731
+ return true;
732
+ }
733
+ };
734
+ // Transaction lifecycle mutations
735
+ const BeginTransactionPayload = new GraphQLObjectType({
736
+ name: 'BeginTransactionPayload',
737
+ fields: {
738
+ transactionID: {
739
+ type: new GraphQLNonNull(GraphQLID)
740
+ }
741
+ }
742
+ });
743
+ const CommitTransactionPayload = new GraphQLObjectType({
744
+ name: 'CommitTransactionPayload',
745
+ fields: {
746
+ success: {
747
+ type: new GraphQLNonNull(GraphQLBoolean)
748
+ }
749
+ }
750
+ });
751
+ const RollbackTransactionPayload = new GraphQLObjectType({
752
+ name: 'RollbackTransactionPayload',
753
+ fields: {
754
+ success: {
755
+ type: new GraphQLNonNull(GraphQLBoolean)
756
+ }
757
+ }
758
+ });
759
+ this._mutations.beginTransaction = {
760
+ type: new GraphQLNonNull(BeginTransactionPayload),
761
+ args: {},
762
+ resolve: (_, _args, ctx)=>{
763
+ return ctx.beginTransaction();
764
+ }
765
+ };
766
+ this._mutations.commitTransaction = {
767
+ type: new GraphQLNonNull(CommitTransactionPayload),
768
+ args: {
769
+ transactionID: {
770
+ type: new GraphQLNonNull(GraphQLID)
771
+ }
772
+ },
773
+ resolve: async (_, args, ctx)=>{
774
+ return await ctx.commitTransaction(args.transactionID);
775
+ }
776
+ };
777
+ this._mutations.rollbackTransaction = {
778
+ type: new GraphQLNonNull(RollbackTransactionPayload),
779
+ args: {
780
+ transactionID: {
781
+ type: new GraphQLNonNull(GraphQLID)
782
+ }
783
+ },
784
+ resolve: (_, args, ctx)=>{
785
+ return ctx.rollbackTransaction(args.transactionID);
786
+ }
787
+ };
788
+ }
789
+ const config = {
790
+ query: new GraphQLObjectType({
791
+ name: 'Query',
792
+ fields: queryFields
793
+ }),
794
+ // Ensure custom scalars stay reachable from the schema typeMap so plugin
795
+ // SDL extensions can reference them by name (URL, DID, DateTimeISO)
796
+ // regardless of whether any model field happens to use them.
797
+ types: [
798
+ GraphQLURL,
799
+ GraphQLDID,
800
+ GraphQLDateTimeISO
801
+ ]
802
+ };
803
+ if (this._includeMutations) {
804
+ config.mutation = new GraphQLObjectType({
805
+ name: 'Mutation',
806
+ fields: this._mutations
807
+ });
808
+ }
809
+ if (this._includeSubscriptions) {
810
+ config.subscription = new GraphQLObjectType({
811
+ name: 'Subscription',
812
+ fields: ()=>{
813
+ const fields = {
814
+ ...this._subscriptions
815
+ };
816
+ if (this._includeSubscriptions) {
817
+ fields.documentChanged = {
818
+ type: new GraphQLNonNull(definitions.documentInterface),
819
+ args: {
820
+ id: {
821
+ type: new GraphQLNonNull(GraphQLID)
822
+ }
823
+ },
824
+ resolve: (doc)=>doc,
825
+ subscribe: (_, args, ctx)=>{
826
+ return ctx.subscribeToDocumentChanged(args.id);
827
+ }
828
+ };
829
+ fields.documentRemoved = {
830
+ type: new GraphQLNonNull(GraphQLID),
831
+ resolve: (id)=>id,
832
+ subscribe: (_src, _args, ctx)=>{
833
+ return ctx.subscribeToDocumentRemoved();
834
+ }
835
+ };
836
+ for (const [id, model] of Object.entries(this._record)){
837
+ if (!this._includedModelIDs.has(id)) continue;
838
+ const name = this._aliases[id];
839
+ fields[`new${name}Created`] = {
840
+ type: new GraphQLNonNull(this._types[id]),
841
+ args: {
842
+ owner: {
843
+ type: GraphQLID
844
+ }
845
+ },
846
+ resolve: (doc)=>doc,
847
+ subscribe: (_src, args, ctx)=>{
848
+ return ctx.subscribeToDocumentCreated(this.getModelIDs(id), args.owner ?? undefined);
849
+ }
850
+ };
851
+ for (const iid of model.interfaces ?? []){
852
+ const interfaceID = getGlobalID(iid, id).toString();
853
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[interfaceID] ?? {})){
854
+ for (const relation of relations){
855
+ this._buildRelationSubscriptions(fields, interfaceID, fieldName, relation);
856
+ }
857
+ }
858
+ }
859
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[id] ?? {})){
860
+ for (const relation of relations){
861
+ if (!this._includedModelIDs.has(relation.model)) continue;
862
+ this._buildRelationSubscriptions(fields, id, fieldName, relation);
863
+ }
864
+ }
865
+ }
866
+ }
867
+ return fields;
868
+ }
869
+ });
870
+ }
871
+ let schema = new GraphQLSchema(config);
872
+ // Apply SDL extensions
873
+ if (this._extensionSDL != null) {
874
+ const typesBefore = new Set(Object.keys(schema.getTypeMap()));
875
+ schema = extendSchema(schema, parseGraphQL(this._extensionSDL));
876
+ // Stub extension resolvers for client-side pre-execution
877
+ if (this._stubExtensionResolvers) {
878
+ this.#stubExtensionFields(schema, typesBefore);
879
+ }
880
+ }
881
+ // Attach resolvers to extended fields
882
+ if (this._extensionResolvers != null) {
883
+ this.#attachExtensionResolvers(schema, this._extensionResolvers);
884
+ }
885
+ return schema;
886
+ }
887
+ #attachExtensionResolvers(schema, resolvers) {
888
+ const queryType = schema.getQueryType();
889
+ if (queryType != null && resolvers.queryFields != null) {
890
+ const fields = queryType.getFields();
891
+ for (const [name, resolver] of Object.entries(resolvers.queryFields)){
892
+ if (fields[name] == null) {
893
+ throw new Error(`Extension resolver for query field "${name}" has no matching field in SDL`);
894
+ }
895
+ fields[name].resolve = resolver;
896
+ }
897
+ }
898
+ const mutationType = schema.getMutationType();
899
+ if (mutationType != null && resolvers.mutationFields != null) {
900
+ const fields = mutationType.getFields();
901
+ for (const [name, resolver] of Object.entries(resolvers.mutationFields)){
902
+ if (fields[name] == null) {
903
+ throw new Error(`Extension resolver for mutation field "${name}" has no matching field in SDL`);
904
+ }
905
+ fields[name].resolve = resolver;
906
+ }
907
+ }
908
+ const subscriptionType = schema.getSubscriptionType();
909
+ if (subscriptionType != null && resolvers.subscriptionFields != null) {
910
+ const fields = subscriptionType.getFields();
911
+ for (const [name, entry] of Object.entries(resolvers.subscriptionFields)){
912
+ if (fields[name] == null) {
913
+ throw new Error(`Extension resolver for subscription field "${name}" has no matching field in SDL`);
914
+ }
915
+ fields[name].resolve = entry.resolve;
916
+ fields[name].subscribe = entry.subscribe;
917
+ }
918
+ }
919
+ if (resolvers.typeFields != null) {
920
+ for (const [typeName, fieldResolvers] of Object.entries(resolvers.typeFields)){
921
+ const type = schema.getType(typeName);
922
+ if (type == null) {
923
+ throw new Error(`Extension resolver references type "${typeName}" which does not exist in the schema`);
924
+ }
925
+ if (!isObjectType(type)) {
926
+ throw new Error(`Extension resolver references type "${typeName}" which is not an object type`);
927
+ }
928
+ const fields = type.getFields();
929
+ for (const [fieldName, resolver] of Object.entries(fieldResolvers)){
930
+ if (fields[fieldName] == null) {
931
+ throw new Error(`Extension resolver for field "${fieldName}" on type "${typeName}" has no matching field in SDL`);
932
+ }
933
+ fields[fieldName].resolve = resolver;
934
+ }
935
+ }
936
+ }
937
+ if (resolvers.nodeResolvers != null) {
938
+ for (const [typeName, resolver] of Object.entries(resolvers.nodeResolvers)){
939
+ this._pluginNodeResolvers[typeName] = resolver;
940
+ }
941
+ }
942
+ }
943
+ #stubExtensionFields(schema, typesBefore) {
944
+ // Stub root Query/Mutation fields added by extension SDL
945
+ for (const rootType of [
946
+ schema.getQueryType(),
947
+ schema.getMutationType()
948
+ ]){
949
+ if (rootType == null) continue;
950
+ const fields = rootType.getFields();
951
+ for (const field of Object.values(fields)){
952
+ if (field.resolve == null) {
953
+ field.resolve = ()=>createDefaultValue(field.type);
954
+ }
955
+ }
956
+ }
957
+ // Stub fields on types added by extension SDL
958
+ for (const [typeName, type] of Object.entries(schema.getTypeMap())){
959
+ if (typesBefore.has(typeName)) continue;
960
+ if (!isObjectType(type)) continue;
961
+ const fields = type.getFields();
962
+ for (const field of Object.values(fields)){
963
+ if (field.resolve == null) {
964
+ const fieldType = field.type;
965
+ field.resolve = (source)=>source[field.name] ?? createDefaultValue(fieldType);
966
+ }
967
+ }
968
+ }
969
+ }
970
+ _getDefinitions() {
971
+ if (this._definitions == null) {
972
+ throw new Error('Definitions must be initialized');
973
+ }
974
+ return this._definitions;
975
+ }
976
+ _getReference(path, parentName) {
977
+ const id = getReferenceID(path);
978
+ const type = this._types[id];
979
+ if (type != null) {
980
+ return type;
981
+ }
982
+ const schema = this._getReferenceSchema(id);
983
+ return this._buildReference(id, schema, parentName);
984
+ }
985
+ _getReferenceSchema(id) {
986
+ const schema = this._references[id];
987
+ if (schema == null) {
988
+ throw new Error(`Could not find reference: ${id}`);
989
+ }
990
+ return schema;
991
+ }
992
+ _getReferenceInput(path, isPartial, parentName = '') {
993
+ const id = getReferenceID(path);
994
+ const schema = this._getReferenceSchema(id);
995
+ if (schema.type === 'array') {
996
+ const itemType = this._getReferenceInput(schema.items.$ref, false, schema.title);
997
+ return new GraphQLList(new GraphQLNonNull(itemType));
998
+ }
999
+ if (schema.type === 'object') {
1000
+ if (schema.additionalProperties) {
1001
+ return GraphQLJSONObject;
1002
+ }
1003
+ const inputID = isPartial ? `${id}-partial` : id;
1004
+ const existing = this._inputObjects[inputID];
1005
+ if (existing != null) {
1006
+ return existing;
1007
+ }
1008
+ const fields = {};
1009
+ for (const [key, value] of Object.entries(schema.properties)){
1010
+ const type = this._getReferenceInput(value.$ref, isPartial, schema.title);
1011
+ fields[key] = {
1012
+ type: isPartial || !schema.required.includes(key) ? type : new GraphQLNonNull(type)
1013
+ };
1014
+ }
1015
+ const name = this._aliases[id] ?? pascalCase(parentName + schema.title);
1016
+ const input = new GraphQLInputObjectType({
1017
+ name: isPartial ? `Partial${name}Input` : `${name}Input`,
1018
+ fields
1019
+ });
1020
+ this._inputObjects[inputID] = input;
1021
+ return input;
1022
+ }
1023
+ return this._buildScalar(id, schema, parentName);
1024
+ }
1025
+ _buildSharedDefinitions() {
1026
+ const nodeDefs = nodeDefinitions(async (id, ctx)=>{
1027
+ if (id.startsWith('did:')) {
1028
+ return id;
1029
+ }
1030
+ const decoded = decodeID(id);
1031
+ if (decoded instanceof AttachmentID) {
1032
+ return decoded;
1033
+ }
1034
+ if (decoded instanceof DocumentID) {
1035
+ return await ctx.loadDocument(id);
1036
+ }
1037
+ if (decoded instanceof PluginNodeID) {
1038
+ const resolver = this._pluginNodeResolvers[decoded.typeName];
1039
+ if (resolver == null) {
1040
+ throw new Error(`No node resolver registered for plugin type "${decoded.typeName}"`);
1041
+ }
1042
+ const result = await resolver.resolve(decoded.localID, ctx);
1043
+ if (result == null) {
1044
+ throw new Error(`Node resolver for "${decoded.typeName}" returned null for localID "${decoded.localID}"`);
1045
+ }
1046
+ return {
1047
+ ...result,
1048
+ __typename: decoded.typeName
1049
+ };
1050
+ }
1051
+ throw new Error(`Unsupported node ID: ${id}`);
1052
+ }, (node)=>{
1053
+ if (typeof node === 'string') return 'AccountNode';
1054
+ if (node instanceof AttachmentID) return 'AttachmentNode';
1055
+ if (node != null && typeof node === 'object' && 'model' in node) {
1056
+ return this._aliases[node.model];
1057
+ }
1058
+ if (node != null && typeof node === 'object' && '__typename' in node) {
1059
+ return node.__typename;
1060
+ }
1061
+ throw new Error('Unknown node type');
1062
+ });
1063
+ const accountObject = new GraphQLObjectType({
1064
+ name: 'AccountNode',
1065
+ interfaces: [
1066
+ nodeDefs.nodeInterface
1067
+ ],
1068
+ fields: ()=>{
1069
+ const config = {
1070
+ id: {
1071
+ type: new GraphQLNonNull(GraphQLID),
1072
+ description: 'Globally unique identifier of the account (DID string)',
1073
+ resolve: (did)=>did
1074
+ },
1075
+ isViewer: {
1076
+ type: new GraphQLNonNull(GraphQLBoolean),
1077
+ description: 'Whether the authenticated request issuer is this account or not',
1078
+ resolve: (did, _, ctx)=>ctx.getViewer() === did
1079
+ }
1080
+ };
1081
+ for (const [id, model] of Object.entries(this._record)){
1082
+ if (!this._includedModelIDs.has(id)) continue;
1083
+ switch(model.behavior){
1084
+ case 'interface':
1085
+ continue;
1086
+ case 'default':
1087
+ {
1088
+ const name = this._aliases[id];
1089
+ config[`own${name}Connection`] = {
1090
+ type: this._types[`${id}-connection`],
1091
+ args: {
1092
+ ...connectionArgs,
1093
+ filter: {
1094
+ type: this._inputObjects[`${id}-filter`]
1095
+ },
1096
+ orderBy: {
1097
+ type: toList(this._inputObjects[`${id}-order`])
1098
+ }
1099
+ },
1100
+ resolve: async (owner, args, ctx)=>{
1101
+ return await ctx.resolveConnection(this.getModelIDs(id), {
1102
+ ...args,
1103
+ owner
1104
+ });
1105
+ }
1106
+ };
1107
+ break;
1108
+ }
1109
+ case 'unique':
1110
+ {
1111
+ const name = this._aliases[id];
1112
+ config[`own${name}`] = {
1113
+ type: this._types[id],
1114
+ args: model.uniqueFields.length ? {
1115
+ with: {
1116
+ type: new GraphQLNonNull(this._buildSetInputObjectType(id, model.uniqueFields))
1117
+ }
1118
+ } : {},
1119
+ resolve: async (owner, args, ctx)=>{
1120
+ const unique = getUniqueDocValue(model.uniqueFields, args.with);
1121
+ const docID = DocumentID.create(id, owner, unique);
1122
+ return await ctx.loadDocument(docID.toString());
1123
+ }
1124
+ };
1125
+ }
1126
+ }
1127
+ }
1128
+ return config;
1129
+ }
1130
+ });
1131
+ const attachmentObject = new GraphQLObjectType({
1132
+ name: 'AttachmentNode',
1133
+ interfaces: [
1134
+ nodeDefs.nodeInterface
1135
+ ],
1136
+ fields: {
1137
+ id: {
1138
+ type: new GraphQLNonNull(GraphQLID),
1139
+ resolve: (aid)=>aid.toString()
1140
+ },
1141
+ contentLength: {
1142
+ type: new GraphQLNonNull(GraphQLInt)
1143
+ },
1144
+ mimeType: {
1145
+ type: new GraphQLNonNull(GraphQLString)
1146
+ }
1147
+ }
1148
+ });
1149
+ const documentNodeFields = {
1150
+ // Node interface
1151
+ id: {
1152
+ type: new GraphQLNonNull(GraphQLID)
1153
+ },
1154
+ // Metadata
1155
+ model: {
1156
+ type: new GraphQLNonNull(GraphQLString)
1157
+ },
1158
+ owner: {
1159
+ type: new GraphQLNonNull(accountObject)
1160
+ },
1161
+ createdAt: {
1162
+ type: new GraphQLNonNull(GraphQLDateTimeISO)
1163
+ },
1164
+ updatedAt: {
1165
+ type: GraphQLDateTimeISO
1166
+ },
1167
+ // Access control
1168
+ accessPermissions: {
1169
+ type: AccessPermissions,
1170
+ resolve: (doc)=>doc.data?.accessPermissions ?? null
1171
+ }
1172
+ };
1173
+ const documentInterface = new GraphQLInterfaceType({
1174
+ name: 'DocumentNode',
1175
+ interfaces: [
1176
+ nodeDefs.nodeInterface
1177
+ ],
1178
+ fields: documentNodeFields,
1179
+ resolveType: (doc)=>this._aliases[doc.model]
1180
+ });
1181
+ const queryFields = {
1182
+ node: nodeDefs.nodeField,
1183
+ viewer: {
1184
+ type: accountObject,
1185
+ description: 'Account issuing the request, if authenticated',
1186
+ resolve: (_self, _args, ctx)=>ctx.getViewer()
1187
+ }
1188
+ };
1189
+ return {
1190
+ ...nodeDefs,
1191
+ accountObject,
1192
+ attachmentObject,
1193
+ documentInterface,
1194
+ documentNodeFields,
1195
+ queryFields
1196
+ };
1197
+ }
1198
+ _buildDocument(id) {
1199
+ if (this._types[id] != null) {
1200
+ return this._types[id];
1201
+ }
1202
+ const model = this._record[id];
1203
+ if (model == null) {
1204
+ throw new Error(`Could not find model id: ${id}`);
1205
+ }
1206
+ const definitions = this._getDefinitions();
1207
+ this._aliases[id] ??= pascalCase(model.name);
1208
+ const name = this._aliases[id];
1209
+ const config = {
1210
+ name,
1211
+ interfaces: ()=>{
1212
+ return [
1213
+ definitions.documentInterface,
1214
+ definitions.nodeInterface
1215
+ ].concat(model.interfaces.map((interfaceID)=>{
1216
+ return this._types[getGlobalID(interfaceID, id).toString()];
1217
+ }).filter((type)=>type != null));
1218
+ },
1219
+ fields: ()=>this._buildDocumentFields(id, model, name)
1220
+ };
1221
+ const isInterface = model.behavior === 'interface';
1222
+ const nodeType = isInterface ? new GraphQLInterfaceType({
1223
+ ...config,
1224
+ resolveType: (doc)=>this._aliases[doc.model]
1225
+ }) : new GraphQLObjectType(config);
1226
+ const aggregateType = this._buildAggregate(id, model, name);
1227
+ const groupedAggregateType = this._buildGroupedAggregate(id, model, name);
1228
+ const { connectionType, edgeType } = connectionDefinitions({
1229
+ nodeType,
1230
+ connectionFields: ()=>{
1231
+ const fields = {
1232
+ aggregates: {
1233
+ type: aggregateType,
1234
+ // The connection carries the query scope so aggregation runs against
1235
+ // the same documents the page was built from, not the current page.
1236
+ // A look-ahead on the selection set turns the requested ops/fields into
1237
+ // store specs, so only what was selected is computed.
1238
+ resolve: async (source, _args, ctx, info)=>{
1239
+ const scope = source.aggregateScope;
1240
+ const { count, aggregates } = this._buildAggregateRequest(id, info);
1241
+ return await ctx.resolveAggregate(scope.modelIDs, {
1242
+ filter: scope.filter,
1243
+ owner: scope.owner,
1244
+ catalogID: scope.catalogID,
1245
+ count,
1246
+ aggregates
1247
+ });
1248
+ }
1249
+ }
1250
+ };
1251
+ // Grouped aggregates only exist for models with at least one groupable scalar
1252
+ // field (see _buildGroupedAggregate). Skip the field entirely otherwise so the
1253
+ // schema does not reference empty GroupKeys/GroupBy/GroupKeysOrderBy types.
1254
+ if (groupedAggregateType != null) {
1255
+ fields.groupedAggregates = {
1256
+ type: new GraphQLNonNull(toOuputList(groupedAggregateType)),
1257
+ args: {
1258
+ groupBy: {
1259
+ type: new GraphQLNonNull(toList(this._inputObjects[`${id}-group-by`]))
1260
+ },
1261
+ having: {
1262
+ type: this._inputObjects[`${id}-having`]
1263
+ },
1264
+ orderBy: {
1265
+ type: toList(this._inputObjects[`${id}-group-order`])
1266
+ },
1267
+ limit: {
1268
+ type: GraphQLInt
1269
+ }
1270
+ },
1271
+ // Translates the field arguments and the selection-set look-ahead into a
1272
+ // single grouped store request, scoped to the same documents the page was
1273
+ // built from, then returns the merged groups for the child resolvers to map.
1274
+ resolve: async (source, args, ctx, info)=>{
1275
+ const scope = source.aggregateScope;
1276
+ const { count, aggregates } = this._buildGroupedAggregateRequest(id, info);
1277
+ const groupBy = (args.groupBy ?? []).map((field)=>({
1278
+ keys: [
1279
+ field
1280
+ ]
1281
+ }));
1282
+ const orderBy = this._buildGroupOrderBy(id, args.orderBy);
1283
+ return await ctx.resolveGroupedAggregate(scope.modelIDs, {
1284
+ filter: scope.filter,
1285
+ owner: scope.owner,
1286
+ catalogID: scope.catalogID,
1287
+ count,
1288
+ aggregates,
1289
+ groupBy,
1290
+ having: args.having,
1291
+ orderBy: orderBy.length > 0 ? orderBy : undefined,
1292
+ limit: args.limit ?? undefined
1293
+ });
1294
+ }
1295
+ };
1296
+ }
1297
+ return fields;
1298
+ }
1299
+ });
1300
+ this._types[id] = nodeType;
1301
+ this._types[`${id}-connection`] = connectionType;
1302
+ this._types[`${id}-edge`] = edgeType;
1303
+ this._buildDocumentDataObject(id, model, name);
1304
+ this._buildDocumentInput(id, model);
1305
+ this._buildObjectFilterInput(id, model.schema, name, true);
1306
+ this._buildObjectOrderByInput(id, model.schema, name, true);
1307
+ // TODO: move to dedicated method
1308
+ if (!isInterface && this._includeMutations) {
1309
+ if (model.behavior === 'default') {
1310
+ this._mutations[`create${name}`] = mutationWithClientMutationId({
1311
+ name: `Create${name}`,
1312
+ inputFields: ()=>({
1313
+ data: {
1314
+ type: new GraphQLNonNull(this._inputObjects[id])
1315
+ },
1316
+ transactionID: {
1317
+ type: GraphQLID
1318
+ }
1319
+ }),
1320
+ outputFields: ()=>({
1321
+ ...definitions.queryFields,
1322
+ document: {
1323
+ type: this._types[id]
1324
+ }
1325
+ }),
1326
+ mutateAndGetPayload: async (input, ctx, info)=>{
1327
+ const document = await ctx.executeCreateMutation({
1328
+ modelID: id,
1329
+ data: input.data,
1330
+ info,
1331
+ transactionID: input.transactionID
1332
+ });
1333
+ return {
1334
+ document
1335
+ };
1336
+ }
1337
+ });
1338
+ } else if (model.behavior === 'unique') {
1339
+ this._mutations[`set${name}`] = mutationWithClientMutationId({
1340
+ name: `Set${name}`,
1341
+ inputFields: ()=>({
1342
+ data: {
1343
+ type: new GraphQLNonNull(this._inputObjects[id])
1344
+ },
1345
+ transactionID: {
1346
+ type: GraphQLID
1347
+ }
1348
+ }),
1349
+ outputFields: ()=>({
1350
+ ...definitions.queryFields,
1351
+ document: {
1352
+ type: this._types[id]
1353
+ }
1354
+ }),
1355
+ mutateAndGetPayload: async (input, ctx, info)=>{
1356
+ const unique = getUniqueDocValue(model.uniqueFields, input.data);
1357
+ const document = await ctx.executeSetMutation({
1358
+ modelID: id,
1359
+ unique,
1360
+ data: input.data,
1361
+ info,
1362
+ transactionID: input.transactionID
1363
+ });
1364
+ return {
1365
+ document
1366
+ };
1367
+ }
1368
+ });
1369
+ }
1370
+ this._mutations[`update${name}`] = mutationWithClientMutationId({
1371
+ name: `Update${name}`,
1372
+ inputFields: ()=>({
1373
+ id: {
1374
+ type: new GraphQLNonNull(GraphQLID)
1375
+ },
1376
+ patch: {
1377
+ type: new GraphQLNonNull(toList(PatchOperation))
1378
+ },
1379
+ from: {
1380
+ type: this._inputObjects[`${id}-update`]
1381
+ },
1382
+ transactionID: {
1383
+ type: GraphQLID
1384
+ }
1385
+ }),
1386
+ outputFields: ()=>({
1387
+ ...definitions.queryFields,
1388
+ document: {
1389
+ type: this._types[id]
1390
+ }
1391
+ }),
1392
+ mutateAndGetPayload: async (input, ctx, info)=>{
1393
+ const document = await ctx.executeUpdateMutation({
1394
+ input,
1395
+ info,
1396
+ transactionID: input.transactionID
1397
+ });
1398
+ return {
1399
+ document
1400
+ };
1401
+ }
1402
+ });
1403
+ this._mutations[`remove${name}`] = mutationWithClientMutationId({
1404
+ name: `Remove${name}`,
1405
+ inputFields: ()=>({
1406
+ id: {
1407
+ type: new GraphQLNonNull(GraphQLID)
1408
+ },
1409
+ transactionID: {
1410
+ type: GraphQLID
1411
+ }
1412
+ }),
1413
+ outputFields: ()=>({
1414
+ ...definitions.queryFields,
1415
+ // Return id so it can be used with Relay's @deleteRecord directive
1416
+ id: {
1417
+ type: new GraphQLNonNull(GraphQLID)
1418
+ }
1419
+ }),
1420
+ mutateAndGetPayload: async (input, ctx, info)=>{
1421
+ await ctx.executeRemoveMutation({
1422
+ id: input.id,
1423
+ info,
1424
+ transactionID: input.transactionID
1425
+ });
1426
+ return {
1427
+ id: input.id
1428
+ };
1429
+ }
1430
+ });
1431
+ }
1432
+ return nodeType;
1433
+ }
1434
+ /**
1435
+ * Build the `<Model>Aggregate` type attached to the connection, exposing `count`
1436
+ * plus per-field `sum`/`average`/`min`/`max` selections gated by field type.
1437
+ *
1438
+ * Eligibility follows what SQL can faithfully compute over JSON-extracted values:
1439
+ * - sum/average: numeric fields only (integer, number).
1440
+ * - min/max: numeric fields plus string/date fields (lexical for strings).
1441
+ * Field-selection subtypes are only emitted when at least one field qualifies,
1442
+ * since an object type with no fields is invalid GraphQL. `count` is always present.
1443
+ */ _buildAggregate(id, model, name) {
1444
+ const existing = this._types[`${id}-aggregate`];
1445
+ if (existing != null) {
1446
+ return existing;
1447
+ }
1448
+ // Resolve each property to its underlying scalar so we can classify it. Only
1449
+ // direct scalar references are eligible; object/array fields are skipped.
1450
+ const sumFields = {};
1451
+ const minMaxFields = {};
1452
+ // Field key -> numeric, consumed by the look-ahead resolver to tag each spec.
1453
+ const numericByKey = new Map();
1454
+ for (const [key, value] of Object.entries(model.schema.properties ?? {})){
1455
+ const ref = value.$ref;
1456
+ if (ref == null) continue;
1457
+ const refID = getReferenceID(ref);
1458
+ const refSchema = this._getReferenceSchema(refID);
1459
+ if (refSchema.type === 'object' || refSchema.type === 'array') continue;
1460
+ const scalar = this._buildScalar(refID, refSchema);
1461
+ const isNumeric = scalar === GraphQLInt || scalar === GraphQLFloat;
1462
+ // Only plain-string formats (date / time / date-time, which map to GraphQLString)
1463
+ // are min/max-eligible; custom-scalar formats (duration / email / uri) and enums
1464
+ // are intentionally excluded from min/max.
1465
+ const isStringLike = scalar === GraphQLString;
1466
+ if (isNumeric) {
1467
+ // sum keeps the field's own numeric type (Int stays Int, number is Float).
1468
+ sumFields[key] = {
1469
+ type: scalar
1470
+ };
1471
+ }
1472
+ if (isNumeric || isStringLike) {
1473
+ minMaxFields[key] = {
1474
+ type: scalar
1475
+ };
1476
+ numericByKey.set(key, isNumeric);
1477
+ }
1478
+ }
1479
+ this._aggregateNumericByModel[id] = numericByKey;
1480
+ const numericKeys = Object.keys(sumFields);
1481
+ // The aggregates connection resolver returns an AggregateValues; each op
1482
+ // reads its slice from that source. Defaulting count to 0 keeps the non-null
1483
+ // contract when a count was not requested by the look-ahead.
1484
+ const aggregateFields = {
1485
+ count: {
1486
+ type: new GraphQLNonNull(GraphQLInt),
1487
+ resolve: (source)=>source.count ?? 0
1488
+ }
1489
+ };
1490
+ if (numericKeys.length > 0) {
1491
+ const sumType = new GraphQLObjectType({
1492
+ name: `${name}SumFields`,
1493
+ fields: sumFields
1494
+ });
1495
+ // Average of integers is fractional, so every average field is Float.
1496
+ const avgFields = {};
1497
+ for (const key of numericKeys){
1498
+ avgFields[key] = {
1499
+ type: GraphQLFloat
1500
+ };
1501
+ }
1502
+ const avgType = new GraphQLObjectType({
1503
+ name: `${name}AvgFields`,
1504
+ fields: avgFields
1505
+ });
1506
+ this._types[`${id}-aggregate-sum`] = sumType;
1507
+ this._types[`${id}-aggregate-avg`] = avgType;
1508
+ // The *Fields child reads each field key off the returned Record via the
1509
+ // default field resolver (e.g. SumFields.delta -> source.delta).
1510
+ aggregateFields.sum = {
1511
+ type: sumType,
1512
+ resolve: (source)=>source.sum ?? null
1513
+ };
1514
+ aggregateFields.average = {
1515
+ type: avgType,
1516
+ resolve: (source)=>source.average ?? null
1517
+ };
1518
+ }
1519
+ if (Object.keys(minMaxFields).length > 0) {
1520
+ // min and max expose the identical field set, so they share one object type.
1521
+ const minMaxType = new GraphQLObjectType({
1522
+ name: `${name}MinMaxFields`,
1523
+ fields: minMaxFields
1524
+ });
1525
+ this._types[`${id}-aggregate-min-max`] = minMaxType;
1526
+ aggregateFields.min = {
1527
+ type: minMaxType,
1528
+ resolve: (source)=>source.min ?? null
1529
+ };
1530
+ aggregateFields.max = {
1531
+ type: minMaxType,
1532
+ resolve: (source)=>source.max ?? null
1533
+ };
1534
+ }
1535
+ const aggregateType = new GraphQLObjectType({
1536
+ name: `${name}Aggregate`,
1537
+ fields: aggregateFields
1538
+ });
1539
+ this._types[`${id}-aggregate`] = aggregateType;
1540
+ return aggregateType;
1541
+ }
1542
+ /**
1543
+ * Build the grouped-aggregate type surface for a model, reusing the v1
1544
+ * `<Model>SumFields` / `AvgFields` / `MinMaxFields` types from
1545
+ * `_buildAggregate` and the existing value-filter / `OrderByDirection` inputs.
1546
+ *
1547
+ * Groupable fields are every property resolving to a scalar (object/array fields
1548
+ * excluded), which is broader than aggregate eligibility: boolean and enum fields
1549
+ * can be grouped on but never summed. The sum/average and min/max sub-shapes are
1550
+ * only emitted when the model actually has those eligible fields, matching the
1551
+ * conditionals in `_buildAggregate`, since an object type with no fields is invalid.
1552
+ *
1553
+ * Produces, registered in `_types` / `_inputObjects`:
1554
+ * `<Model>GroupBy` enum, `<Model>GroupKeys`, `<Model>GroupedAggregate`,
1555
+ * `<Model>Having` (+ `HavingSum`/`HavingAverage`/`HavingMin`/`HavingMax`),
1556
+ * `<Model>GroupOrderBy` (+ `GroupKeysOrderBy` and per-op `<Op>FieldsOrderBy`).
1557
+ */ _buildGroupedAggregate(id, model, name) {
1558
+ const existing = this._types[`${id}-grouped-aggregate`];
1559
+ if (existing != null) {
1560
+ return existing;
1561
+ }
1562
+ // Every scalar property is groupable (object/array excluded), so this set is a
1563
+ // superset of the aggregate-eligible fields, additionally covering boolean/enum.
1564
+ const groupableTypes = new Map();
1565
+ const groupKeyNumeric = new Map();
1566
+ for (const [key, value] of Object.entries(model.schema.properties ?? {})){
1567
+ const ref = value.$ref;
1568
+ if (ref == null) continue;
1569
+ const refID = getReferenceID(ref);
1570
+ const refSchema = this._getReferenceSchema(refID);
1571
+ if (refSchema.type === 'object' || refSchema.type === 'array') continue;
1572
+ const scalar = this._buildScalar(refID, refSchema);
1573
+ groupableTypes.set(key, scalar);
1574
+ groupKeyNumeric.set(key, scalar === GraphQLInt || scalar === GraphQLFloat);
1575
+ }
1576
+ this._groupKeyNumericByModel[id] = groupKeyNumeric;
1577
+ // GraphQL forbids empty object/input/enum types, so skip the whole grouped-aggregate
1578
+ // surface (GroupKeys, GroupBy, GroupKeysOrderBy, GroupedAggregate, Having, GroupOrderBy)
1579
+ // when no groupable scalar field exists — typical for tag-style models whose only
1580
+ // properties are document references.
1581
+ if (groupableTypes.size === 0) {
1582
+ return null;
1583
+ }
1584
+ const sumType = this._types[`${id}-aggregate-sum`];
1585
+ const avgType = this._types[`${id}-aggregate-avg`];
1586
+ const minMaxType = this._types[`${id}-aggregate-min-max`];
1587
+ // GroupBy enum: one value per groupable field key.
1588
+ const groupByValues = {};
1589
+ for (const key of groupableTypes.keys()){
1590
+ groupByValues[key] = {
1591
+ value: key
1592
+ };
1593
+ }
1594
+ const groupByEnum = new GraphQLEnumType({
1595
+ name: `${name}GroupBy`,
1596
+ values: groupByValues
1597
+ });
1598
+ this._types[`${id}-group-by`] = groupByEnum;
1599
+ this._inputObjects[`${id}-group-by`] = groupByEnum;
1600
+ // GroupKeys: every groupable field, each nullable since a group may bucket
1601
+ // documents missing the field into a null key.
1602
+ const groupKeysFields = {};
1603
+ for (const [key, scalar] of groupableTypes){
1604
+ groupKeysFields[key] = {
1605
+ type: scalar
1606
+ };
1607
+ }
1608
+ const groupKeysType = new GraphQLObjectType({
1609
+ name: `${name}GroupKeys`,
1610
+ fields: groupKeysFields
1611
+ });
1612
+ this._types[`${id}-group-keys`] = groupKeysType;
1613
+ // GroupedAggregate: the per-group keys plus count, reusing the v1 aggregate field
1614
+ // types for sum/average/min/max under the same conditionals as `_buildAggregate`.
1615
+ // Each op resolves its slice off the returned group; the *Fields / GroupKeys
1616
+ // leaves keep the default resolver. Defaulting count to 0 and field ops to null
1617
+ // mirrors `_buildAggregate` so the non-null contracts hold on missing slices.
1618
+ const groupedFields = {
1619
+ keys: {
1620
+ type: new GraphQLNonNull(groupKeysType),
1621
+ resolve: (group)=>group.keys
1622
+ },
1623
+ count: {
1624
+ type: new GraphQLNonNull(GraphQLInt),
1625
+ resolve: (group)=>group.count ?? 0
1626
+ }
1627
+ };
1628
+ if (sumType != null && avgType != null) {
1629
+ groupedFields.sum = {
1630
+ type: sumType,
1631
+ resolve: (group)=>group.sum ?? null
1632
+ };
1633
+ groupedFields.average = {
1634
+ type: avgType,
1635
+ resolve: (group)=>group.average ?? null
1636
+ };
1637
+ }
1638
+ if (minMaxType != null) {
1639
+ groupedFields.min = {
1640
+ type: minMaxType,
1641
+ resolve: (group)=>group.min ?? null
1642
+ };
1643
+ groupedFields.max = {
1644
+ type: minMaxType,
1645
+ resolve: (group)=>group.max ?? null
1646
+ };
1647
+ }
1648
+ const groupedAggregateType = new GraphQLObjectType({
1649
+ name: `${name}GroupedAggregate`,
1650
+ fields: groupedFields
1651
+ });
1652
+ this._types[`${id}-grouped-aggregate`] = groupedAggregateType;
1653
+ // Map a scalar to its existing value-filter input by name (IntValueFilter etc.).
1654
+ const valueFilterFor = (scalar)=>{
1655
+ return this._inputObjects[`${scalar.name}ValueFilter`];
1656
+ };
1657
+ // Having: each op maps its eligible fields to the value filter matching the op's
1658
+ // scalar. Sum keeps each numeric field's own scalar; average is always Float;
1659
+ // min/max keep each field's own scalar (Int / Float / String).
1660
+ const havingFields = {
1661
+ count: {
1662
+ type: this._inputObjects.IntValueFilter
1663
+ }
1664
+ };
1665
+ if (sumType != null && avgType != null) {
1666
+ const havingSumFields = {};
1667
+ const havingAvgFields = {};
1668
+ for (const [key, field] of Object.entries(sumType.getFields())){
1669
+ havingSumFields[key] = {
1670
+ type: valueFilterFor(field.type)
1671
+ };
1672
+ havingAvgFields[key] = {
1673
+ type: this._inputObjects.FloatValueFilter
1674
+ };
1675
+ }
1676
+ const havingSum = new GraphQLInputObjectType({
1677
+ name: `${name}HavingSum`,
1678
+ fields: havingSumFields
1679
+ });
1680
+ const havingAvg = new GraphQLInputObjectType({
1681
+ name: `${name}HavingAverage`,
1682
+ fields: havingAvgFields
1683
+ });
1684
+ this._inputObjects[`${id}-having-sum`] = havingSum;
1685
+ this._inputObjects[`${id}-having-average`] = havingAvg;
1686
+ havingFields.sum = {
1687
+ type: havingSum
1688
+ };
1689
+ havingFields.average = {
1690
+ type: havingAvg
1691
+ };
1692
+ }
1693
+ if (minMaxType != null) {
1694
+ const havingMinFields = {};
1695
+ const havingMaxFields = {};
1696
+ for (const [key, field] of Object.entries(minMaxType.getFields())){
1697
+ havingMinFields[key] = {
1698
+ type: valueFilterFor(field.type)
1699
+ };
1700
+ havingMaxFields[key] = {
1701
+ type: valueFilterFor(field.type)
1702
+ };
1703
+ }
1704
+ const havingMin = new GraphQLInputObjectType({
1705
+ name: `${name}HavingMin`,
1706
+ fields: havingMinFields
1707
+ });
1708
+ const havingMax = new GraphQLInputObjectType({
1709
+ name: `${name}HavingMax`,
1710
+ fields: havingMaxFields
1711
+ });
1712
+ this._inputObjects[`${id}-having-min`] = havingMin;
1713
+ this._inputObjects[`${id}-having-max`] = havingMax;
1714
+ havingFields.min = {
1715
+ type: havingMin
1716
+ };
1717
+ havingFields.max = {
1718
+ type: havingMax
1719
+ };
1720
+ }
1721
+ const havingInput = new GraphQLInputObjectType({
1722
+ name: `${name}Having`,
1723
+ fields: havingFields
1724
+ });
1725
+ this._inputObjects[`${id}-having`] = havingInput;
1726
+ // GroupOrderBy: every leaf orders by the shared OrderByDirection enum. The keys
1727
+ // sub-input covers all groupable fields; each op sub-input mirrors that op's
1728
+ // aggregate field map, emitted only when the op's fields exist.
1729
+ const buildDirectionInput = (subName, keys)=>{
1730
+ const fields = {};
1731
+ for (const key of keys){
1732
+ fields[key] = {
1733
+ type: OrderByDirection
1734
+ };
1735
+ }
1736
+ return new GraphQLInputObjectType({
1737
+ name: subName,
1738
+ fields
1739
+ });
1740
+ };
1741
+ const groupKeysOrderBy = buildDirectionInput(`${name}GroupKeysOrderBy`, groupableTypes.keys());
1742
+ this._inputObjects[`${id}-group-keys-order`] = groupKeysOrderBy;
1743
+ const groupOrderByFields = {
1744
+ keys: {
1745
+ type: groupKeysOrderBy
1746
+ },
1747
+ count: {
1748
+ type: OrderByDirection
1749
+ }
1750
+ };
1751
+ if (sumType != null && avgType != null) {
1752
+ const sumOrderBy = buildDirectionInput(`${name}SumFieldsOrderBy`, Object.keys(sumType.getFields()));
1753
+ const avgOrderBy = buildDirectionInput(`${name}AvgFieldsOrderBy`, Object.keys(avgType.getFields()));
1754
+ this._inputObjects[`${id}-sum-order`] = sumOrderBy;
1755
+ this._inputObjects[`${id}-avg-order`] = avgOrderBy;
1756
+ groupOrderByFields.sum = {
1757
+ type: sumOrderBy
1758
+ };
1759
+ groupOrderByFields.average = {
1760
+ type: avgOrderBy
1761
+ };
1762
+ }
1763
+ if (minMaxType != null) {
1764
+ const minOrderBy = buildDirectionInput(`${name}MinFieldsOrderBy`, Object.keys(minMaxType.getFields()));
1765
+ const maxOrderBy = buildDirectionInput(`${name}MaxFieldsOrderBy`, Object.keys(minMaxType.getFields()));
1766
+ this._inputObjects[`${id}-min-order`] = minOrderBy;
1767
+ this._inputObjects[`${id}-max-order`] = maxOrderBy;
1768
+ groupOrderByFields.min = {
1769
+ type: minOrderBy
1770
+ };
1771
+ groupOrderByFields.max = {
1772
+ type: maxOrderBy
1773
+ };
1774
+ }
1775
+ const groupOrderBy = new GraphQLInputObjectType({
1776
+ name: `${name}GroupOrderBy`,
1777
+ fields: groupOrderByFields
1778
+ });
1779
+ this._inputObjects[`${id}-group-order`] = groupOrderBy;
1780
+ return groupedAggregateType;
1781
+ }
1782
+ /**
1783
+ * Inspect the selection set under the `aggregates` field to determine exactly
1784
+ * which aggregation work to ask the store for: whether a count was selected,
1785
+ * and for each of sum/average/min/max, which field subselections were
1786
+ * requested. Field aggregates not selected are never computed, so a
1787
+ * `{ count }`-only query issues `aggregates: []`.
1788
+ *
1789
+ * Reads the resolved field nodes via `info.fieldNodes`. Selections reached
1790
+ * through fragment spreads and inline fragments are expanded transparently, so
1791
+ * `aggregates { ...AggFields }`, `aggregates { ... { count } }` and
1792
+ * `sum { ...SumFields }` all request the same work as their inlined forms.
1793
+ */ _buildAggregateRequest(id, info) {
1794
+ const numericByKey = this._aggregateNumericByModel[id] ?? new Map();
1795
+ const ops = [
1796
+ 'sum',
1797
+ 'average',
1798
+ 'min',
1799
+ 'max'
1800
+ ];
1801
+ let count = false;
1802
+ const aggregates = [];
1803
+ for (const fieldNode of info.fieldNodes){
1804
+ const selections = fieldNode.selectionSet?.selections ?? [];
1805
+ for (const selection of this._expandFieldSelections(selections, info, new Set())){
1806
+ const fieldName = selection.name.value;
1807
+ if (fieldName === 'count') {
1808
+ count = true;
1809
+ continue;
1810
+ }
1811
+ if (!ops.includes(fieldName)) continue;
1812
+ const op = fieldName;
1813
+ const opSelections = selection.selectionSet?.selections ?? [];
1814
+ for (const opSelection of this._expandFieldSelections(opSelections, info, new Set())){
1815
+ const key = opSelection.name.value;
1816
+ // For a top-level scalar field the JSON path is just the field key.
1817
+ aggregates.push({
1818
+ op,
1819
+ keys: [
1820
+ key
1821
+ ],
1822
+ numeric: numericByKey.get(key) ?? false
1823
+ });
1824
+ }
1825
+ }
1826
+ }
1827
+ return {
1828
+ count,
1829
+ aggregates
1830
+ };
1831
+ }
1832
+ /**
1833
+ * Inspect the selection set under the `groupedAggregates` field to determine the
1834
+ * aggregation work to ask the store for. The aggregate ops (`count`/`sum`/
1835
+ * `average`/`min`/`max`) are nested directly under the field alongside `keys`,
1836
+ * which is skipped here since it is shaped by the `groupBy` argument, not the
1837
+ * selection. Fragment spreads and inline fragments are expanded transparently.
1838
+ * Selecting only `keys` and `count` yields `aggregates: []`.
1839
+ */ _buildGroupedAggregateRequest(id, info) {
1840
+ const numericByKey = this._aggregateNumericByModel[id] ?? new Map();
1841
+ const ops = [
1842
+ 'sum',
1843
+ 'average',
1844
+ 'min',
1845
+ 'max'
1846
+ ];
1847
+ let count = false;
1848
+ const aggregates = [];
1849
+ for (const fieldNode of info.fieldNodes){
1850
+ const selections = fieldNode.selectionSet?.selections ?? [];
1851
+ for (const selection of this._expandFieldSelections(selections, info, new Set())){
1852
+ const fieldName = selection.name.value;
1853
+ if (fieldName === 'count') {
1854
+ count = true;
1855
+ continue;
1856
+ }
1857
+ // `keys` is driven by the groupBy argument, not the aggregate look-ahead.
1858
+ if (fieldName === 'keys') continue;
1859
+ if (!ops.includes(fieldName)) continue;
1860
+ const op = fieldName;
1861
+ const opSelections = selection.selectionSet?.selections ?? [];
1862
+ for (const opSelection of this._expandFieldSelections(opSelections, info, new Set())){
1863
+ const key = opSelection.name.value;
1864
+ aggregates.push({
1865
+ op,
1866
+ keys: [
1867
+ key
1868
+ ],
1869
+ numeric: numericByKey.get(key) ?? false
1870
+ });
1871
+ }
1872
+ }
1873
+ }
1874
+ return {
1875
+ count,
1876
+ aggregates
1877
+ };
1878
+ }
1879
+ /**
1880
+ * Translate the `orderBy` argument entries into store ordering terms over each
1881
+ * finalised group. Each entry's present leaves become a term whose `path`
1882
+ * navigates the group object (`['count']`, `['keys', field]`, `['sum', field]`,
1883
+ * etc.) and whose `numeric` flag drives arithmetic comparison. List order is
1884
+ * preserved so earlier terms take precedence and later terms break ties.
1885
+ */ _buildGroupOrderBy(id, orderBy) {
1886
+ if (orderBy == null) return [];
1887
+ const aggregateNumeric = this._aggregateNumericByModel[id] ?? new Map();
1888
+ const groupKeyNumeric = this._groupKeyNumericByModel[id] ?? new Map();
1889
+ const terms = [];
1890
+ for (const entry of orderBy){
1891
+ if (entry.count != null) {
1892
+ terms.push({
1893
+ path: [
1894
+ 'count'
1895
+ ],
1896
+ direction: entry.count,
1897
+ numeric: true
1898
+ });
1899
+ }
1900
+ for (const [field, direction] of Object.entries(entry.keys ?? {})){
1901
+ terms.push({
1902
+ path: [
1903
+ 'keys',
1904
+ field
1905
+ ],
1906
+ direction,
1907
+ numeric: groupKeyNumeric.get(field) ?? false
1908
+ });
1909
+ }
1910
+ for (const [field, direction] of Object.entries(entry.sum ?? {})){
1911
+ terms.push({
1912
+ path: [
1913
+ 'sum',
1914
+ field
1915
+ ],
1916
+ direction,
1917
+ numeric: aggregateNumeric.get(field) ?? false
1918
+ });
1919
+ }
1920
+ for (const [field, direction] of Object.entries(entry.average ?? {})){
1921
+ // Averages are always fractional, so they always compare numerically.
1922
+ terms.push({
1923
+ path: [
1924
+ 'average',
1925
+ field
1926
+ ],
1927
+ direction,
1928
+ numeric: true
1929
+ });
1930
+ }
1931
+ for (const [field, direction] of Object.entries(entry.min ?? {})){
1932
+ terms.push({
1933
+ path: [
1934
+ 'min',
1935
+ field
1936
+ ],
1937
+ direction,
1938
+ numeric: aggregateNumeric.get(field) ?? false
1939
+ });
1940
+ }
1941
+ for (const [field, direction] of Object.entries(entry.max ?? {})){
1942
+ terms.push({
1943
+ path: [
1944
+ 'max',
1945
+ field
1946
+ ],
1947
+ direction,
1948
+ numeric: aggregateNumeric.get(field) ?? false
1949
+ });
1950
+ }
1951
+ }
1952
+ return terms;
1953
+ }
1954
+ /**
1955
+ * Walk a selection set and yield the underlying field nodes, expanding
1956
+ * fragment spreads and inline fragments in place. The `visited` set tracks
1957
+ * named fragments already entered so cyclic fragment references terminate.
1958
+ */ *_expandFieldSelections(selections, info, visited) {
1959
+ for (const selection of selections){
1960
+ switch(selection.kind){
1961
+ case Kind.FIELD:
1962
+ yield selection;
1963
+ break;
1964
+ case Kind.INLINE_FRAGMENT:
1965
+ yield* this._expandFieldSelections(selection.selectionSet.selections, info, visited);
1966
+ break;
1967
+ case Kind.FRAGMENT_SPREAD:
1968
+ {
1969
+ const fragmentName = selection.name.value;
1970
+ if (visited.has(fragmentName)) break;
1971
+ const fragment = info.fragments[fragmentName];
1972
+ if (fragment == null) break;
1973
+ visited.add(fragmentName);
1974
+ yield* this._expandFieldSelections(fragment.selectionSet.selections, info, visited);
1975
+ break;
1976
+ }
1977
+ }
1978
+ }
1979
+ }
1980
+ _buildDocumentFields(id, model, name) {
1981
+ const dataObject = this._buildDocumentDataObject(id, model, name);
1982
+ const definitions = this._getDefinitions();
1983
+ const fields = {
1984
+ ...definitions.documentNodeFields,
1985
+ data: {
1986
+ type: new GraphQLNonNull(dataObject)
1987
+ }
1988
+ };
1989
+ for (const [key, field] of Object.entries(dataObject.getFields())){
1990
+ const meta = model.fieldsMeta[key];
1991
+ if (meta == null) {
1992
+ continue;
1993
+ }
1994
+ switch(meta.type){
1995
+ case 'account':
1996
+ {
1997
+ if (isList(field.type)) {
1998
+ fields[`${key}Accounts`] = {
1999
+ type: new GraphQLNonNull(toOuputList(definitions.accountObject)),
2000
+ resolve: (doc)=>doc.data?.[key] ?? []
2001
+ };
2002
+ } else {
2003
+ fields[`${key}Account`] = {
2004
+ type: definitions.accountObject,
2005
+ resolve: (doc)=>doc.data?.[key] ?? null
2006
+ };
2007
+ }
2008
+ break;
2009
+ }
2010
+ case 'attachment':
2011
+ {
2012
+ if (isList(field.type)) {
2013
+ fields[`${key}Attachments`] = {
2014
+ type: new GraphQLNonNull(toOuputList(definitions.attachmentObject)),
2015
+ resolve: (doc)=>{
2016
+ const ids = doc.data?.[key] ?? [];
2017
+ return ids.map(AttachmentID.fromString);
2018
+ }
2019
+ };
2020
+ } else {
2021
+ fields[`${key}Attachment`] = {
2022
+ type: definitions.attachmentObject,
2023
+ resolve: (doc)=>{
2024
+ const id = doc.data?.[key];
2025
+ return id ? AttachmentID.fromString(id) : null;
2026
+ }
2027
+ };
2028
+ }
2029
+ break;
2030
+ }
2031
+ case 'document':
2032
+ {
2033
+ const relationModel = meta.model;
2034
+ if (typeof relationModel === 'undefined') {
2035
+ continue;
2036
+ }
2037
+ let relationModelID;
2038
+ let relationType;
2039
+ if (relationModel === null) {
2040
+ relationModelID = null;
2041
+ relationType = definitions.documentInterface;
2042
+ } else {
2043
+ relationModelID = getGlobalID(relationModel, id).toString();
2044
+ // Check if the related model is included in the schema
2045
+ if (this._includedModelIDs.has(relationModelID)) {
2046
+ relationType = this._buildDocument(relationModelID);
2047
+ } else {
2048
+ // Fall back to generic DocumentNode interface
2049
+ relationType = definitions.documentInterface;
2050
+ relationModelID = null; // Treat as unspecified model
2051
+ }
2052
+ }
2053
+ if (isList(field.type)) {
2054
+ // TODO: add suport for filters?
2055
+ fields[`${key}Documents`] = {
2056
+ type: new GraphQLNonNull(toOuputList(relationType)),
2057
+ resolve: async (doc, _args, ctx)=>{
2058
+ const relationIDs = doc.data?.[key] ?? [];
2059
+ return relationIDs.length ? await ctx.resolveList(this.getModelIDs(relationModelID), relationIDs) : [];
2060
+ }
2061
+ };
2062
+ } else {
2063
+ fields[`${key}Document`] = {
2064
+ type: relationType,
2065
+ resolve: async (doc, _args, ctx)=>{
2066
+ const relationID = doc.data?.[key];
2067
+ return relationID ? await ctx.loadDocument(relationID) : null;
2068
+ }
2069
+ };
2070
+ }
2071
+ break;
2072
+ }
2073
+ }
2074
+ }
2075
+ for (const iid of model.interfaces ?? []){
2076
+ const interfaceID = getGlobalID(iid, id).toString();
2077
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[interfaceID] ?? {})){
2078
+ for (const relation of relations){
2079
+ if (!this._includedModelIDs.has(relation.model)) continue;
2080
+ this._buildRelationFields(fields, fieldName, relation);
2081
+ }
2082
+ }
2083
+ }
2084
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[id] ?? {})){
2085
+ for (const relation of relations){
2086
+ if (!this._includedModelIDs.has(relation.model)) continue;
2087
+ this._buildRelationFields(fields, fieldName, relation);
2088
+ }
2089
+ }
2090
+ return fields;
2091
+ }
2092
+ _buildDocumentDataObject(id, model, name) {
2093
+ const typeID = `${id}-data`;
2094
+ if (this._types[typeID] != null) {
2095
+ return this._types[typeID];
2096
+ }
2097
+ const required = model.schema.required ?? [];
2098
+ const fields = {};
2099
+ for (const [key, value] of Object.entries(model.schema.properties ?? {})){
2100
+ const fieldRef = value.$ref;
2101
+ if (fieldRef == null) {
2102
+ throw new Error(`Missing ref for field ${key} of object ${id}`);
2103
+ }
2104
+ const type = this._getReference(fieldRef);
2105
+ fields[key] = {
2106
+ type: required.includes(key) ? new GraphQLNonNull(type) : type
2107
+ };
2108
+ }
2109
+ const config = {
2110
+ name: `${name}Data`,
2111
+ interfaces: ()=>{
2112
+ return model.interfaces.map((interfaceID)=>{
2113
+ const typeID = `${getGlobalID(interfaceID, id).toString()}-data`;
2114
+ return this._types[typeID];
2115
+ }).filter((type)=>type != null);
2116
+ },
2117
+ fields
2118
+ };
2119
+ const object = model.behavior === 'interface' ? new GraphQLInterfaceType(config) : new GraphQLObjectType(config);
2120
+ this._types[typeID] = object;
2121
+ return object;
2122
+ }
2123
+ _buildDocumentInput(id, model) {
2124
+ if (model.behavior === 'interface') {
2125
+ // No mutations for interfaces
2126
+ return;
2127
+ }
2128
+ const setFields = {};
2129
+ const updateFields = {};
2130
+ for (const [key, value] of Object.entries(model.schema.properties)){
2131
+ const path = value.$ref;
2132
+ const setType = this._getReferenceInput(path, false);
2133
+ setFields[key] = {
2134
+ type: model.schema.required.includes(key) ? new GraphQLNonNull(setType) : setType
2135
+ };
2136
+ const id = getReferenceID(path);
2137
+ const schema = this._getReferenceSchema(id);
2138
+ if (schema.const == null) {
2139
+ updateFields[key] = {
2140
+ type: this._getReferenceInput(path, true)
2141
+ };
2142
+ }
2143
+ }
2144
+ const name = this._aliases[id];
2145
+ this._inputObjects[id] = new GraphQLInputObjectType({
2146
+ name: `${name}Input`,
2147
+ fields: setFields
2148
+ });
2149
+ this._inputObjects[`${id}-update`] = new GraphQLInputObjectType({
2150
+ name: `Partial${name}Input`,
2151
+ fields: updateFields
2152
+ });
2153
+ }
2154
+ _buildReferenceFilterInput(path, parentName = '') {
2155
+ const id = getReferenceID(path);
2156
+ const schema = this._getReferenceSchema(id);
2157
+ let refParentName = parentName;
2158
+ if (schema.type === 'object') {
2159
+ return schema.additionalProperties ? null : this._buildObjectFilterInput(id, schema, parentName);
2160
+ }
2161
+ if (schema.type === 'array') {
2162
+ const name = this._aliases[id] ?? pascalCase(parentName + schema.title);
2163
+ refParentName = name;
2164
+ // const itemType = this._buildReferenceFilterInput(schema.items.$ref, schema.title)
2165
+ return new GraphQLInputObjectType({
2166
+ name: `${name}Filter`,
2167
+ isOneOf: true,
2168
+ fields: {
2169
+ // DB does not support filtering on array values
2170
+ // contains: { type: toList(itemType) },
2171
+ // isEmpty: { type: GraphQLBoolean },
2172
+ isNull: {
2173
+ type: GraphQLBoolean
2174
+ }
2175
+ }
2176
+ });
2177
+ }
2178
+ const scalar = this._buildScalar(id, schema, refParentName);
2179
+ const knownFilter = VALUE_FILTER_INPUT_TYPES[scalar.name];
2180
+ if (knownFilter != null) {
2181
+ return this._inputObjects[knownFilter];
2182
+ }
2183
+ const filter = isEnumType(scalar) ? createEnumValueFilterInput(scalar) : createScalarValueFilterInput(scalar);
2184
+ if (this._inputObjects[filter.name] == null) {
2185
+ this._inputObjects[filter.name] = filter;
2186
+ }
2187
+ return this._inputObjects[filter.name];
2188
+ }
2189
+ _buildReferenceOrderByInput(path, parentName = '') {
2190
+ const id = getReferenceID(path);
2191
+ const schema = this._getReferenceSchema(id);
2192
+ switch(schema.type){
2193
+ case 'array':
2194
+ return null;
2195
+ case 'object':
2196
+ return schema.additionalProperties ? null : this._buildObjectOrderByInput(id, schema, parentName);
2197
+ default:
2198
+ return OrderByDirection;
2199
+ }
2200
+ }
2201
+ _buildObjectFilterInput(id, schema, parentName = '', isDocument = false) {
2202
+ const inputID = `${id}-filter`;
2203
+ const existing = this._inputObjects[inputID];
2204
+ if (existing != null) {
2205
+ return existing;
2206
+ }
2207
+ const name = this._aliases[id] ?? pascalCase(parentName + (schema.title ?? ''));
2208
+ const fields = isDocument ? {
2209
+ _owner: {
2210
+ type: this._inputObjects.AccountValueFilter
2211
+ }
2212
+ } : {};
2213
+ for (const [key, value] of Object.entries(schema.properties)){
2214
+ const type = this._buildReferenceFilterInput(value.$ref, name);
2215
+ if (type !== null) {
2216
+ fields[key] = {
2217
+ type
2218
+ };
2219
+ }
2220
+ }
2221
+ const objectInput = new GraphQLInputObjectType({
2222
+ name: `${name}ObjectFilter`,
2223
+ fields
2224
+ });
2225
+ const input = new GraphQLInputObjectType({
2226
+ name: `${name}Filter`,
2227
+ isOneOf: true,
2228
+ fields: ()=>({
2229
+ where: {
2230
+ type: objectInput
2231
+ },
2232
+ and: {
2233
+ type: toList(this._inputObjects[inputID])
2234
+ },
2235
+ or: {
2236
+ type: toList(this._inputObjects[inputID])
2237
+ },
2238
+ not: {
2239
+ type: this._inputObjects[inputID]
2240
+ }
2241
+ })
2242
+ });
2243
+ this._inputObjects[inputID] = input;
2244
+ return input;
2245
+ }
2246
+ _buildObjectOrderByInput(id, schema, parentName = '', isDocument = false) {
2247
+ const inputID = `${id}-order`;
2248
+ const existing = this._inputObjects[inputID];
2249
+ if (existing != null) {
2250
+ return existing;
2251
+ }
2252
+ const name = this._aliases[id] ?? pascalCase(parentName + (schema.title ?? ''));
2253
+ const fields = isDocument ? {
2254
+ _docOwner: {
2255
+ type: OrderByDirection
2256
+ },
2257
+ _createdAt: {
2258
+ type: OrderByDirection
2259
+ }
2260
+ } : {};
2261
+ for (const [key, value] of Object.entries(schema.properties)){
2262
+ const type = this._buildReferenceOrderByInput(value.$ref, name);
2263
+ if (type != null) {
2264
+ fields[key] = {
2265
+ type
2266
+ };
2267
+ }
2268
+ }
2269
+ const input = new GraphQLInputObjectType({
2270
+ name: `${name}OrderBy`,
2271
+ isOneOf: true,
2272
+ fields
2273
+ });
2274
+ this._inputObjects[inputID] = input;
2275
+ return input;
2276
+ }
2277
+ _buildSetInputObjectType(id, withFields, relationField) {
2278
+ const relationFieldName = relationField ? pascalCase(relationField) : '';
2279
+ const inputID = `${id}-with-${relationFieldName}`;
2280
+ const existing = this._inputObjects[inputID];
2281
+ if (existing != null) {
2282
+ return existing;
2283
+ }
2284
+ const model = this._record[id];
2285
+ const name = this._aliases[id];
2286
+ this._inputObjects[inputID] = new GraphQLInputObjectType({
2287
+ name: `With${relationFieldName}${name}Input`,
2288
+ fields: ()=>{
2289
+ const fields = {};
2290
+ for (const fieldName of withFields){
2291
+ const field = model.schema.properties[fieldName];
2292
+ if (field == null) {
2293
+ throw new Error(`Field ${fieldName} not found on model ${name}`);
2294
+ }
2295
+ const type = this._getReferenceInput(field.$ref, false);
2296
+ fields[fieldName] = {
2297
+ type: new GraphQLNonNull(type)
2298
+ };
2299
+ }
2300
+ return fields;
2301
+ }
2302
+ });
2303
+ return this._inputObjects[inputID];
2304
+ }
2305
+ _buildReference(id, schema, parentName = '') {
2306
+ if (this._types[id] != null) {
2307
+ return this._types[id];
2308
+ }
2309
+ switch(schema.type){
2310
+ case 'array':
2311
+ this._types[id] = this._buildArray(schema, parentName);
2312
+ return this._types[id];
2313
+ case 'object':
2314
+ this._types[id] = schema.additionalProperties ? GraphQLJSONObject : this._buildObject(id, schema, parentName);
2315
+ return this._types[id];
2316
+ default:
2317
+ return this._buildScalar(id, schema, parentName);
2318
+ }
2319
+ }
2320
+ _buildScalar(id, schema, parentName = '') {
2321
+ if (this._types[id] != null) {
2322
+ return this._types[id];
2323
+ }
2324
+ switch(schema.type){
2325
+ case 'boolean':
2326
+ this._types[id] = GraphQLBoolean;
2327
+ break;
2328
+ case 'integer':
2329
+ this._types[id] = GraphQLInt;
2330
+ break;
2331
+ case 'number':
2332
+ this._types[id] = GraphQLFloat;
2333
+ break;
2334
+ case 'string':
2335
+ this._types[id] = this._buildString(schema, parentName);
2336
+ break;
2337
+ }
2338
+ return this._types[id];
2339
+ }
2340
+ _buildArray(schema, parentName = '') {
2341
+ const itemRef = schema.items.$ref;
2342
+ return new GraphQLList(new GraphQLNonNull(this._getReference(itemRef, pascalCase(parentName + schema.title))));
2343
+ }
2344
+ _buildObject(id, schema, parentName = '') {
2345
+ const name = pascalCase(parentName + schema.title);
2346
+ const fields = this._buildObjectFields(id, schema, name);
2347
+ return new GraphQLObjectType({
2348
+ name,
2349
+ fields
2350
+ });
2351
+ }
2352
+ _buildObjectFields(id, schema, parentName = '') {
2353
+ const required = schema.required ?? [];
2354
+ const fields = {};
2355
+ for (const [key, value] of Object.entries(schema.properties ?? {})){
2356
+ const fieldRef = value.$ref;
2357
+ if (fieldRef == null) {
2358
+ throw new Error(`Missing ref for field ${key} of object ${id}`);
2359
+ }
2360
+ const type = this._getReference(fieldRef, parentName);
2361
+ fields[key] = {
2362
+ type: required.includes(key) ? new GraphQLNonNull(type) : type
2363
+ };
2364
+ }
2365
+ return fields;
2366
+ }
2367
+ _buildRelationFields(fields, fieldName, relation) {
2368
+ if (relation.isList) {
2369
+ // DB does not support matches for array containing values
2370
+ return;
2371
+ }
2372
+ const relationID = relation.model;
2373
+ const relationName = `${pascalCase(fieldName)}Of${this._aliases[relationID]}`;
2374
+ fields[`in${relationName}`] = {
2375
+ type: new GraphQLNonNull(this._types[`${relationID}-connection`]),
2376
+ args: {
2377
+ ...connectionArgs,
2378
+ filter: {
2379
+ type: this._inputObjects[`${relationID}-filter`]
2380
+ },
2381
+ orderBy: {
2382
+ type: toList(this._inputObjects[`${relationID}-order`])
2383
+ }
2384
+ },
2385
+ resolve: (doc, args, ctx)=>{
2386
+ const docFilter = {
2387
+ where: {
2388
+ [fieldName]: {
2389
+ equalTo: doc.id
2390
+ }
2391
+ }
2392
+ };
2393
+ const filter = args.filter ? {
2394
+ and: [
2395
+ args.filter,
2396
+ docFilter
2397
+ ]
2398
+ } : docFilter;
2399
+ return ctx.resolveConnection(this.getModelIDs(relationID), {
2400
+ ...args,
2401
+ filter
2402
+ });
2403
+ }
2404
+ };
2405
+ }
2406
+ _buildRelationSubscriptions(fields, modelID, fieldName, relation) {
2407
+ if (relation.isList) {
2408
+ // Inverse relations to list fields are not supported in connections so we skip them for subscriptions as well
2409
+ return;
2410
+ }
2411
+ const targetFieldName = pascalCase(fieldName);
2412
+ fields[`edge${this._aliases[relation.model]}AddedAs${targetFieldName}To${this._aliases[modelID]}`] = {
2413
+ type: new GraphQLNonNull(this._types[`${relation.model}-edge`]),
2414
+ args: {
2415
+ id: {
2416
+ type: new GraphQLNonNull(GraphQLID)
2417
+ }
2418
+ },
2419
+ resolve: (edge)=>edge,
2420
+ subscribe: (_src, args, ctx)=>{
2421
+ return ctx.subscribeToDocumentEdgeAdded(this.getModelIDs(relation.model), (data)=>data[fieldName] === args.id);
2422
+ }
2423
+ };
2424
+ fields[`edge${this._aliases[relation.model]}RemovedAs${targetFieldName}From${this._aliases[modelID]}`] = {
2425
+ type: new GraphQLNonNull(GraphQLID),
2426
+ args: {
2427
+ id: {
2428
+ type: new GraphQLNonNull(GraphQLID)
2429
+ }
2430
+ },
2431
+ resolve: (id)=>id,
2432
+ subscribe: (_src, args, ctx)=>{
2433
+ return ctx.subscribeToDocumentEdgeRemoved(this.getModelIDs(relation.model), (data)=>data[fieldName] === args.id);
2434
+ }
2435
+ };
2436
+ }
2437
+ _buildString(schema, parentName) {
2438
+ if (schema.const != null) {
2439
+ return GraphQLString;
2440
+ }
2441
+ if (schema.enum != null) {
2442
+ return this._buildEnumString(schema, parentName);
2443
+ }
2444
+ if (schema.format != null) {
2445
+ return this._buildFormatString(schema);
2446
+ }
2447
+ return this._buildCustomString(schema);
2448
+ }
2449
+ _buildCustomString(schema) {
2450
+ if (schema.title == null) {
2451
+ // Plain string type
2452
+ return GraphQLString;
2453
+ }
2454
+ const title = schema.title.toLowerCase();
2455
+ return STRING_TITLES[title] ?? GraphQLString;
2456
+ }
2457
+ _buildEnumString(schema, parentName = '') {
2458
+ const name = pascalCase(parentName + schema.title);
2459
+ const typeName = `${name}-enum`;
2460
+ if (this._types[typeName] == null) {
2461
+ const values = {};
2462
+ for (const value of schema.enum){
2463
+ values[value] = {
2464
+ value
2465
+ };
2466
+ }
2467
+ this._types[typeName] = new GraphQLEnumType({
2468
+ name,
2469
+ values
2470
+ });
2471
+ }
2472
+ return this._types[typeName];
2473
+ }
2474
+ _buildFormatString(schema) {
2475
+ const scalar = STRING_FORMATS[schema.format];
2476
+ if (scalar == null) {
2477
+ throw new Error(`Unsupported string format: ${schema.format}`);
2478
+ }
2479
+ return scalar;
2480
+ }
2481
+ }
2482
+ export function createSchema(params) {
2483
+ return new SchemaBuilder(params).build();
2484
+ }