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