@kubun/graphql 0.4.5 → 0.6.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 CHANGED
@@ -2,7 +2,7 @@ import type { KubunDB } from '@kubun/db';
2
2
  import type { DocumentData, DocumentNode } from '@kubun/protocol';
3
3
  import type { GraphQLResolveInfo } from 'graphql';
4
4
  import type { Connection, Edge } from 'graphql-relay';
5
- import type { AllConnectionArguments, UpdateMutationInput } from './types.js';
5
+ import type { AllConnectionArguments, ModelAccessDefaults, UpdateMutationInput } from './types.js';
6
6
  /**
7
7
  * Query functions requiring server implementation
8
8
  */
@@ -11,6 +11,11 @@ export type QueryContext = {
11
11
  loadDocument(id: string): Promise<DocumentNode | null>;
12
12
  resolveConnection(modelIDs: Array<string>, args: AllConnectionArguments): Promise<Connection<DocumentNode>>;
13
13
  resolveList(modelIDs: Array<string>, ids: Array<string>): Promise<Array<DocumentNode>>;
14
+ searchDocuments(query: string, modelIDs: Array<string>, first?: number): Promise<Array<{
15
+ documentID: string;
16
+ modelID: string;
17
+ rank: number;
18
+ }>>;
14
19
  };
15
20
  /**
16
21
  * Subscription functions requiring server implementation
@@ -41,10 +46,9 @@ export type MutationContext = {
41
46
  executeSetMutation(modelID: string, unique: Uint8Array, data: DocumentData, info: GraphQLResolveInfo): Promise<DocumentNode>;
42
47
  executeUpdateMutation(input: UpdateMutationInput, info: GraphQLResolveInfo): Promise<DocumentNode>;
43
48
  executeRemoveMutation(id: string, info: GraphQLResolveInfo): Promise<void>;
44
- executeSetModelAccessDefaults(modelId: string, permissionType: 'read' | 'write', accessLevel: string, allowedDIDs: Array<string> | null): Promise<void>;
49
+ executeSetModelAccessDefaults(modelId: string, permissionType: 'read' | 'write', accessLevel: string, allowedDIDs: Array<string> | null): Promise<ModelAccessDefaults>;
45
50
  executeRemoveModelAccessDefaults(modelId: string, permissionTypes: Array<'read' | 'write'>): Promise<void>;
46
- executeSetDocumentAccessOverride(documentId: string, permissionType: 'read' | 'write', accessLevel: string, allowedDIDs: Array<string> | null): Promise<void>;
51
+ executeSetDocumentAccessOverride(documentId: string, permissionType: 'read' | 'write', accessLevel: string, allowedDIDs: Array<string> | null): Promise<DocumentNode>;
47
52
  executeRemoveDocumentAccessOverride(documentId: string, permissionTypes: Array<'read' | 'write'>): Promise<void>;
48
53
  };
49
54
  export type Context = ReadOnlyContext & MutationContext;
50
- //# sourceMappingURL=context.d.ts.map
package/lib/context.js CHANGED
@@ -1,223 +1 @@
1
- import { defer } from '@enkaku/async';
2
- import { DocumentID } from '@kubun/id';
3
- export function createReadContext(params) {
4
- const { db, viewerDID, accessChecker } = params;
5
- function createEventGenerator(filter, transform) {
6
- let isDone = false;
7
- let pending = null;
8
- const queue = [];
9
- const stop = db.events.on('document:saved', (event)=>{
10
- if (filter(event)) {
11
- const toPush = transform(event);
12
- if (pending == null) {
13
- queue.push(toPush);
14
- } else {
15
- pending.resolve(toPush);
16
- pending = null;
17
- }
18
- }
19
- });
20
- const setDone = ()=>{
21
- stop();
22
- isDone = true;
23
- return Promise.resolve({
24
- done: true,
25
- value: undefined
26
- });
27
- };
28
- return {
29
- [Symbol.asyncDispose] () {
30
- stop();
31
- isDone = true;
32
- return Promise.resolve();
33
- },
34
- [Symbol.asyncIterator] () {
35
- return this;
36
- },
37
- next: ()=>{
38
- if (isDone) {
39
- return Promise.resolve({
40
- done: true,
41
- value: undefined
42
- });
43
- }
44
- const value = queue.shift();
45
- if (value != null) {
46
- return Promise.resolve({
47
- value,
48
- done: false
49
- });
50
- }
51
- pending = defer();
52
- return pending.promise.then((value)=>({
53
- value,
54
- done: false
55
- }));
56
- },
57
- return: setDone,
58
- throw: setDone
59
- };
60
- }
61
- return {
62
- getViewer () {
63
- return viewerDID;
64
- },
65
- async loadDocument (id) {
66
- const doc = await db.getDocument(DocumentID.fromString(id));
67
- if (!doc) return null;
68
- // Check access if access checker is provided
69
- if (accessChecker) {
70
- const hasAccess = await accessChecker(doc, 'read');
71
- if (!hasAccess) {
72
- // Access denied - return null (error will be added by GraphQL layer)
73
- return null;
74
- }
75
- }
76
- return doc;
77
- },
78
- async resolveConnection (modelIDs, args) {
79
- // If no access checker, return all documents
80
- if (accessChecker == null) {
81
- const { entries, hasMore } = await db.queryDocuments({
82
- ...args,
83
- modelIDs
84
- });
85
- return {
86
- edges: entries.map((e)=>({
87
- cursor: e.cursor,
88
- node: e.document
89
- })),
90
- pageInfo: {
91
- hasNextPage: args.first ? hasMore : false,
92
- hasPreviousPage: args.first ? false : hasMore,
93
- endCursor: entries.at(-1)?.cursor ?? null,
94
- startCursor: entries.at(0)?.cursor ?? null
95
- }
96
- };
97
- }
98
- // With access control: post-fetch filtering with over-fetching
99
- const allEntries = [];
100
- let requestedCount = 0;
101
- let attempts = 0;
102
- const maxAttempts = 10 // Prevent infinite loops
103
- ;
104
- if (args.first != null) {
105
- requestedCount = args.first;
106
- const fetchSize = requestedCount * 2 // Over-fetch to reduce round trips
107
- ;
108
- let cursor = args.after;
109
- while(allEntries.length < requestedCount && attempts < maxAttempts){
110
- // Fetch unfiltered batch from DB
111
- const { entries, hasMore } = await db.queryDocuments({
112
- ...args,
113
- modelIDs,
114
- first: fetchSize,
115
- after: cursor
116
- });
117
- if (entries.length === 0 && !hasMore) break;
118
- // Filter based on access rules
119
- for (const entry of entries){
120
- if (await accessChecker(entry.document, 'read')) {
121
- allEntries.push(entry);
122
- if (allEntries.length >= requestedCount) break;
123
- }
124
- }
125
- if (!hasMore) break;
126
- cursor = entries.at(-1)?.cursor ?? null;
127
- attempts++;
128
- }
129
- } else if (args.last != null) {
130
- requestedCount = args.last;
131
- const fetchSize = requestedCount * 2 // Over-fetch to reduce round trips
132
- ;
133
- let cursor = args.before;
134
- while(allEntries.length < requestedCount && attempts < maxAttempts){
135
- // Fetch unfiltered batch from DB
136
- const { entries, hasMore } = await db.queryDocuments({
137
- ...args,
138
- modelIDs,
139
- last: fetchSize,
140
- before: cursor
141
- });
142
- if (entries.length === 0 && !hasMore) break;
143
- // Filter based on access rules - note we reverse the entries array for backward pagination
144
- for (const entry of [
145
- ...entries
146
- ].reverse()){
147
- if (await accessChecker(entry.document, 'read')) {
148
- allEntries.unshift(entry);
149
- if (allEntries.length >= requestedCount) break;
150
- }
151
- }
152
- if (!hasMore) break;
153
- cursor = entries.at(0)?.cursor ?? null;
154
- attempts++;
155
- }
156
- }
157
- // Return only requested count
158
- const resultEntries = allEntries.slice(0, requestedCount);
159
- const hasMoreResults = allEntries.length > requestedCount;
160
- return {
161
- edges: resultEntries.map((e)=>({
162
- cursor: e.cursor,
163
- node: e.document
164
- })),
165
- pageInfo: {
166
- hasNextPage: args.first ? hasMoreResults : false,
167
- hasPreviousPage: args.first ? false : hasMoreResults,
168
- endCursor: resultEntries.at(-1)?.cursor ?? null,
169
- startCursor: resultEntries.at(0)?.cursor ?? null
170
- }
171
- };
172
- },
173
- async resolveList (modelIDs, docIDs) {
174
- const documents = await db.listDocuments({
175
- modelIDs,
176
- docIDs
177
- });
178
- // If no access checker, return all documents
179
- if (!accessChecker) {
180
- return documents;
181
- }
182
- // Filter based on access control
183
- const accessible = [];
184
- for (const doc of documents){
185
- if (await accessChecker(doc, 'read')) {
186
- accessible.push(doc);
187
- }
188
- }
189
- return accessible;
190
- },
191
- subscribeToDocumentCreated (modelIDs) {
192
- return createEventGenerator((event)=>event.type === 'create' && modelIDs.includes(event.document.model), (event)=>event.document);
193
- },
194
- subscribeToDocumentChanged (id) {
195
- return createEventGenerator((event)=>{
196
- return event.type === 'update' && event.document.id === id && event.document.data !== null;
197
- }, (event)=>event.document);
198
- },
199
- subscribeToDocumentRemoved () {
200
- return createEventGenerator((event)=>event.type === 'update' && event.document.data === null, (event)=>event.document.id);
201
- },
202
- subscribeToDocumentEdgeAdded (modelIDs, hasRelation) {
203
- return createEventGenerator((event)=>{
204
- return(// document has data
205
- event.document.data !== null && // document model is one of the target models
206
- modelIDs.includes(event.document.model) && // relation field targets the expected document
207
- hasRelation(event.document.data) && // new document or new relation to document
208
- (event.type === 'create' || event.previous.data !== null && hasRelation(event.previous.data)));
209
- }, (event)=>({
210
- cursor: event.getCursor(),
211
- node: event.document
212
- }));
213
- },
214
- subscribeToDocumentEdgeRemoved (modelIDs, hasRelation) {
215
- return createEventGenerator((event)=>{
216
- return event.type === 'update' && // document model is one of the target models
217
- modelIDs.includes(event.document.model) && // relation existed in previous version
218
- event.previous.data !== null && hasRelation(event.previous.data) && // relation is removed
219
- (event.document.data === null || !hasRelation(event.document.data));
220
- }, (event)=>event.document.id);
221
- }
222
- };
223
- }
1
+ import{defer as e}from"@enkaku/async";import{DocumentID as t}from"@kubun/id";export function createReadContext(r){let{db:a,viewerDID:n,accessChecker:o}=r;function u(t,r){let n=!1,o=null,u=[],s=a.events.on("document:saved",e=>{if(t(e)){let t=r(e);null==o?u.push(t):(o.resolve(t),o=null)}}),l=()=>(s(),n=!0,Promise.resolve({done:!0,value:void 0}));return{[Symbol.asyncDispose]:()=>(s(),n=!0,Promise.resolve()),[Symbol.asyncIterator](){return this},next:()=>{if(n)return Promise.resolve({done:!0,value:void 0});let t=u.shift();return null!=t?Promise.resolve({value:t,done:!1}):(o=e()).promise.then(e=>({value:e,done:!1}))},return:l,throw:l}}return{getViewer:()=>n,async loadDocument(e){let r=await a.getDocument(t.fromString(e));return r&&(!o||await o(r,"read"))?r:null},searchDocuments:async(e,t,r)=>await a.searchDocuments({query:e,modelIDs:t,first:r}),async resolveConnection(e,t){if(null!=t.search&&""!==t.search.trim()){let r=await a.searchDocuments({query:t.search,modelIDs:e,first:200});if(0===r.length)return{edges:[],pageInfo:{hasNextPage:!1,hasPreviousPage:!1,startCursor:null,endCursor:null}};let n=r.map(e=>e.documentID),u=await a.listDocuments({modelIDs:e,docIDs:n}),s=u;if(o)for(let e of(s=[],u))await o(e,"read")&&s.push(e);let l=new Map(r.map(e=>[e.documentID,e.rank]));s.sort((e,t)=>Math.abs(l.get(t.id)??0)-Math.abs(l.get(e.id)??0));let i=s.slice(0,t.first??50);return{edges:i.map((e,t)=>({cursor:String(t),node:e})),pageInfo:{hasNextPage:s.length>i.length,hasPreviousPage:!1,startCursor:i.length>0?"0":null,endCursor:i.length>0?String(i.length-1):null}}}if(null==o){let{entries:r,hasMore:n}=await a.queryDocuments({...t,modelIDs:e});return{edges:r.map(e=>({cursor:e.cursor,node:e.document})),pageInfo:{hasNextPage:!!t.first&&n,hasPreviousPage:!t.first&&n,endCursor:r.at(-1)?.cursor??null,startCursor:r.at(0)?.cursor??null}}}let r=[],n=0,u=0;if(null!=t.first){let s=2*(n=t.first),l=t.after;for(;r.length<n&&u<10;){let{entries:i,hasMore:d}=await a.queryDocuments({...t,modelIDs:e,first:s,after:l});if(0===i.length&&!d)break;for(let e of i)if(await o(e.document,"read")&&(r.push(e),r.length>=n))break;if(!d)break;l=i.at(-1)?.cursor??null,u++}}else if(null!=t.last){let s=2*(n=t.last),l=t.before;for(;r.length<n&&u<10;){let{entries:i,hasMore:d}=await a.queryDocuments({...t,modelIDs:e,last:s,before:l});if(0===i.length&&!d)break;for(let e of[...i].reverse())if(await o(e.document,"read")&&(r.unshift(e),r.length>=n))break;if(!d)break;l=i.at(0)?.cursor??null,u++}}let s=r.slice(0,n),l=r.length>n;return{edges:s.map(e=>({cursor:e.cursor,node:e.document})),pageInfo:{hasNextPage:!!t.first&&l,hasPreviousPage:!t.first&&l,endCursor:s.at(-1)?.cursor??null,startCursor:s.at(0)?.cursor??null}}},async resolveList(e,t){let r=await a.listDocuments({modelIDs:e,docIDs:t});if(!o)return r;let n=[];for(let e of r)await o(e,"read")&&n.push(e);return n},subscribeToDocumentCreated(e){let t=u(t=>"create"===t.type&&e.includes(t.document.model),e=>e.document);return o?async function*(){for await(let e of t)await o(e,"read")&&(yield e)}():t},subscribeToDocumentChanged(e){let t=u(t=>"update"===t.type&&t.document.id===e&&null!==t.document.data,e=>e.document);return o?async function*(){for await(let e of t)await o(e,"read")&&(yield e)}():t},subscribeToDocumentRemoved:()=>u(e=>"update"===e.type&&null===e.document.data,e=>e.document.id),subscribeToDocumentEdgeAdded(e,t){let r=u(r=>null!==r.document.data&&e.includes(r.document.model)&&t(r.document.data)&&("create"===r.type||null!==r.previous.data&&t(r.previous.data)),e=>({cursor:e.getCursor(),node:e.document}));return o?async function*(){for await(let e of r)await o(e.node,"read")&&(yield e)}():r},subscribeToDocumentEdgeRemoved:(e,t)=>u(r=>"update"===r.type&&e.includes(r.document.model)&&null!==r.previous.data&&t(r.previous.data)&&(null===r.document.data||!t(r.document.data)),e=>e.document.id)}}
@@ -1,4 +1,3 @@
1
1
  import { GraphQLScalarType } from 'graphql';
2
2
  export declare const GraphQLAttachmentID: GraphQLScalarType<string, string>;
3
3
  export declare const GraphQLDocID: GraphQLScalarType<string, string>;
4
- //# sourceMappingURL=identifiers.d.ts.map
@@ -1,42 +1 @@
1
- import { AttachmentID, DocumentID } from '@kubun/id';
2
- import { GraphQLError, GraphQLScalarType, Kind } from 'graphql';
3
- function validateAttachmentID(input) {
4
- if (typeof input !== 'string') {
5
- throw new GraphQLError(`Invalid AttachmentID input: ${input}`);
6
- }
7
- // Throws if input is not a valid AttachmentID string
8
- AttachmentID.fromString(input);
9
- return input;
10
- }
11
- export const GraphQLAttachmentID = new GraphQLScalarType({
12
- name: 'AttachmentID',
13
- description: 'A Kubun Attachment identifier',
14
- serialize: validateAttachmentID,
15
- parseValue: validateAttachmentID,
16
- parseLiteral (ast) {
17
- if (ast.kind !== Kind.STRING) {
18
- throw new GraphQLError(`Can only validate strings as AttachmentID but got a: ${ast.kind}`);
19
- }
20
- return validateAttachmentID(ast.value);
21
- }
22
- });
23
- function validateDocID(input) {
24
- if (typeof input !== 'string') {
25
- throw new GraphQLError(`Invalid DocumentID input: ${input}`);
26
- }
27
- // Throws if input is not a valid DocumentID string
28
- DocumentID.fromString(input);
29
- return input;
30
- }
31
- export const GraphQLDocID = new GraphQLScalarType({
32
- name: 'DocumentID',
33
- description: 'A Kubun Document identifier',
34
- serialize: validateDocID,
35
- parseValue: validateDocID,
36
- parseLiteral (ast) {
37
- if (ast.kind !== Kind.STRING) {
38
- throw new GraphQLError(`Can only validate strings as DocumentID but got a: ${ast.kind}`);
39
- }
40
- return validateDocID(ast.value);
41
- }
42
- });
1
+ import{AttachmentID as t,DocumentID as n}from"@kubun/id";import{GraphQLError as e,GraphQLScalarType as i,Kind as r}from"graphql";function a(n){if("string"!=typeof n)throw new e(`Invalid AttachmentID input: ${n}`);return t.fromString(n),n}export const GraphQLAttachmentID=new i({name:"AttachmentID",description:"A Kubun Attachment identifier",serialize:a,parseValue:a,parseLiteral(t){if(t.kind!==r.STRING)throw new e(`Can only validate strings as AttachmentID but got a: ${t.kind}`);return a(t.value)}});function o(t){if("string"!=typeof t)throw new e(`Invalid DocumentID input: ${t}`);return n.fromString(t),t}export const GraphQLDocID=new i({name:"DocumentID",description:"A Kubun Document identifier",serialize:o,parseValue:o,parseLiteral(t){if(t.kind!==r.STRING)throw new e(`Can only validate strings as DocumentID but got a: ${t.kind}`);return o(t.value)}});
package/lib/index.d.ts CHANGED
@@ -4,4 +4,3 @@ export { GraphQLAttachmentID, GraphQLDocID } from './identifiers.js';
4
4
  export type { SchemaBuilderParams, SharedDefinitions, } from './schema.js';
5
5
  export { createSchema, SchemaBuilder } from './schema.js';
6
6
  export type * from './types.js';
7
- //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -1,3 +1 @@
1
- export { createReadContext } from './context.js';
2
- export { GraphQLAttachmentID, GraphQLDocID } from './identifiers.js';
3
- export { createSchema, SchemaBuilder } from './schema.js';
1
+ export{createReadContext}from"./context.js";export{GraphQLAttachmentID,GraphQLDocID}from"./identifiers.js";export{createSchema,SchemaBuilder}from"./schema.js";
package/lib/schema.d.ts CHANGED
@@ -85,4 +85,3 @@ export declare class SchemaBuilder {
85
85
  }
86
86
  export declare function createSchema(params: SchemaBuilderParams): GraphQLSchema;
87
87
  export {};
88
- //# sourceMappingURL=schema.d.ts.map