@kubun/graphql 0.10.1 → 0.11.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,2448 @@
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
+ // Add access control mutations if mutations are enabled
585
+ if (this._includeMutations) {
586
+ this._mutations.setModelAccessDefaults = {
587
+ type: ModelAccessDefaults,
588
+ args: {
589
+ modelID: {
590
+ type: new GraphQLNonNull(GraphQLID)
591
+ },
592
+ permissionType: {
593
+ type: new GraphQLNonNull(GraphQLString)
594
+ },
595
+ accessLevel: {
596
+ type: new GraphQLNonNull(GraphQLString)
597
+ },
598
+ allowedDIDs: {
599
+ type: toList(GraphQLString)
600
+ },
601
+ allowedCircles: {
602
+ type: toList(GraphQLString)
603
+ },
604
+ allowedGroups: {
605
+ type: toList(GraphQLString)
606
+ }
607
+ },
608
+ resolve: async (_, args, ctx)=>{
609
+ const allowedDIDs = args.allowedDIDs ?? null;
610
+ const allowedCircles = args.allowedCircles ?? null;
611
+ const allowedGroups = args.allowedGroups ?? null;
612
+ validateAccessIdentifiers({
613
+ allowedDIDs,
614
+ allowedCircles,
615
+ allowedGroups
616
+ });
617
+ return await ctx.executeSetModelAccessDefaults({
618
+ modelID: args.modelID,
619
+ permissionType: args.permissionType,
620
+ accessLevel: args.accessLevel,
621
+ allowedDIDs,
622
+ allowedCircles,
623
+ allowedGroups
624
+ });
625
+ }
626
+ };
627
+ this._mutations.removeModelAccessDefaults = {
628
+ type: GraphQLBoolean,
629
+ args: {
630
+ modelID: {
631
+ type: new GraphQLNonNull(GraphQLID)
632
+ },
633
+ permissionTypes: {
634
+ type: new GraphQLNonNull(toList(GraphQLString))
635
+ }
636
+ },
637
+ resolve: async (_, args, ctx)=>{
638
+ await ctx.executeRemoveModelAccessDefaults(args.modelID, args.permissionTypes);
639
+ return true;
640
+ }
641
+ };
642
+ this._mutations.setDocumentAccessOverride = {
643
+ type: definitions.documentInterface,
644
+ args: {
645
+ documentID: {
646
+ type: new GraphQLNonNull(GraphQLID)
647
+ },
648
+ permissionType: {
649
+ type: new GraphQLNonNull(GraphQLString)
650
+ },
651
+ accessLevel: {
652
+ type: new GraphQLNonNull(GraphQLString)
653
+ },
654
+ allowedDIDs: {
655
+ type: toList(GraphQLString)
656
+ },
657
+ allowedCircles: {
658
+ type: toList(GraphQLString)
659
+ },
660
+ allowedGroups: {
661
+ type: toList(GraphQLString)
662
+ }
663
+ },
664
+ resolve: async (_, args, ctx)=>{
665
+ const allowedDIDs = args.allowedDIDs ?? null;
666
+ const allowedCircles = args.allowedCircles ?? null;
667
+ const allowedGroups = args.allowedGroups ?? null;
668
+ validateAccessIdentifiers({
669
+ allowedDIDs,
670
+ allowedCircles,
671
+ allowedGroups
672
+ });
673
+ return await ctx.executeSetDocumentAccessOverride({
674
+ documentID: args.documentID,
675
+ permissionType: args.permissionType,
676
+ accessLevel: args.accessLevel,
677
+ allowedDIDs,
678
+ allowedCircles,
679
+ allowedGroups
680
+ });
681
+ }
682
+ };
683
+ this._mutations.removeDocumentAccessOverride = {
684
+ type: GraphQLBoolean,
685
+ args: {
686
+ documentID: {
687
+ type: new GraphQLNonNull(GraphQLID)
688
+ },
689
+ permissionTypes: {
690
+ type: new GraphQLNonNull(toList(GraphQLString))
691
+ }
692
+ },
693
+ resolve: async (_, args, ctx)=>{
694
+ await ctx.executeRemoveDocumentAccessOverride(args.documentID, args.permissionTypes);
695
+ return true;
696
+ }
697
+ };
698
+ // Transaction lifecycle mutations
699
+ const BeginTransactionPayload = new GraphQLObjectType({
700
+ name: 'BeginTransactionPayload',
701
+ fields: {
702
+ transactionID: {
703
+ type: new GraphQLNonNull(GraphQLID)
704
+ }
705
+ }
706
+ });
707
+ const CommitTransactionPayload = new GraphQLObjectType({
708
+ name: 'CommitTransactionPayload',
709
+ fields: {
710
+ success: {
711
+ type: new GraphQLNonNull(GraphQLBoolean)
712
+ }
713
+ }
714
+ });
715
+ const RollbackTransactionPayload = new GraphQLObjectType({
716
+ name: 'RollbackTransactionPayload',
717
+ fields: {
718
+ success: {
719
+ type: new GraphQLNonNull(GraphQLBoolean)
720
+ }
721
+ }
722
+ });
723
+ this._mutations.beginTransaction = {
724
+ type: new GraphQLNonNull(BeginTransactionPayload),
725
+ args: {},
726
+ resolve: (_, _args, ctx)=>{
727
+ return ctx.beginTransaction();
728
+ }
729
+ };
730
+ this._mutations.commitTransaction = {
731
+ type: new GraphQLNonNull(CommitTransactionPayload),
732
+ args: {
733
+ transactionID: {
734
+ type: new GraphQLNonNull(GraphQLID)
735
+ }
736
+ },
737
+ resolve: async (_, args, ctx)=>{
738
+ return await ctx.commitTransaction(args.transactionID);
739
+ }
740
+ };
741
+ this._mutations.rollbackTransaction = {
742
+ type: new GraphQLNonNull(RollbackTransactionPayload),
743
+ args: {
744
+ transactionID: {
745
+ type: new GraphQLNonNull(GraphQLID)
746
+ }
747
+ },
748
+ resolve: (_, args, ctx)=>{
749
+ return ctx.rollbackTransaction(args.transactionID);
750
+ }
751
+ };
752
+ }
753
+ const config = {
754
+ query: new GraphQLObjectType({
755
+ name: 'Query',
756
+ fields: queryFields
757
+ }),
758
+ // Ensure custom scalars stay reachable from the schema typeMap so plugin
759
+ // SDL extensions can reference them by name (URL, DID, DateTimeISO)
760
+ // regardless of whether any model field happens to use them.
761
+ types: [
762
+ GraphQLURL,
763
+ GraphQLDID,
764
+ GraphQLDateTimeISO
765
+ ]
766
+ };
767
+ if (this._includeMutations) {
768
+ config.mutation = new GraphQLObjectType({
769
+ name: 'Mutation',
770
+ fields: this._mutations
771
+ });
772
+ }
773
+ if (this._includeSubscriptions) {
774
+ config.subscription = new GraphQLObjectType({
775
+ name: 'Subscription',
776
+ fields: ()=>{
777
+ const fields = {
778
+ ...this._subscriptions
779
+ };
780
+ if (this._includeSubscriptions) {
781
+ fields.documentChanged = {
782
+ type: new GraphQLNonNull(definitions.documentInterface),
783
+ args: {
784
+ id: {
785
+ type: new GraphQLNonNull(GraphQLID)
786
+ }
787
+ },
788
+ resolve: (doc)=>doc,
789
+ subscribe: (_, args, ctx)=>{
790
+ return ctx.subscribeToDocumentChanged(args.id);
791
+ }
792
+ };
793
+ fields.documentRemoved = {
794
+ type: new GraphQLNonNull(GraphQLID),
795
+ resolve: (id)=>id,
796
+ subscribe: (_src, _args, ctx)=>{
797
+ return ctx.subscribeToDocumentRemoved();
798
+ }
799
+ };
800
+ for (const [id, model] of Object.entries(this._record)){
801
+ if (!this._includedModelIDs.has(id)) continue;
802
+ const name = this._aliases[id];
803
+ fields[`new${name}Created`] = {
804
+ type: new GraphQLNonNull(this._types[id]),
805
+ args: {
806
+ owner: {
807
+ type: GraphQLID
808
+ }
809
+ },
810
+ resolve: (doc)=>doc,
811
+ subscribe: (_src, args, ctx)=>{
812
+ return ctx.subscribeToDocumentCreated(this.getModelIDs(id), args.owner ?? undefined);
813
+ }
814
+ };
815
+ for (const iid of model.interfaces ?? []){
816
+ const interfaceID = getGlobalID(iid, id).toString();
817
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[interfaceID] ?? {})){
818
+ for (const relation of relations){
819
+ this._buildRelationSubscriptions(fields, interfaceID, fieldName, relation);
820
+ }
821
+ }
822
+ }
823
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[id] ?? {})){
824
+ for (const relation of relations){
825
+ if (!this._includedModelIDs.has(relation.model)) continue;
826
+ this._buildRelationSubscriptions(fields, id, fieldName, relation);
827
+ }
828
+ }
829
+ }
830
+ }
831
+ return fields;
832
+ }
833
+ });
834
+ }
835
+ let schema = new GraphQLSchema(config);
836
+ // Apply SDL extensions
837
+ if (this._extensionSDL != null) {
838
+ const typesBefore = new Set(Object.keys(schema.getTypeMap()));
839
+ schema = extendSchema(schema, parseGraphQL(this._extensionSDL));
840
+ // Stub extension resolvers for client-side pre-execution
841
+ if (this._stubExtensionResolvers) {
842
+ this.#stubExtensionFields(schema, typesBefore);
843
+ }
844
+ }
845
+ // Attach resolvers to extended fields
846
+ if (this._extensionResolvers != null) {
847
+ this.#attachExtensionResolvers(schema, this._extensionResolvers);
848
+ }
849
+ return schema;
850
+ }
851
+ #attachExtensionResolvers(schema, resolvers) {
852
+ const queryType = schema.getQueryType();
853
+ if (queryType != null && resolvers.queryFields != null) {
854
+ const fields = queryType.getFields();
855
+ for (const [name, resolver] of Object.entries(resolvers.queryFields)){
856
+ if (fields[name] == null) {
857
+ throw new Error(`Extension resolver for query field "${name}" has no matching field in SDL`);
858
+ }
859
+ fields[name].resolve = resolver;
860
+ }
861
+ }
862
+ const mutationType = schema.getMutationType();
863
+ if (mutationType != null && resolvers.mutationFields != null) {
864
+ const fields = mutationType.getFields();
865
+ for (const [name, resolver] of Object.entries(resolvers.mutationFields)){
866
+ if (fields[name] == null) {
867
+ throw new Error(`Extension resolver for mutation field "${name}" has no matching field in SDL`);
868
+ }
869
+ fields[name].resolve = resolver;
870
+ }
871
+ }
872
+ const subscriptionType = schema.getSubscriptionType();
873
+ if (subscriptionType != null && resolvers.subscriptionFields != null) {
874
+ const fields = subscriptionType.getFields();
875
+ for (const [name, entry] of Object.entries(resolvers.subscriptionFields)){
876
+ if (fields[name] == null) {
877
+ throw new Error(`Extension resolver for subscription field "${name}" has no matching field in SDL`);
878
+ }
879
+ fields[name].resolve = entry.resolve;
880
+ fields[name].subscribe = entry.subscribe;
881
+ }
882
+ }
883
+ if (resolvers.typeFields != null) {
884
+ for (const [typeName, fieldResolvers] of Object.entries(resolvers.typeFields)){
885
+ const type = schema.getType(typeName);
886
+ if (type == null) {
887
+ throw new Error(`Extension resolver references type "${typeName}" which does not exist in the schema`);
888
+ }
889
+ if (!isObjectType(type)) {
890
+ throw new Error(`Extension resolver references type "${typeName}" which is not an object type`);
891
+ }
892
+ const fields = type.getFields();
893
+ for (const [fieldName, resolver] of Object.entries(fieldResolvers)){
894
+ if (fields[fieldName] == null) {
895
+ throw new Error(`Extension resolver for field "${fieldName}" on type "${typeName}" has no matching field in SDL`);
896
+ }
897
+ fields[fieldName].resolve = resolver;
898
+ }
899
+ }
900
+ }
901
+ if (resolvers.nodeResolvers != null) {
902
+ for (const [typeName, resolver] of Object.entries(resolvers.nodeResolvers)){
903
+ this._pluginNodeResolvers[typeName] = resolver;
904
+ }
905
+ }
906
+ }
907
+ #stubExtensionFields(schema, typesBefore) {
908
+ // Stub root Query/Mutation fields added by extension SDL
909
+ for (const rootType of [
910
+ schema.getQueryType(),
911
+ schema.getMutationType()
912
+ ]){
913
+ if (rootType == null) continue;
914
+ const fields = rootType.getFields();
915
+ for (const field of Object.values(fields)){
916
+ if (field.resolve == null) {
917
+ field.resolve = ()=>createDefaultValue(field.type);
918
+ }
919
+ }
920
+ }
921
+ // Stub fields on types added by extension SDL
922
+ for (const [typeName, type] of Object.entries(schema.getTypeMap())){
923
+ if (typesBefore.has(typeName)) continue;
924
+ if (!isObjectType(type)) continue;
925
+ const fields = type.getFields();
926
+ for (const field of Object.values(fields)){
927
+ if (field.resolve == null) {
928
+ const fieldType = field.type;
929
+ field.resolve = (source)=>source[field.name] ?? createDefaultValue(fieldType);
930
+ }
931
+ }
932
+ }
933
+ }
934
+ _getDefinitions() {
935
+ if (this._definitions == null) {
936
+ throw new Error('Definitions must be initialized');
937
+ }
938
+ return this._definitions;
939
+ }
940
+ _getReference(path, parentName) {
941
+ const id = getReferenceID(path);
942
+ const type = this._types[id];
943
+ if (type != null) {
944
+ return type;
945
+ }
946
+ const schema = this._getReferenceSchema(id);
947
+ return this._buildReference(id, schema, parentName);
948
+ }
949
+ _getReferenceSchema(id) {
950
+ const schema = this._references[id];
951
+ if (schema == null) {
952
+ throw new Error(`Could not find reference: ${id}`);
953
+ }
954
+ return schema;
955
+ }
956
+ _getReferenceInput(path, isPartial, parentName = '') {
957
+ const id = getReferenceID(path);
958
+ const schema = this._getReferenceSchema(id);
959
+ if (schema.type === 'array') {
960
+ const itemType = this._getReferenceInput(schema.items.$ref, false, schema.title);
961
+ return new GraphQLList(new GraphQLNonNull(itemType));
962
+ }
963
+ if (schema.type === 'object') {
964
+ if (schema.additionalProperties) {
965
+ return GraphQLJSONObject;
966
+ }
967
+ const inputID = isPartial ? `${id}-partial` : id;
968
+ const existing = this._inputObjects[inputID];
969
+ if (existing != null) {
970
+ return existing;
971
+ }
972
+ const fields = {};
973
+ for (const [key, value] of Object.entries(schema.properties)){
974
+ const type = this._getReferenceInput(value.$ref, isPartial, schema.title);
975
+ fields[key] = {
976
+ type: isPartial || !schema.required.includes(key) ? type : new GraphQLNonNull(type)
977
+ };
978
+ }
979
+ const name = this._aliases[id] ?? pascalCase(parentName + schema.title);
980
+ const input = new GraphQLInputObjectType({
981
+ name: isPartial ? `Partial${name}Input` : `${name}Input`,
982
+ fields
983
+ });
984
+ this._inputObjects[inputID] = input;
985
+ return input;
986
+ }
987
+ return this._buildScalar(id, schema, parentName);
988
+ }
989
+ _buildSharedDefinitions() {
990
+ const nodeDefs = nodeDefinitions(async (id, ctx)=>{
991
+ if (id.startsWith('did:')) {
992
+ return id;
993
+ }
994
+ const decoded = decodeID(id);
995
+ if (decoded instanceof AttachmentID) {
996
+ return decoded;
997
+ }
998
+ if (decoded instanceof DocumentID) {
999
+ return await ctx.loadDocument(id);
1000
+ }
1001
+ if (decoded instanceof PluginNodeID) {
1002
+ const resolver = this._pluginNodeResolvers[decoded.typeName];
1003
+ if (resolver == null) {
1004
+ throw new Error(`No node resolver registered for plugin type "${decoded.typeName}"`);
1005
+ }
1006
+ const result = await resolver.resolve(decoded.localID, ctx);
1007
+ if (result == null) {
1008
+ throw new Error(`Node resolver for "${decoded.typeName}" returned null for localID "${decoded.localID}"`);
1009
+ }
1010
+ return {
1011
+ ...result,
1012
+ __typename: decoded.typeName
1013
+ };
1014
+ }
1015
+ throw new Error(`Unsupported node ID: ${id}`);
1016
+ }, (node)=>{
1017
+ if (typeof node === 'string') return 'AccountNode';
1018
+ if (node instanceof AttachmentID) return 'AttachmentNode';
1019
+ if (node != null && typeof node === 'object' && 'model' in node) {
1020
+ return this._aliases[node.model];
1021
+ }
1022
+ if (node != null && typeof node === 'object' && '__typename' in node) {
1023
+ return node.__typename;
1024
+ }
1025
+ throw new Error('Unknown node type');
1026
+ });
1027
+ const accountObject = new GraphQLObjectType({
1028
+ name: 'AccountNode',
1029
+ interfaces: [
1030
+ nodeDefs.nodeInterface
1031
+ ],
1032
+ fields: ()=>{
1033
+ const config = {
1034
+ id: {
1035
+ type: new GraphQLNonNull(GraphQLID),
1036
+ description: 'Globally unique identifier of the account (DID string)',
1037
+ resolve: (did)=>did
1038
+ },
1039
+ isViewer: {
1040
+ type: new GraphQLNonNull(GraphQLBoolean),
1041
+ description: 'Whether the authenticated request issuer is this account or not',
1042
+ resolve: (did, _, ctx)=>ctx.getViewer() === did
1043
+ }
1044
+ };
1045
+ for (const [id, model] of Object.entries(this._record)){
1046
+ if (!this._includedModelIDs.has(id)) continue;
1047
+ switch(model.behavior){
1048
+ case 'interface':
1049
+ continue;
1050
+ case 'default':
1051
+ {
1052
+ const name = this._aliases[id];
1053
+ config[`own${name}Connection`] = {
1054
+ type: this._types[`${id}-connection`],
1055
+ args: {
1056
+ ...connectionArgs,
1057
+ filter: {
1058
+ type: this._inputObjects[`${id}-filter`]
1059
+ },
1060
+ orderBy: {
1061
+ type: toList(this._inputObjects[`${id}-order`])
1062
+ }
1063
+ },
1064
+ resolve: async (owner, args, ctx)=>{
1065
+ return await ctx.resolveConnection(this.getModelIDs(id), {
1066
+ ...args,
1067
+ owner
1068
+ });
1069
+ }
1070
+ };
1071
+ break;
1072
+ }
1073
+ case 'unique':
1074
+ {
1075
+ const name = this._aliases[id];
1076
+ config[`own${name}`] = {
1077
+ type: this._types[id],
1078
+ args: model.uniqueFields.length ? {
1079
+ with: {
1080
+ type: new GraphQLNonNull(this._buildSetInputObjectType(id, model.uniqueFields))
1081
+ }
1082
+ } : {},
1083
+ resolve: async (owner, args, ctx)=>{
1084
+ const unique = getUniqueDocValue(model.uniqueFields, args.with);
1085
+ const docID = DocumentID.create(id, owner, unique);
1086
+ return await ctx.loadDocument(docID.toString());
1087
+ }
1088
+ };
1089
+ }
1090
+ }
1091
+ }
1092
+ return config;
1093
+ }
1094
+ });
1095
+ const attachmentObject = new GraphQLObjectType({
1096
+ name: 'AttachmentNode',
1097
+ interfaces: [
1098
+ nodeDefs.nodeInterface
1099
+ ],
1100
+ fields: {
1101
+ id: {
1102
+ type: new GraphQLNonNull(GraphQLID),
1103
+ resolve: (aid)=>aid.toString()
1104
+ },
1105
+ contentLength: {
1106
+ type: new GraphQLNonNull(GraphQLInt)
1107
+ },
1108
+ mimeType: {
1109
+ type: new GraphQLNonNull(GraphQLString)
1110
+ }
1111
+ }
1112
+ });
1113
+ const documentNodeFields = {
1114
+ // Node interface
1115
+ id: {
1116
+ type: new GraphQLNonNull(GraphQLID)
1117
+ },
1118
+ // Metadata
1119
+ model: {
1120
+ type: new GraphQLNonNull(GraphQLString)
1121
+ },
1122
+ owner: {
1123
+ type: new GraphQLNonNull(accountObject)
1124
+ },
1125
+ createdAt: {
1126
+ type: new GraphQLNonNull(GraphQLDateTimeISO)
1127
+ },
1128
+ updatedAt: {
1129
+ type: GraphQLDateTimeISO
1130
+ },
1131
+ // Access control
1132
+ accessPermissions: {
1133
+ type: AccessPermissions,
1134
+ resolve: (doc)=>doc.data?.accessPermissions ?? null
1135
+ }
1136
+ };
1137
+ const documentInterface = new GraphQLInterfaceType({
1138
+ name: 'DocumentNode',
1139
+ interfaces: [
1140
+ nodeDefs.nodeInterface
1141
+ ],
1142
+ fields: documentNodeFields,
1143
+ resolveType: (doc)=>this._aliases[doc.model]
1144
+ });
1145
+ const queryFields = {
1146
+ node: nodeDefs.nodeField,
1147
+ viewer: {
1148
+ type: accountObject,
1149
+ description: 'Account issuing the request, if authenticated',
1150
+ resolve: (_self, _args, ctx)=>ctx.getViewer()
1151
+ }
1152
+ };
1153
+ return {
1154
+ ...nodeDefs,
1155
+ accountObject,
1156
+ attachmentObject,
1157
+ documentInterface,
1158
+ documentNodeFields,
1159
+ queryFields
1160
+ };
1161
+ }
1162
+ _buildDocument(id) {
1163
+ if (this._types[id] != null) {
1164
+ return this._types[id];
1165
+ }
1166
+ const model = this._record[id];
1167
+ if (model == null) {
1168
+ throw new Error(`Could not find model id: ${id}`);
1169
+ }
1170
+ const definitions = this._getDefinitions();
1171
+ this._aliases[id] ??= pascalCase(model.name);
1172
+ const name = this._aliases[id];
1173
+ const config = {
1174
+ name,
1175
+ interfaces: ()=>{
1176
+ return [
1177
+ definitions.documentInterface,
1178
+ definitions.nodeInterface
1179
+ ].concat(model.interfaces.map((interfaceID)=>{
1180
+ return this._types[getGlobalID(interfaceID, id).toString()];
1181
+ }).filter((type)=>type != null));
1182
+ },
1183
+ fields: ()=>this._buildDocumentFields(id, model, name)
1184
+ };
1185
+ const isInterface = model.behavior === 'interface';
1186
+ const nodeType = isInterface ? new GraphQLInterfaceType({
1187
+ ...config,
1188
+ resolveType: (doc)=>this._aliases[doc.model]
1189
+ }) : new GraphQLObjectType(config);
1190
+ const aggregateType = this._buildAggregate(id, model, name);
1191
+ const groupedAggregateType = this._buildGroupedAggregate(id, model, name);
1192
+ const { connectionType, edgeType } = connectionDefinitions({
1193
+ nodeType,
1194
+ connectionFields: ()=>{
1195
+ const fields = {
1196
+ aggregates: {
1197
+ type: aggregateType,
1198
+ // The connection carries the query scope so aggregation runs against
1199
+ // the same documents the page was built from, not the current page.
1200
+ // A look-ahead on the selection set turns the requested ops/fields into
1201
+ // store specs, so only what was selected is computed.
1202
+ resolve: async (source, _args, ctx, info)=>{
1203
+ const scope = source.aggregateScope;
1204
+ const { count, aggregates } = this._buildAggregateRequest(id, info);
1205
+ return await ctx.resolveAggregate(scope.modelIDs, {
1206
+ filter: scope.filter,
1207
+ owner: scope.owner,
1208
+ catalogID: scope.catalogID,
1209
+ count,
1210
+ aggregates
1211
+ });
1212
+ }
1213
+ }
1214
+ };
1215
+ // Grouped aggregates only exist for models with at least one groupable scalar
1216
+ // field (see _buildGroupedAggregate). Skip the field entirely otherwise so the
1217
+ // schema does not reference empty GroupKeys/GroupBy/GroupKeysOrderBy types.
1218
+ if (groupedAggregateType != null) {
1219
+ fields.groupedAggregates = {
1220
+ type: new GraphQLNonNull(toOuputList(groupedAggregateType)),
1221
+ args: {
1222
+ groupBy: {
1223
+ type: new GraphQLNonNull(toList(this._inputObjects[`${id}-group-by`]))
1224
+ },
1225
+ having: {
1226
+ type: this._inputObjects[`${id}-having`]
1227
+ },
1228
+ orderBy: {
1229
+ type: toList(this._inputObjects[`${id}-group-order`])
1230
+ },
1231
+ limit: {
1232
+ type: GraphQLInt
1233
+ }
1234
+ },
1235
+ // Translates the field arguments and the selection-set look-ahead into a
1236
+ // single grouped store request, scoped to the same documents the page was
1237
+ // built from, then returns the merged groups for the child resolvers to map.
1238
+ resolve: async (source, args, ctx, info)=>{
1239
+ const scope = source.aggregateScope;
1240
+ const { count, aggregates } = this._buildGroupedAggregateRequest(id, info);
1241
+ const groupBy = (args.groupBy ?? []).map((field)=>({
1242
+ keys: [
1243
+ field
1244
+ ]
1245
+ }));
1246
+ const orderBy = this._buildGroupOrderBy(id, args.orderBy);
1247
+ return await ctx.resolveGroupedAggregate(scope.modelIDs, {
1248
+ filter: scope.filter,
1249
+ owner: scope.owner,
1250
+ catalogID: scope.catalogID,
1251
+ count,
1252
+ aggregates,
1253
+ groupBy,
1254
+ having: args.having,
1255
+ orderBy: orderBy.length > 0 ? orderBy : undefined,
1256
+ limit: args.limit ?? undefined
1257
+ });
1258
+ }
1259
+ };
1260
+ }
1261
+ return fields;
1262
+ }
1263
+ });
1264
+ this._types[id] = nodeType;
1265
+ this._types[`${id}-connection`] = connectionType;
1266
+ this._types[`${id}-edge`] = edgeType;
1267
+ this._buildDocumentDataObject(id, model, name);
1268
+ this._buildDocumentInput(id, model);
1269
+ this._buildObjectFilterInput(id, model.schema, name, true);
1270
+ this._buildObjectOrderByInput(id, model.schema, name, true);
1271
+ // TODO: move to dedicated method
1272
+ if (!isInterface && this._includeMutations) {
1273
+ if (model.behavior === 'default') {
1274
+ this._mutations[`create${name}`] = mutationWithClientMutationId({
1275
+ name: `Create${name}`,
1276
+ inputFields: ()=>({
1277
+ data: {
1278
+ type: new GraphQLNonNull(this._inputObjects[id])
1279
+ },
1280
+ transactionID: {
1281
+ type: GraphQLID
1282
+ }
1283
+ }),
1284
+ outputFields: ()=>({
1285
+ ...definitions.queryFields,
1286
+ document: {
1287
+ type: this._types[id]
1288
+ }
1289
+ }),
1290
+ mutateAndGetPayload: async (input, ctx, info)=>{
1291
+ const document = await ctx.executeCreateMutation({
1292
+ modelID: id,
1293
+ data: input.data,
1294
+ info,
1295
+ transactionID: input.transactionID
1296
+ });
1297
+ return {
1298
+ document
1299
+ };
1300
+ }
1301
+ });
1302
+ } else if (model.behavior === 'unique') {
1303
+ this._mutations[`set${name}`] = mutationWithClientMutationId({
1304
+ name: `Set${name}`,
1305
+ inputFields: ()=>({
1306
+ data: {
1307
+ type: new GraphQLNonNull(this._inputObjects[id])
1308
+ },
1309
+ transactionID: {
1310
+ type: GraphQLID
1311
+ }
1312
+ }),
1313
+ outputFields: ()=>({
1314
+ ...definitions.queryFields,
1315
+ document: {
1316
+ type: this._types[id]
1317
+ }
1318
+ }),
1319
+ mutateAndGetPayload: async (input, ctx, info)=>{
1320
+ const unique = getUniqueDocValue(model.uniqueFields, input.data);
1321
+ const document = await ctx.executeSetMutation({
1322
+ modelID: id,
1323
+ unique,
1324
+ data: input.data,
1325
+ info,
1326
+ transactionID: input.transactionID
1327
+ });
1328
+ return {
1329
+ document
1330
+ };
1331
+ }
1332
+ });
1333
+ }
1334
+ this._mutations[`update${name}`] = mutationWithClientMutationId({
1335
+ name: `Update${name}`,
1336
+ inputFields: ()=>({
1337
+ id: {
1338
+ type: new GraphQLNonNull(GraphQLID)
1339
+ },
1340
+ patch: {
1341
+ type: new GraphQLNonNull(toList(PatchOperation))
1342
+ },
1343
+ from: {
1344
+ type: this._inputObjects[`${id}-update`]
1345
+ },
1346
+ transactionID: {
1347
+ type: GraphQLID
1348
+ }
1349
+ }),
1350
+ outputFields: ()=>({
1351
+ ...definitions.queryFields,
1352
+ document: {
1353
+ type: this._types[id]
1354
+ }
1355
+ }),
1356
+ mutateAndGetPayload: async (input, ctx, info)=>{
1357
+ const document = await ctx.executeUpdateMutation({
1358
+ input,
1359
+ info,
1360
+ transactionID: input.transactionID
1361
+ });
1362
+ return {
1363
+ document
1364
+ };
1365
+ }
1366
+ });
1367
+ this._mutations[`remove${name}`] = mutationWithClientMutationId({
1368
+ name: `Remove${name}`,
1369
+ inputFields: ()=>({
1370
+ id: {
1371
+ type: new GraphQLNonNull(GraphQLID)
1372
+ },
1373
+ transactionID: {
1374
+ type: GraphQLID
1375
+ }
1376
+ }),
1377
+ outputFields: ()=>({
1378
+ ...definitions.queryFields,
1379
+ // Return id so it can be used with Relay's @deleteRecord directive
1380
+ id: {
1381
+ type: new GraphQLNonNull(GraphQLID)
1382
+ }
1383
+ }),
1384
+ mutateAndGetPayload: async (input, ctx, info)=>{
1385
+ await ctx.executeRemoveMutation({
1386
+ id: input.id,
1387
+ info,
1388
+ transactionID: input.transactionID
1389
+ });
1390
+ return {
1391
+ id: input.id
1392
+ };
1393
+ }
1394
+ });
1395
+ }
1396
+ return nodeType;
1397
+ }
1398
+ /**
1399
+ * Build the `<Model>Aggregate` type attached to the connection, exposing `count`
1400
+ * plus per-field `sum`/`average`/`min`/`max` selections gated by field type.
1401
+ *
1402
+ * Eligibility follows what SQL can faithfully compute over JSON-extracted values:
1403
+ * - sum/average: numeric fields only (integer, number).
1404
+ * - min/max: numeric fields plus string/date fields (lexical for strings).
1405
+ * Field-selection subtypes are only emitted when at least one field qualifies,
1406
+ * since an object type with no fields is invalid GraphQL. `count` is always present.
1407
+ */ _buildAggregate(id, model, name) {
1408
+ const existing = this._types[`${id}-aggregate`];
1409
+ if (existing != null) {
1410
+ return existing;
1411
+ }
1412
+ // Resolve each property to its underlying scalar so we can classify it. Only
1413
+ // direct scalar references are eligible; object/array fields are skipped.
1414
+ const sumFields = {};
1415
+ const minMaxFields = {};
1416
+ // Field key -> numeric, consumed by the look-ahead resolver to tag each spec.
1417
+ const numericByKey = new Map();
1418
+ for (const [key, value] of Object.entries(model.schema.properties ?? {})){
1419
+ const ref = value.$ref;
1420
+ if (ref == null) continue;
1421
+ const refID = getReferenceID(ref);
1422
+ const refSchema = this._getReferenceSchema(refID);
1423
+ if (refSchema.type === 'object' || refSchema.type === 'array') continue;
1424
+ const scalar = this._buildScalar(refID, refSchema);
1425
+ const isNumeric = scalar === GraphQLInt || scalar === GraphQLFloat;
1426
+ // Only plain-string formats (date / time / date-time, which map to GraphQLString)
1427
+ // are min/max-eligible; custom-scalar formats (duration / email / uri) and enums
1428
+ // are intentionally excluded from min/max.
1429
+ const isStringLike = scalar === GraphQLString;
1430
+ if (isNumeric) {
1431
+ // sum keeps the field's own numeric type (Int stays Int, number is Float).
1432
+ sumFields[key] = {
1433
+ type: scalar
1434
+ };
1435
+ }
1436
+ if (isNumeric || isStringLike) {
1437
+ minMaxFields[key] = {
1438
+ type: scalar
1439
+ };
1440
+ numericByKey.set(key, isNumeric);
1441
+ }
1442
+ }
1443
+ this._aggregateNumericByModel[id] = numericByKey;
1444
+ const numericKeys = Object.keys(sumFields);
1445
+ // The aggregates connection resolver returns an AggregateValues; each op
1446
+ // reads its slice from that source. Defaulting count to 0 keeps the non-null
1447
+ // contract when a count was not requested by the look-ahead.
1448
+ const aggregateFields = {
1449
+ count: {
1450
+ type: new GraphQLNonNull(GraphQLInt),
1451
+ resolve: (source)=>source.count ?? 0
1452
+ }
1453
+ };
1454
+ if (numericKeys.length > 0) {
1455
+ const sumType = new GraphQLObjectType({
1456
+ name: `${name}SumFields`,
1457
+ fields: sumFields
1458
+ });
1459
+ // Average of integers is fractional, so every average field is Float.
1460
+ const avgFields = {};
1461
+ for (const key of numericKeys){
1462
+ avgFields[key] = {
1463
+ type: GraphQLFloat
1464
+ };
1465
+ }
1466
+ const avgType = new GraphQLObjectType({
1467
+ name: `${name}AvgFields`,
1468
+ fields: avgFields
1469
+ });
1470
+ this._types[`${id}-aggregate-sum`] = sumType;
1471
+ this._types[`${id}-aggregate-avg`] = avgType;
1472
+ // The *Fields child reads each field key off the returned Record via the
1473
+ // default field resolver (e.g. SumFields.delta -> source.delta).
1474
+ aggregateFields.sum = {
1475
+ type: sumType,
1476
+ resolve: (source)=>source.sum ?? null
1477
+ };
1478
+ aggregateFields.average = {
1479
+ type: avgType,
1480
+ resolve: (source)=>source.average ?? null
1481
+ };
1482
+ }
1483
+ if (Object.keys(minMaxFields).length > 0) {
1484
+ // min and max expose the identical field set, so they share one object type.
1485
+ const minMaxType = new GraphQLObjectType({
1486
+ name: `${name}MinMaxFields`,
1487
+ fields: minMaxFields
1488
+ });
1489
+ this._types[`${id}-aggregate-min-max`] = minMaxType;
1490
+ aggregateFields.min = {
1491
+ type: minMaxType,
1492
+ resolve: (source)=>source.min ?? null
1493
+ };
1494
+ aggregateFields.max = {
1495
+ type: minMaxType,
1496
+ resolve: (source)=>source.max ?? null
1497
+ };
1498
+ }
1499
+ const aggregateType = new GraphQLObjectType({
1500
+ name: `${name}Aggregate`,
1501
+ fields: aggregateFields
1502
+ });
1503
+ this._types[`${id}-aggregate`] = aggregateType;
1504
+ return aggregateType;
1505
+ }
1506
+ /**
1507
+ * Build the grouped-aggregate type surface for a model, reusing the v1
1508
+ * `<Model>SumFields` / `AvgFields` / `MinMaxFields` types from
1509
+ * `_buildAggregate` and the existing value-filter / `OrderByDirection` inputs.
1510
+ *
1511
+ * Groupable fields are every property resolving to a scalar (object/array fields
1512
+ * excluded), which is broader than aggregate eligibility: boolean and enum fields
1513
+ * can be grouped on but never summed. The sum/average and min/max sub-shapes are
1514
+ * only emitted when the model actually has those eligible fields, matching the
1515
+ * conditionals in `_buildAggregate`, since an object type with no fields is invalid.
1516
+ *
1517
+ * Produces, registered in `_types` / `_inputObjects`:
1518
+ * `<Model>GroupBy` enum, `<Model>GroupKeys`, `<Model>GroupedAggregate`,
1519
+ * `<Model>Having` (+ `HavingSum`/`HavingAverage`/`HavingMin`/`HavingMax`),
1520
+ * `<Model>GroupOrderBy` (+ `GroupKeysOrderBy` and per-op `<Op>FieldsOrderBy`).
1521
+ */ _buildGroupedAggregate(id, model, name) {
1522
+ const existing = this._types[`${id}-grouped-aggregate`];
1523
+ if (existing != null) {
1524
+ return existing;
1525
+ }
1526
+ // Every scalar property is groupable (object/array excluded), so this set is a
1527
+ // superset of the aggregate-eligible fields, additionally covering boolean/enum.
1528
+ const groupableTypes = new Map();
1529
+ const groupKeyNumeric = new Map();
1530
+ for (const [key, value] of Object.entries(model.schema.properties ?? {})){
1531
+ const ref = value.$ref;
1532
+ if (ref == null) continue;
1533
+ const refID = getReferenceID(ref);
1534
+ const refSchema = this._getReferenceSchema(refID);
1535
+ if (refSchema.type === 'object' || refSchema.type === 'array') continue;
1536
+ const scalar = this._buildScalar(refID, refSchema);
1537
+ groupableTypes.set(key, scalar);
1538
+ groupKeyNumeric.set(key, scalar === GraphQLInt || scalar === GraphQLFloat);
1539
+ }
1540
+ this._groupKeyNumericByModel[id] = groupKeyNumeric;
1541
+ // GraphQL forbids empty object/input/enum types, so skip the whole grouped-aggregate
1542
+ // surface (GroupKeys, GroupBy, GroupKeysOrderBy, GroupedAggregate, Having, GroupOrderBy)
1543
+ // when no groupable scalar field exists — typical for tag-style models whose only
1544
+ // properties are document references.
1545
+ if (groupableTypes.size === 0) {
1546
+ return null;
1547
+ }
1548
+ const sumType = this._types[`${id}-aggregate-sum`];
1549
+ const avgType = this._types[`${id}-aggregate-avg`];
1550
+ const minMaxType = this._types[`${id}-aggregate-min-max`];
1551
+ // GroupBy enum: one value per groupable field key.
1552
+ const groupByValues = {};
1553
+ for (const key of groupableTypes.keys()){
1554
+ groupByValues[key] = {
1555
+ value: key
1556
+ };
1557
+ }
1558
+ const groupByEnum = new GraphQLEnumType({
1559
+ name: `${name}GroupBy`,
1560
+ values: groupByValues
1561
+ });
1562
+ this._types[`${id}-group-by`] = groupByEnum;
1563
+ this._inputObjects[`${id}-group-by`] = groupByEnum;
1564
+ // GroupKeys: every groupable field, each nullable since a group may bucket
1565
+ // documents missing the field into a null key.
1566
+ const groupKeysFields = {};
1567
+ for (const [key, scalar] of groupableTypes){
1568
+ groupKeysFields[key] = {
1569
+ type: scalar
1570
+ };
1571
+ }
1572
+ const groupKeysType = new GraphQLObjectType({
1573
+ name: `${name}GroupKeys`,
1574
+ fields: groupKeysFields
1575
+ });
1576
+ this._types[`${id}-group-keys`] = groupKeysType;
1577
+ // GroupedAggregate: the per-group keys plus count, reusing the v1 aggregate field
1578
+ // types for sum/average/min/max under the same conditionals as `_buildAggregate`.
1579
+ // Each op resolves its slice off the returned group; the *Fields / GroupKeys
1580
+ // leaves keep the default resolver. Defaulting count to 0 and field ops to null
1581
+ // mirrors `_buildAggregate` so the non-null contracts hold on missing slices.
1582
+ const groupedFields = {
1583
+ keys: {
1584
+ type: new GraphQLNonNull(groupKeysType),
1585
+ resolve: (group)=>group.keys
1586
+ },
1587
+ count: {
1588
+ type: new GraphQLNonNull(GraphQLInt),
1589
+ resolve: (group)=>group.count ?? 0
1590
+ }
1591
+ };
1592
+ if (sumType != null && avgType != null) {
1593
+ groupedFields.sum = {
1594
+ type: sumType,
1595
+ resolve: (group)=>group.sum ?? null
1596
+ };
1597
+ groupedFields.average = {
1598
+ type: avgType,
1599
+ resolve: (group)=>group.average ?? null
1600
+ };
1601
+ }
1602
+ if (minMaxType != null) {
1603
+ groupedFields.min = {
1604
+ type: minMaxType,
1605
+ resolve: (group)=>group.min ?? null
1606
+ };
1607
+ groupedFields.max = {
1608
+ type: minMaxType,
1609
+ resolve: (group)=>group.max ?? null
1610
+ };
1611
+ }
1612
+ const groupedAggregateType = new GraphQLObjectType({
1613
+ name: `${name}GroupedAggregate`,
1614
+ fields: groupedFields
1615
+ });
1616
+ this._types[`${id}-grouped-aggregate`] = groupedAggregateType;
1617
+ // Map a scalar to its existing value-filter input by name (IntValueFilter etc.).
1618
+ const valueFilterFor = (scalar)=>{
1619
+ return this._inputObjects[`${scalar.name}ValueFilter`];
1620
+ };
1621
+ // Having: each op maps its eligible fields to the value filter matching the op's
1622
+ // scalar. Sum keeps each numeric field's own scalar; average is always Float;
1623
+ // min/max keep each field's own scalar (Int / Float / String).
1624
+ const havingFields = {
1625
+ count: {
1626
+ type: this._inputObjects.IntValueFilter
1627
+ }
1628
+ };
1629
+ if (sumType != null && avgType != null) {
1630
+ const havingSumFields = {};
1631
+ const havingAvgFields = {};
1632
+ for (const [key, field] of Object.entries(sumType.getFields())){
1633
+ havingSumFields[key] = {
1634
+ type: valueFilterFor(field.type)
1635
+ };
1636
+ havingAvgFields[key] = {
1637
+ type: this._inputObjects.FloatValueFilter
1638
+ };
1639
+ }
1640
+ const havingSum = new GraphQLInputObjectType({
1641
+ name: `${name}HavingSum`,
1642
+ fields: havingSumFields
1643
+ });
1644
+ const havingAvg = new GraphQLInputObjectType({
1645
+ name: `${name}HavingAverage`,
1646
+ fields: havingAvgFields
1647
+ });
1648
+ this._inputObjects[`${id}-having-sum`] = havingSum;
1649
+ this._inputObjects[`${id}-having-average`] = havingAvg;
1650
+ havingFields.sum = {
1651
+ type: havingSum
1652
+ };
1653
+ havingFields.average = {
1654
+ type: havingAvg
1655
+ };
1656
+ }
1657
+ if (minMaxType != null) {
1658
+ const havingMinFields = {};
1659
+ const havingMaxFields = {};
1660
+ for (const [key, field] of Object.entries(minMaxType.getFields())){
1661
+ havingMinFields[key] = {
1662
+ type: valueFilterFor(field.type)
1663
+ };
1664
+ havingMaxFields[key] = {
1665
+ type: valueFilterFor(field.type)
1666
+ };
1667
+ }
1668
+ const havingMin = new GraphQLInputObjectType({
1669
+ name: `${name}HavingMin`,
1670
+ fields: havingMinFields
1671
+ });
1672
+ const havingMax = new GraphQLInputObjectType({
1673
+ name: `${name}HavingMax`,
1674
+ fields: havingMaxFields
1675
+ });
1676
+ this._inputObjects[`${id}-having-min`] = havingMin;
1677
+ this._inputObjects[`${id}-having-max`] = havingMax;
1678
+ havingFields.min = {
1679
+ type: havingMin
1680
+ };
1681
+ havingFields.max = {
1682
+ type: havingMax
1683
+ };
1684
+ }
1685
+ const havingInput = new GraphQLInputObjectType({
1686
+ name: `${name}Having`,
1687
+ fields: havingFields
1688
+ });
1689
+ this._inputObjects[`${id}-having`] = havingInput;
1690
+ // GroupOrderBy: every leaf orders by the shared OrderByDirection enum. The keys
1691
+ // sub-input covers all groupable fields; each op sub-input mirrors that op's
1692
+ // aggregate field map, emitted only when the op's fields exist.
1693
+ const buildDirectionInput = (subName, keys)=>{
1694
+ const fields = {};
1695
+ for (const key of keys){
1696
+ fields[key] = {
1697
+ type: OrderByDirection
1698
+ };
1699
+ }
1700
+ return new GraphQLInputObjectType({
1701
+ name: subName,
1702
+ fields
1703
+ });
1704
+ };
1705
+ const groupKeysOrderBy = buildDirectionInput(`${name}GroupKeysOrderBy`, groupableTypes.keys());
1706
+ this._inputObjects[`${id}-group-keys-order`] = groupKeysOrderBy;
1707
+ const groupOrderByFields = {
1708
+ keys: {
1709
+ type: groupKeysOrderBy
1710
+ },
1711
+ count: {
1712
+ type: OrderByDirection
1713
+ }
1714
+ };
1715
+ if (sumType != null && avgType != null) {
1716
+ const sumOrderBy = buildDirectionInput(`${name}SumFieldsOrderBy`, Object.keys(sumType.getFields()));
1717
+ const avgOrderBy = buildDirectionInput(`${name}AvgFieldsOrderBy`, Object.keys(avgType.getFields()));
1718
+ this._inputObjects[`${id}-sum-order`] = sumOrderBy;
1719
+ this._inputObjects[`${id}-avg-order`] = avgOrderBy;
1720
+ groupOrderByFields.sum = {
1721
+ type: sumOrderBy
1722
+ };
1723
+ groupOrderByFields.average = {
1724
+ type: avgOrderBy
1725
+ };
1726
+ }
1727
+ if (minMaxType != null) {
1728
+ const minOrderBy = buildDirectionInput(`${name}MinFieldsOrderBy`, Object.keys(minMaxType.getFields()));
1729
+ const maxOrderBy = buildDirectionInput(`${name}MaxFieldsOrderBy`, Object.keys(minMaxType.getFields()));
1730
+ this._inputObjects[`${id}-min-order`] = minOrderBy;
1731
+ this._inputObjects[`${id}-max-order`] = maxOrderBy;
1732
+ groupOrderByFields.min = {
1733
+ type: minOrderBy
1734
+ };
1735
+ groupOrderByFields.max = {
1736
+ type: maxOrderBy
1737
+ };
1738
+ }
1739
+ const groupOrderBy = new GraphQLInputObjectType({
1740
+ name: `${name}GroupOrderBy`,
1741
+ fields: groupOrderByFields
1742
+ });
1743
+ this._inputObjects[`${id}-group-order`] = groupOrderBy;
1744
+ return groupedAggregateType;
1745
+ }
1746
+ /**
1747
+ * Inspect the selection set under the `aggregates` field to determine exactly
1748
+ * which aggregation work to ask the store for: whether a count was selected,
1749
+ * and for each of sum/average/min/max, which field subselections were
1750
+ * requested. Field aggregates not selected are never computed, so a
1751
+ * `{ count }`-only query issues `aggregates: []`.
1752
+ *
1753
+ * Reads the resolved field nodes via `info.fieldNodes`. Selections reached
1754
+ * through fragment spreads and inline fragments are expanded transparently, so
1755
+ * `aggregates { ...AggFields }`, `aggregates { ... { count } }` and
1756
+ * `sum { ...SumFields }` all request the same work as their inlined forms.
1757
+ */ _buildAggregateRequest(id, info) {
1758
+ const numericByKey = this._aggregateNumericByModel[id] ?? new Map();
1759
+ const ops = [
1760
+ 'sum',
1761
+ 'average',
1762
+ 'min',
1763
+ 'max'
1764
+ ];
1765
+ let count = false;
1766
+ const aggregates = [];
1767
+ for (const fieldNode of info.fieldNodes){
1768
+ const selections = fieldNode.selectionSet?.selections ?? [];
1769
+ for (const selection of this._expandFieldSelections(selections, info, new Set())){
1770
+ const fieldName = selection.name.value;
1771
+ if (fieldName === 'count') {
1772
+ count = true;
1773
+ continue;
1774
+ }
1775
+ if (!ops.includes(fieldName)) continue;
1776
+ const op = fieldName;
1777
+ const opSelections = selection.selectionSet?.selections ?? [];
1778
+ for (const opSelection of this._expandFieldSelections(opSelections, info, new Set())){
1779
+ const key = opSelection.name.value;
1780
+ // For a top-level scalar field the JSON path is just the field key.
1781
+ aggregates.push({
1782
+ op,
1783
+ keys: [
1784
+ key
1785
+ ],
1786
+ numeric: numericByKey.get(key) ?? false
1787
+ });
1788
+ }
1789
+ }
1790
+ }
1791
+ return {
1792
+ count,
1793
+ aggregates
1794
+ };
1795
+ }
1796
+ /**
1797
+ * Inspect the selection set under the `groupedAggregates` field to determine the
1798
+ * aggregation work to ask the store for. The aggregate ops (`count`/`sum`/
1799
+ * `average`/`min`/`max`) are nested directly under the field alongside `keys`,
1800
+ * which is skipped here since it is shaped by the `groupBy` argument, not the
1801
+ * selection. Fragment spreads and inline fragments are expanded transparently.
1802
+ * Selecting only `keys` and `count` yields `aggregates: []`.
1803
+ */ _buildGroupedAggregateRequest(id, info) {
1804
+ const numericByKey = this._aggregateNumericByModel[id] ?? new Map();
1805
+ const ops = [
1806
+ 'sum',
1807
+ 'average',
1808
+ 'min',
1809
+ 'max'
1810
+ ];
1811
+ let count = false;
1812
+ const aggregates = [];
1813
+ for (const fieldNode of info.fieldNodes){
1814
+ const selections = fieldNode.selectionSet?.selections ?? [];
1815
+ for (const selection of this._expandFieldSelections(selections, info, new Set())){
1816
+ const fieldName = selection.name.value;
1817
+ if (fieldName === 'count') {
1818
+ count = true;
1819
+ continue;
1820
+ }
1821
+ // `keys` is driven by the groupBy argument, not the aggregate look-ahead.
1822
+ if (fieldName === 'keys') continue;
1823
+ if (!ops.includes(fieldName)) continue;
1824
+ const op = fieldName;
1825
+ const opSelections = selection.selectionSet?.selections ?? [];
1826
+ for (const opSelection of this._expandFieldSelections(opSelections, info, new Set())){
1827
+ const key = opSelection.name.value;
1828
+ aggregates.push({
1829
+ op,
1830
+ keys: [
1831
+ key
1832
+ ],
1833
+ numeric: numericByKey.get(key) ?? false
1834
+ });
1835
+ }
1836
+ }
1837
+ }
1838
+ return {
1839
+ count,
1840
+ aggregates
1841
+ };
1842
+ }
1843
+ /**
1844
+ * Translate the `orderBy` argument entries into store ordering terms over each
1845
+ * finalised group. Each entry's present leaves become a term whose `path`
1846
+ * navigates the group object (`['count']`, `['keys', field]`, `['sum', field]`,
1847
+ * etc.) and whose `numeric` flag drives arithmetic comparison. List order is
1848
+ * preserved so earlier terms take precedence and later terms break ties.
1849
+ */ _buildGroupOrderBy(id, orderBy) {
1850
+ if (orderBy == null) return [];
1851
+ const aggregateNumeric = this._aggregateNumericByModel[id] ?? new Map();
1852
+ const groupKeyNumeric = this._groupKeyNumericByModel[id] ?? new Map();
1853
+ const terms = [];
1854
+ for (const entry of orderBy){
1855
+ if (entry.count != null) {
1856
+ terms.push({
1857
+ path: [
1858
+ 'count'
1859
+ ],
1860
+ direction: entry.count,
1861
+ numeric: true
1862
+ });
1863
+ }
1864
+ for (const [field, direction] of Object.entries(entry.keys ?? {})){
1865
+ terms.push({
1866
+ path: [
1867
+ 'keys',
1868
+ field
1869
+ ],
1870
+ direction,
1871
+ numeric: groupKeyNumeric.get(field) ?? false
1872
+ });
1873
+ }
1874
+ for (const [field, direction] of Object.entries(entry.sum ?? {})){
1875
+ terms.push({
1876
+ path: [
1877
+ 'sum',
1878
+ field
1879
+ ],
1880
+ direction,
1881
+ numeric: aggregateNumeric.get(field) ?? false
1882
+ });
1883
+ }
1884
+ for (const [field, direction] of Object.entries(entry.average ?? {})){
1885
+ // Averages are always fractional, so they always compare numerically.
1886
+ terms.push({
1887
+ path: [
1888
+ 'average',
1889
+ field
1890
+ ],
1891
+ direction,
1892
+ numeric: true
1893
+ });
1894
+ }
1895
+ for (const [field, direction] of Object.entries(entry.min ?? {})){
1896
+ terms.push({
1897
+ path: [
1898
+ 'min',
1899
+ field
1900
+ ],
1901
+ direction,
1902
+ numeric: aggregateNumeric.get(field) ?? false
1903
+ });
1904
+ }
1905
+ for (const [field, direction] of Object.entries(entry.max ?? {})){
1906
+ terms.push({
1907
+ path: [
1908
+ 'max',
1909
+ field
1910
+ ],
1911
+ direction,
1912
+ numeric: aggregateNumeric.get(field) ?? false
1913
+ });
1914
+ }
1915
+ }
1916
+ return terms;
1917
+ }
1918
+ /**
1919
+ * Walk a selection set and yield the underlying field nodes, expanding
1920
+ * fragment spreads and inline fragments in place. The `visited` set tracks
1921
+ * named fragments already entered so cyclic fragment references terminate.
1922
+ */ *_expandFieldSelections(selections, info, visited) {
1923
+ for (const selection of selections){
1924
+ switch(selection.kind){
1925
+ case Kind.FIELD:
1926
+ yield selection;
1927
+ break;
1928
+ case Kind.INLINE_FRAGMENT:
1929
+ yield* this._expandFieldSelections(selection.selectionSet.selections, info, visited);
1930
+ break;
1931
+ case Kind.FRAGMENT_SPREAD:
1932
+ {
1933
+ const fragmentName = selection.name.value;
1934
+ if (visited.has(fragmentName)) break;
1935
+ const fragment = info.fragments[fragmentName];
1936
+ if (fragment == null) break;
1937
+ visited.add(fragmentName);
1938
+ yield* this._expandFieldSelections(fragment.selectionSet.selections, info, visited);
1939
+ break;
1940
+ }
1941
+ }
1942
+ }
1943
+ }
1944
+ _buildDocumentFields(id, model, name) {
1945
+ const dataObject = this._buildDocumentDataObject(id, model, name);
1946
+ const definitions = this._getDefinitions();
1947
+ const fields = {
1948
+ ...definitions.documentNodeFields,
1949
+ data: {
1950
+ type: new GraphQLNonNull(dataObject)
1951
+ }
1952
+ };
1953
+ for (const [key, field] of Object.entries(dataObject.getFields())){
1954
+ const meta = model.fieldsMeta[key];
1955
+ if (meta == null) {
1956
+ continue;
1957
+ }
1958
+ switch(meta.type){
1959
+ case 'account':
1960
+ {
1961
+ if (isList(field.type)) {
1962
+ fields[`${key}Accounts`] = {
1963
+ type: new GraphQLNonNull(toOuputList(definitions.accountObject)),
1964
+ resolve: (doc)=>doc.data?.[key] ?? []
1965
+ };
1966
+ } else {
1967
+ fields[`${key}Account`] = {
1968
+ type: definitions.accountObject,
1969
+ resolve: (doc)=>doc.data?.[key] ?? null
1970
+ };
1971
+ }
1972
+ break;
1973
+ }
1974
+ case 'attachment':
1975
+ {
1976
+ if (isList(field.type)) {
1977
+ fields[`${key}Attachments`] = {
1978
+ type: new GraphQLNonNull(toOuputList(definitions.attachmentObject)),
1979
+ resolve: (doc)=>{
1980
+ const ids = doc.data?.[key] ?? [];
1981
+ return ids.map(AttachmentID.fromString);
1982
+ }
1983
+ };
1984
+ } else {
1985
+ fields[`${key}Attachment`] = {
1986
+ type: definitions.attachmentObject,
1987
+ resolve: (doc)=>{
1988
+ const id = doc.data?.[key];
1989
+ return id ? AttachmentID.fromString(id) : null;
1990
+ }
1991
+ };
1992
+ }
1993
+ break;
1994
+ }
1995
+ case 'document':
1996
+ {
1997
+ const relationModel = meta.model;
1998
+ if (typeof relationModel === 'undefined') {
1999
+ continue;
2000
+ }
2001
+ let relationModelID;
2002
+ let relationType;
2003
+ if (relationModel === null) {
2004
+ relationModelID = null;
2005
+ relationType = definitions.documentInterface;
2006
+ } else {
2007
+ relationModelID = getGlobalID(relationModel, id).toString();
2008
+ // Check if the related model is included in the schema
2009
+ if (this._includedModelIDs.has(relationModelID)) {
2010
+ relationType = this._buildDocument(relationModelID);
2011
+ } else {
2012
+ // Fall back to generic DocumentNode interface
2013
+ relationType = definitions.documentInterface;
2014
+ relationModelID = null; // Treat as unspecified model
2015
+ }
2016
+ }
2017
+ if (isList(field.type)) {
2018
+ // TODO: add suport for filters?
2019
+ fields[`${key}Documents`] = {
2020
+ type: new GraphQLNonNull(toOuputList(relationType)),
2021
+ resolve: async (doc, _args, ctx)=>{
2022
+ const relationIDs = doc.data?.[key] ?? [];
2023
+ return relationIDs.length ? await ctx.resolveList(this.getModelIDs(relationModelID), relationIDs) : [];
2024
+ }
2025
+ };
2026
+ } else {
2027
+ fields[`${key}Document`] = {
2028
+ type: relationType,
2029
+ resolve: async (doc, _args, ctx)=>{
2030
+ const relationID = doc.data?.[key];
2031
+ return relationID ? await ctx.loadDocument(relationID) : null;
2032
+ }
2033
+ };
2034
+ }
2035
+ break;
2036
+ }
2037
+ }
2038
+ }
2039
+ for (const iid of model.interfaces ?? []){
2040
+ const interfaceID = getGlobalID(iid, id).toString();
2041
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[interfaceID] ?? {})){
2042
+ for (const relation of relations){
2043
+ if (!this._includedModelIDs.has(relation.model)) continue;
2044
+ this._buildRelationFields(fields, fieldName, relation);
2045
+ }
2046
+ }
2047
+ }
2048
+ for (const [fieldName, relations] of Object.entries(this._relationsTo[id] ?? {})){
2049
+ for (const relation of relations){
2050
+ if (!this._includedModelIDs.has(relation.model)) continue;
2051
+ this._buildRelationFields(fields, fieldName, relation);
2052
+ }
2053
+ }
2054
+ return fields;
2055
+ }
2056
+ _buildDocumentDataObject(id, model, name) {
2057
+ const typeID = `${id}-data`;
2058
+ if (this._types[typeID] != null) {
2059
+ return this._types[typeID];
2060
+ }
2061
+ const required = model.schema.required ?? [];
2062
+ const fields = {};
2063
+ for (const [key, value] of Object.entries(model.schema.properties ?? {})){
2064
+ const fieldRef = value.$ref;
2065
+ if (fieldRef == null) {
2066
+ throw new Error(`Missing ref for field ${key} of object ${id}`);
2067
+ }
2068
+ const type = this._getReference(fieldRef);
2069
+ fields[key] = {
2070
+ type: required.includes(key) ? new GraphQLNonNull(type) : type
2071
+ };
2072
+ }
2073
+ const config = {
2074
+ name: `${name}Data`,
2075
+ interfaces: ()=>{
2076
+ return model.interfaces.map((interfaceID)=>{
2077
+ const typeID = `${getGlobalID(interfaceID, id).toString()}-data`;
2078
+ return this._types[typeID];
2079
+ }).filter((type)=>type != null);
2080
+ },
2081
+ fields
2082
+ };
2083
+ const object = model.behavior === 'interface' ? new GraphQLInterfaceType(config) : new GraphQLObjectType(config);
2084
+ this._types[typeID] = object;
2085
+ return object;
2086
+ }
2087
+ _buildDocumentInput(id, model) {
2088
+ if (model.behavior === 'interface') {
2089
+ // No mutations for interfaces
2090
+ return;
2091
+ }
2092
+ const setFields = {};
2093
+ const updateFields = {};
2094
+ for (const [key, value] of Object.entries(model.schema.properties)){
2095
+ const path = value.$ref;
2096
+ const setType = this._getReferenceInput(path, false);
2097
+ setFields[key] = {
2098
+ type: model.schema.required.includes(key) ? new GraphQLNonNull(setType) : setType
2099
+ };
2100
+ const id = getReferenceID(path);
2101
+ const schema = this._getReferenceSchema(id);
2102
+ if (schema.const == null) {
2103
+ updateFields[key] = {
2104
+ type: this._getReferenceInput(path, true)
2105
+ };
2106
+ }
2107
+ }
2108
+ const name = this._aliases[id];
2109
+ this._inputObjects[id] = new GraphQLInputObjectType({
2110
+ name: `${name}Input`,
2111
+ fields: setFields
2112
+ });
2113
+ this._inputObjects[`${id}-update`] = new GraphQLInputObjectType({
2114
+ name: `Partial${name}Input`,
2115
+ fields: updateFields
2116
+ });
2117
+ }
2118
+ _buildReferenceFilterInput(path, parentName = '') {
2119
+ const id = getReferenceID(path);
2120
+ const schema = this._getReferenceSchema(id);
2121
+ let refParentName = parentName;
2122
+ if (schema.type === 'object') {
2123
+ return schema.additionalProperties ? null : this._buildObjectFilterInput(id, schema, parentName);
2124
+ }
2125
+ if (schema.type === 'array') {
2126
+ const name = this._aliases[id] ?? pascalCase(parentName + schema.title);
2127
+ refParentName = name;
2128
+ // const itemType = this._buildReferenceFilterInput(schema.items.$ref, schema.title)
2129
+ return new GraphQLInputObjectType({
2130
+ name: `${name}Filter`,
2131
+ isOneOf: true,
2132
+ fields: {
2133
+ // DB does not support filtering on array values
2134
+ // contains: { type: toList(itemType) },
2135
+ // isEmpty: { type: GraphQLBoolean },
2136
+ isNull: {
2137
+ type: GraphQLBoolean
2138
+ }
2139
+ }
2140
+ });
2141
+ }
2142
+ const scalar = this._buildScalar(id, schema, refParentName);
2143
+ const knownFilter = VALUE_FILTER_INPUT_TYPES[scalar.name];
2144
+ if (knownFilter != null) {
2145
+ return this._inputObjects[knownFilter];
2146
+ }
2147
+ const filter = isEnumType(scalar) ? createEnumValueFilterInput(scalar) : createScalarValueFilterInput(scalar);
2148
+ if (this._inputObjects[filter.name] == null) {
2149
+ this._inputObjects[filter.name] = filter;
2150
+ }
2151
+ return this._inputObjects[filter.name];
2152
+ }
2153
+ _buildReferenceOrderByInput(path, parentName = '') {
2154
+ const id = getReferenceID(path);
2155
+ const schema = this._getReferenceSchema(id);
2156
+ switch(schema.type){
2157
+ case 'array':
2158
+ return null;
2159
+ case 'object':
2160
+ return schema.additionalProperties ? null : this._buildObjectOrderByInput(id, schema, parentName);
2161
+ default:
2162
+ return OrderByDirection;
2163
+ }
2164
+ }
2165
+ _buildObjectFilterInput(id, schema, parentName = '', isDocument = false) {
2166
+ const inputID = `${id}-filter`;
2167
+ const existing = this._inputObjects[inputID];
2168
+ if (existing != null) {
2169
+ return existing;
2170
+ }
2171
+ const name = this._aliases[id] ?? pascalCase(parentName + (schema.title ?? ''));
2172
+ const fields = isDocument ? {
2173
+ _owner: {
2174
+ type: this._inputObjects.AccountValueFilter
2175
+ }
2176
+ } : {};
2177
+ for (const [key, value] of Object.entries(schema.properties)){
2178
+ const type = this._buildReferenceFilterInput(value.$ref, name);
2179
+ if (type !== null) {
2180
+ fields[key] = {
2181
+ type
2182
+ };
2183
+ }
2184
+ }
2185
+ const objectInput = new GraphQLInputObjectType({
2186
+ name: `${name}ObjectFilter`,
2187
+ fields
2188
+ });
2189
+ const input = new GraphQLInputObjectType({
2190
+ name: `${name}Filter`,
2191
+ isOneOf: true,
2192
+ fields: ()=>({
2193
+ where: {
2194
+ type: objectInput
2195
+ },
2196
+ and: {
2197
+ type: toList(this._inputObjects[inputID])
2198
+ },
2199
+ or: {
2200
+ type: toList(this._inputObjects[inputID])
2201
+ },
2202
+ not: {
2203
+ type: this._inputObjects[inputID]
2204
+ }
2205
+ })
2206
+ });
2207
+ this._inputObjects[inputID] = input;
2208
+ return input;
2209
+ }
2210
+ _buildObjectOrderByInput(id, schema, parentName = '', isDocument = false) {
2211
+ const inputID = `${id}-order`;
2212
+ const existing = this._inputObjects[inputID];
2213
+ if (existing != null) {
2214
+ return existing;
2215
+ }
2216
+ const name = this._aliases[id] ?? pascalCase(parentName + (schema.title ?? ''));
2217
+ const fields = isDocument ? {
2218
+ _docOwner: {
2219
+ type: OrderByDirection
2220
+ },
2221
+ _createdAt: {
2222
+ type: OrderByDirection
2223
+ }
2224
+ } : {};
2225
+ for (const [key, value] of Object.entries(schema.properties)){
2226
+ const type = this._buildReferenceOrderByInput(value.$ref, name);
2227
+ if (type != null) {
2228
+ fields[key] = {
2229
+ type
2230
+ };
2231
+ }
2232
+ }
2233
+ const input = new GraphQLInputObjectType({
2234
+ name: `${name}OrderBy`,
2235
+ isOneOf: true,
2236
+ fields
2237
+ });
2238
+ this._inputObjects[inputID] = input;
2239
+ return input;
2240
+ }
2241
+ _buildSetInputObjectType(id, withFields, relationField) {
2242
+ const relationFieldName = relationField ? pascalCase(relationField) : '';
2243
+ const inputID = `${id}-with-${relationFieldName}`;
2244
+ const existing = this._inputObjects[inputID];
2245
+ if (existing != null) {
2246
+ return existing;
2247
+ }
2248
+ const model = this._record[id];
2249
+ const name = this._aliases[id];
2250
+ this._inputObjects[inputID] = new GraphQLInputObjectType({
2251
+ name: `With${relationFieldName}${name}Input`,
2252
+ fields: ()=>{
2253
+ const fields = {};
2254
+ for (const fieldName of withFields){
2255
+ const field = model.schema.properties[fieldName];
2256
+ if (field == null) {
2257
+ throw new Error(`Field ${fieldName} not found on model ${name}`);
2258
+ }
2259
+ const type = this._getReferenceInput(field.$ref, false);
2260
+ fields[fieldName] = {
2261
+ type: new GraphQLNonNull(type)
2262
+ };
2263
+ }
2264
+ return fields;
2265
+ }
2266
+ });
2267
+ return this._inputObjects[inputID];
2268
+ }
2269
+ _buildReference(id, schema, parentName = '') {
2270
+ if (this._types[id] != null) {
2271
+ return this._types[id];
2272
+ }
2273
+ switch(schema.type){
2274
+ case 'array':
2275
+ this._types[id] = this._buildArray(schema, parentName);
2276
+ return this._types[id];
2277
+ case 'object':
2278
+ this._types[id] = schema.additionalProperties ? GraphQLJSONObject : this._buildObject(id, schema, parentName);
2279
+ return this._types[id];
2280
+ default:
2281
+ return this._buildScalar(id, schema, parentName);
2282
+ }
2283
+ }
2284
+ _buildScalar(id, schema, parentName = '') {
2285
+ if (this._types[id] != null) {
2286
+ return this._types[id];
2287
+ }
2288
+ switch(schema.type){
2289
+ case 'boolean':
2290
+ this._types[id] = GraphQLBoolean;
2291
+ break;
2292
+ case 'integer':
2293
+ this._types[id] = GraphQLInt;
2294
+ break;
2295
+ case 'number':
2296
+ this._types[id] = GraphQLFloat;
2297
+ break;
2298
+ case 'string':
2299
+ this._types[id] = this._buildString(schema, parentName);
2300
+ break;
2301
+ }
2302
+ return this._types[id];
2303
+ }
2304
+ _buildArray(schema, parentName = '') {
2305
+ const itemRef = schema.items.$ref;
2306
+ return new GraphQLList(new GraphQLNonNull(this._getReference(itemRef, pascalCase(parentName + schema.title))));
2307
+ }
2308
+ _buildObject(id, schema, parentName = '') {
2309
+ const name = pascalCase(parentName + schema.title);
2310
+ const fields = this._buildObjectFields(id, schema, name);
2311
+ return new GraphQLObjectType({
2312
+ name,
2313
+ fields
2314
+ });
2315
+ }
2316
+ _buildObjectFields(id, schema, parentName = '') {
2317
+ const required = schema.required ?? [];
2318
+ const fields = {};
2319
+ for (const [key, value] of Object.entries(schema.properties ?? {})){
2320
+ const fieldRef = value.$ref;
2321
+ if (fieldRef == null) {
2322
+ throw new Error(`Missing ref for field ${key} of object ${id}`);
2323
+ }
2324
+ const type = this._getReference(fieldRef, parentName);
2325
+ fields[key] = {
2326
+ type: required.includes(key) ? new GraphQLNonNull(type) : type
2327
+ };
2328
+ }
2329
+ return fields;
2330
+ }
2331
+ _buildRelationFields(fields, fieldName, relation) {
2332
+ if (relation.isList) {
2333
+ // DB does not support matches for array containing values
2334
+ return;
2335
+ }
2336
+ const relationID = relation.model;
2337
+ const relationName = `${pascalCase(fieldName)}Of${this._aliases[relationID]}`;
2338
+ fields[`in${relationName}`] = {
2339
+ type: new GraphQLNonNull(this._types[`${relationID}-connection`]),
2340
+ args: {
2341
+ ...connectionArgs,
2342
+ filter: {
2343
+ type: this._inputObjects[`${relationID}-filter`]
2344
+ },
2345
+ orderBy: {
2346
+ type: toList(this._inputObjects[`${relationID}-order`])
2347
+ }
2348
+ },
2349
+ resolve: (doc, args, ctx)=>{
2350
+ const docFilter = {
2351
+ where: {
2352
+ [fieldName]: {
2353
+ equalTo: doc.id
2354
+ }
2355
+ }
2356
+ };
2357
+ const filter = args.filter ? {
2358
+ and: [
2359
+ args.filter,
2360
+ docFilter
2361
+ ]
2362
+ } : docFilter;
2363
+ return ctx.resolveConnection(this.getModelIDs(relationID), {
2364
+ ...args,
2365
+ filter
2366
+ });
2367
+ }
2368
+ };
2369
+ }
2370
+ _buildRelationSubscriptions(fields, modelID, fieldName, relation) {
2371
+ if (relation.isList) {
2372
+ // Inverse relations to list fields are not supported in connections so we skip them for subscriptions as well
2373
+ return;
2374
+ }
2375
+ const targetFieldName = pascalCase(fieldName);
2376
+ fields[`edge${this._aliases[relation.model]}AddedAs${targetFieldName}To${this._aliases[modelID]}`] = {
2377
+ type: new GraphQLNonNull(this._types[`${relation.model}-edge`]),
2378
+ args: {
2379
+ id: {
2380
+ type: new GraphQLNonNull(GraphQLID)
2381
+ }
2382
+ },
2383
+ resolve: (edge)=>edge,
2384
+ subscribe: (_src, args, ctx)=>{
2385
+ return ctx.subscribeToDocumentEdgeAdded(this.getModelIDs(relation.model), (data)=>data[fieldName] === args.id);
2386
+ }
2387
+ };
2388
+ fields[`edge${this._aliases[relation.model]}RemovedAs${targetFieldName}From${this._aliases[modelID]}`] = {
2389
+ type: new GraphQLNonNull(GraphQLID),
2390
+ args: {
2391
+ id: {
2392
+ type: new GraphQLNonNull(GraphQLID)
2393
+ }
2394
+ },
2395
+ resolve: (id)=>id,
2396
+ subscribe: (_src, args, ctx)=>{
2397
+ return ctx.subscribeToDocumentEdgeRemoved(this.getModelIDs(relation.model), (data)=>data[fieldName] === args.id);
2398
+ }
2399
+ };
2400
+ }
2401
+ _buildString(schema, parentName) {
2402
+ if (schema.const != null) {
2403
+ return GraphQLString;
2404
+ }
2405
+ if (schema.enum != null) {
2406
+ return this._buildEnumString(schema, parentName);
2407
+ }
2408
+ if (schema.format != null) {
2409
+ return this._buildFormatString(schema);
2410
+ }
2411
+ return this._buildCustomString(schema);
2412
+ }
2413
+ _buildCustomString(schema) {
2414
+ if (schema.title == null) {
2415
+ // Plain string type
2416
+ return GraphQLString;
2417
+ }
2418
+ const title = schema.title.toLowerCase();
2419
+ return STRING_TITLES[title] ?? GraphQLString;
2420
+ }
2421
+ _buildEnumString(schema, parentName = '') {
2422
+ const name = pascalCase(parentName + schema.title);
2423
+ const typeName = `${name}-enum`;
2424
+ if (this._types[typeName] == null) {
2425
+ const values = {};
2426
+ for (const value of schema.enum){
2427
+ values[value] = {
2428
+ value
2429
+ };
2430
+ }
2431
+ this._types[typeName] = new GraphQLEnumType({
2432
+ name,
2433
+ values
2434
+ });
2435
+ }
2436
+ return this._types[typeName];
2437
+ }
2438
+ _buildFormatString(schema) {
2439
+ const scalar = STRING_FORMATS[schema.format];
2440
+ if (scalar == null) {
2441
+ throw new Error(`Unsupported string format: ${schema.format}`);
2442
+ }
2443
+ return scalar;
2444
+ }
2445
+ }
2446
+ export function createSchema(params) {
2447
+ return new SchemaBuilder(params).build();
2448
+ }