@kubun/graphql 0.11.0 → 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 +8 -1
- package/lib/context.js +34 -22
- package/lib/scalars.js +1 -0
- package/lib/schema.js +36 -0
- package/package.json +23 -23
package/lib/context.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DocumentID } from '@kubun/id';
|
|
2
2
|
import type { AggregateHaving, AggregateSpec, AggregateValues, DocumentData, DocumentFilter, DocumentNode, DocumentOrderBy, GroupedAggregateOrderTerm, GroupedAggregateValue } from '@kubun/protocol';
|
|
3
|
-
import type { ViewerReadAccess } from '@kubun/store-graph';
|
|
3
|
+
import type { StoredAccessDefaultRow, ViewerReadAccess } from '@kubun/store-graph';
|
|
4
4
|
import type { Tracer } from '@sozai/otel';
|
|
5
5
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
6
6
|
import type { Connection, Edge } from 'graphql-relay';
|
|
@@ -83,6 +83,7 @@ export type ReadContextDB = {
|
|
|
83
83
|
modelID: string;
|
|
84
84
|
rank: number;
|
|
85
85
|
}>>;
|
|
86
|
+
listUserModelAccessDefaults(ownerDID: string): Promise<Array<StoredAccessDefaultRow>>;
|
|
86
87
|
};
|
|
87
88
|
/**
|
|
88
89
|
* Query scope carried alongside a connection value so child resolvers (e.g.
|
|
@@ -148,6 +149,12 @@ export type QueryContext = {
|
|
|
148
149
|
modelID: string;
|
|
149
150
|
rank: number;
|
|
150
151
|
}>>;
|
|
152
|
+
/**
|
|
153
|
+
* The viewer's own stored model-access-default rows, across every model and
|
|
154
|
+
* permission type. Scoped to the current viewer DID - not for cross-member
|
|
155
|
+
* visibility.
|
|
156
|
+
*/
|
|
157
|
+
listModelAccessDefaults(): Promise<Array<StoredAccessDefaultRow>>;
|
|
151
158
|
};
|
|
152
159
|
/**
|
|
153
160
|
* Subscription functions requiring server implementation
|
package/lib/context.js
CHANGED
|
@@ -8,6 +8,7 @@ import { withSpan } from '@sozai/otel';
|
|
|
8
8
|
* generator. When a consumer falls behind, the oldest events are evicted so a
|
|
9
9
|
* single slow subscriber cannot grow memory without bound.
|
|
10
10
|
*/ const DEFAULT_SUBSCRIPTION_QUEUE_SIZE = 1000;
|
|
11
|
+
/** Access checks in flight at once — see `filterByAccess`. */ const ACCESS_CHECK_CONCURRENCY = 25;
|
|
11
12
|
/**
|
|
12
13
|
* Minimum spacing between repeated overflow warnings for a single generator, so
|
|
13
14
|
* a sustained overflow logs at most once per interval instead of per drop.
|
|
@@ -25,6 +26,34 @@ import { withSpan } from '@sozai/otel';
|
|
|
25
26
|
const logger = getKubunLogger('graphql');
|
|
26
27
|
export function createReadContext(params) {
|
|
27
28
|
const { store, viewerDID, accessChecker, viewerReadAccess } = params;
|
|
29
|
+
/**
|
|
30
|
+
* Keep the documents a viewer may read, in their original order.
|
|
31
|
+
*
|
|
32
|
+
* The checks are independent reads over one viewer, so they run concurrently;
|
|
33
|
+
* checking in sequence only ever bought the order the denial warnings were
|
|
34
|
+
* pushed in, and filtering after every verdict is in preserves that too.
|
|
35
|
+
*/ async function filterByAccess(documents, check) {
|
|
36
|
+
// Concurrent but bounded. Each check issues its own reads, and the page
|
|
37
|
+
// reaching here can be as large as `unboundedScanCap` (5000) — one
|
|
38
|
+
// `Promise.all` over that queues five thousand reads onto a single SQLite
|
|
39
|
+
// connection at once, which is slower than the sequential version it
|
|
40
|
+
// replaced and starves everything else on that connection.
|
|
41
|
+
const verdicts = [];
|
|
42
|
+
for(let index = 0; index < documents.length; index += ACCESS_CHECK_CONCURRENCY){
|
|
43
|
+
const batch = documents.slice(index, index + ACCESS_CHECK_CONCURRENCY);
|
|
44
|
+
verdicts.push(...await Promise.all(batch.map((doc)=>check(doc, 'read'))));
|
|
45
|
+
}
|
|
46
|
+
return documents.filter((doc, index)=>{
|
|
47
|
+
if (verdicts[index]) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
params.warnings?.push({
|
|
51
|
+
message: `Access denied for document ${doc.id}`,
|
|
52
|
+
code: 'KB07'
|
|
53
|
+
});
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
28
57
|
function traced(name, attributes, fn) {
|
|
29
58
|
if (params.tracer != null) {
|
|
30
59
|
return withSpan(params.tracer, name, {
|
|
@@ -206,6 +235,9 @@ export function createReadContext(params) {
|
|
|
206
235
|
first
|
|
207
236
|
});
|
|
208
237
|
},
|
|
238
|
+
async listModelAccessDefaults () {
|
|
239
|
+
return await store.listUserModelAccessDefaults(viewerDID);
|
|
240
|
+
},
|
|
209
241
|
async resolveConnection (modelIDs, args) {
|
|
210
242
|
const resolverKey = `connection:${modelIDs.join(',')}`;
|
|
211
243
|
const start = params.complexity?.resolverTimings != null ? performance.now() : 0;
|
|
@@ -252,17 +284,7 @@ export function createReadContext(params) {
|
|
|
252
284
|
// Apply access control
|
|
253
285
|
let accessible = documents;
|
|
254
286
|
if (accessChecker) {
|
|
255
|
-
accessible =
|
|
256
|
-
for (const doc of documents){
|
|
257
|
-
if (await accessChecker(doc, 'read')) {
|
|
258
|
-
accessible.push(doc);
|
|
259
|
-
} else if (params.warnings != null) {
|
|
260
|
-
params.warnings.push({
|
|
261
|
-
message: `Access denied for document ${doc.id}`,
|
|
262
|
-
code: 'KB07'
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
287
|
+
accessible = await filterByAccess(documents, accessChecker);
|
|
266
288
|
}
|
|
267
289
|
// Order by search rank
|
|
268
290
|
const rankMap = new Map(searchResults.map((r)=>[
|
|
@@ -491,17 +513,7 @@ export function createReadContext(params) {
|
|
|
491
513
|
return documents;
|
|
492
514
|
}
|
|
493
515
|
// Filter based on access control
|
|
494
|
-
const accessible =
|
|
495
|
-
for (const doc of documents){
|
|
496
|
-
if (await accessChecker(doc, 'read')) {
|
|
497
|
-
accessible.push(doc);
|
|
498
|
-
} else if (params.warnings != null) {
|
|
499
|
-
params.warnings.push({
|
|
500
|
-
message: `Access denied for document ${doc.id}`,
|
|
501
|
-
code: 'KB07'
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
}
|
|
516
|
+
const accessible = await filterByAccess(documents, accessChecker);
|
|
505
517
|
if (params.complexity != null) {
|
|
506
518
|
params.complexity.resolverCount++;
|
|
507
519
|
for (const id of modelIDs){
|
package/lib/scalars.js
CHANGED
package/lib/schema.js
CHANGED
|
@@ -581,6 +581,42 @@ export class SchemaBuilder {
|
|
|
581
581
|
}
|
|
582
582
|
};
|
|
583
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
|
+
};
|
|
584
620
|
// Add access control mutations if mutations are enabled
|
|
585
621
|
if (this._includeMutations) {
|
|
586
622
|
this._mutations.setModelAccessDefaults = {
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/graphql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "see LICENSE.md",
|
|
3
|
+
"version": "0.12.0",
|
|
5
4
|
"keywords": [],
|
|
5
|
+
"license": "see LICENSE.md",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"type": "module",
|
|
7
|
-
"main": "lib/index.js",
|
|
8
|
-
"types": "lib/index.d.ts",
|
|
9
8
|
"exports": {
|
|
10
9
|
".": "./lib/index.js"
|
|
11
10
|
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
12
13
|
"files": [
|
|
13
14
|
"lib/*",
|
|
14
15
|
"LICENSE.md"
|
|
15
16
|
],
|
|
16
|
-
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@sozai/async": "^0.1
|
|
19
|
-
"@sozai/codec": "^0.
|
|
20
|
-
"@sozai/event": "^0.1.
|
|
21
|
-
"@sozai/otel": "^0.2
|
|
18
|
+
"@sozai/async": "^0.2.1",
|
|
19
|
+
"@sozai/codec": "^0.4.0",
|
|
20
|
+
"@sozai/event": "^0.1.3",
|
|
21
|
+
"@sozai/otel": "^0.3.2",
|
|
22
22
|
"change-case": "^5.4.4",
|
|
23
23
|
"graphql": "^16.14.2",
|
|
24
24
|
"graphql-relay": "^0.10.2",
|
|
25
25
|
"graphql-scalars": "^1.25.0",
|
|
26
|
-
"multiformats": "^14.0.
|
|
27
|
-
"@kubun/
|
|
28
|
-
"@kubun/
|
|
29
|
-
"@kubun/
|
|
30
|
-
"@kubun/
|
|
26
|
+
"multiformats": "^14.0.5",
|
|
27
|
+
"@kubun/id": "^0.12.0",
|
|
28
|
+
"@kubun/logger": "^0.12.0",
|
|
29
|
+
"@kubun/protocol": "^0.12.0",
|
|
30
|
+
"@kubun/hlc": "^0.12.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@kokuin/capability": "^0.1
|
|
34
|
-
"@kokuin/token": "^0.
|
|
35
|
-
"@kubun/db
|
|
36
|
-
"@kubun/
|
|
37
|
-
"@kubun/
|
|
38
|
-
"@kubun/
|
|
33
|
+
"@kokuin/capability": "^0.2.1",
|
|
34
|
+
"@kokuin/token": "^0.3.0",
|
|
35
|
+
"@kubun/db": "^0.12.0",
|
|
36
|
+
"@kubun/store-graph": "^0.12.0",
|
|
37
|
+
"@kubun/test-utils": "^0.12.0",
|
|
38
|
+
"@kubun/db-better-sqlite": "^0.12.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
41
42
|
"build:clean": "del lib",
|
|
42
43
|
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
43
44
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
44
45
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
45
|
-
"
|
|
46
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
46
47
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
47
|
-
"test:unit": "vitest run"
|
|
48
|
-
"test": "pnpm run test:types && pnpm run test:unit"
|
|
48
|
+
"test:unit": "vitest run"
|
|
49
49
|
}
|
|
50
50
|
}
|