@fitzzero/quickdraw-core 5.0.0 → 5.1.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/README.md +343 -49
- package/dist/{admin-BiU4LRZS.d.ts → admin-DKwkIh5i.d.ts} +25 -10
- package/dist/{adminSchemas-k5a1Ov7b.d.ts → adminSchemas-2DUA_hZ8.d.ts} +15 -9
- package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
- package/dist/{caller-DHzK3oiI.d.ts → caller-CXTFswBO.d.ts} +28 -6
- package/dist/{chunk-2XJFNVHW.js → chunk-2PWMUVC7.js} +1 -1
- package/dist/{chunk-2XJFNVHW.js.map → chunk-2PWMUVC7.js.map} +1 -1
- package/dist/{chunk-5IC5VZ7T.js → chunk-2UELNHDD.js} +3 -3
- package/dist/{chunk-XFDOGTMC.js → chunk-3674EIRT.js} +4 -4
- package/dist/{chunk-DNCAI3KY.js → chunk-4DIQHASK.js} +1 -1
- package/dist/chunk-4DIQHASK.js.map +1 -0
- package/dist/{chunk-LLLYILEP.js → chunk-A6DJBGOG.js} +411 -43
- package/dist/chunk-A6DJBGOG.js.map +1 -0
- package/dist/{chunk-B2UQ27GH.js → chunk-CDKG4TFA.js} +25 -5
- package/dist/{chunk-B2UQ27GH.js.map → chunk-CDKG4TFA.js.map} +1 -1
- package/dist/{chunk-IRGAGRVG.js → chunk-JZBCLNDU.js} +4 -4
- package/dist/{chunk-XUSN5WKU.js → chunk-KNDDCVQJ.js} +4 -4
- package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
- package/dist/chunk-RT6OILOI.js.map +1 -0
- package/dist/{chunk-7QIY2DDX.js → chunk-VJTFXHOK.js} +2 -2
- package/dist/{chunk-7QIY2DDX.js.map → chunk-VJTFXHOK.js.map} +1 -1
- package/dist/chunk-XNLWBZIG.js +9 -0
- package/dist/chunk-XNLWBZIG.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +241 -27
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +7 -7
- package/dist/client/index.js +5 -5
- package/dist/{clientTypes-B3o_99Xq.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
- package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
- package/dist/{createServer-DUacdfKl.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
- package/dist/{createTestApp-DFLXoTKm.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
- package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
- package/dist/{http--uoooIH5.d.ts → http-Djj2Inuu.d.ts} +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -7
- package/dist/index.js.map +1 -1
- package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
- package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
- package/dist/prisma/index.d.ts +4 -4
- package/dist/prisma/index.js +1 -1
- package/dist/server/auth/index.d.ts +14 -5
- package/dist/server/auth/index.js +0 -5
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/index.d.ts +120 -33
- package/dist/server/index.js +146 -193
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +51 -11
- package/dist/server/mcp/index.js +129 -8
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-n4ss-ONM.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
- package/dist/testing/client.d.ts +13 -13
- package/dist/testing/client.js +6 -6
- package/dist/testing/index.d.ts +156 -12
- package/dist/testing/index.js +1037 -22
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +5 -5
- package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-5YRQ26LF.js.map +0 -1
- package/dist/chunk-DNCAI3KY.js.map +0 -1
- package/dist/chunk-LLLYILEP.js.map +0 -1
- /package/dist/{chunk-5IC5VZ7T.js.map → chunk-2UELNHDD.js.map} +0 -0
- /package/dist/{chunk-XFDOGTMC.js.map → chunk-3674EIRT.js.map} +0 -0
- /package/dist/{chunk-IRGAGRVG.js.map → chunk-JZBCLNDU.js.map} +0 -0
- /package/dist/{chunk-XUSN5WKU.js.map → chunk-KNDDCVQJ.js.map} +0 -0
package/dist/server/index.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
accessFormProblem,
|
|
21
21
|
afterCursor,
|
|
22
22
|
allowedScopes,
|
|
23
|
+
anyOf,
|
|
24
|
+
anyOfPolicies,
|
|
23
25
|
applyRateLimitMiddleware,
|
|
24
26
|
checkName,
|
|
25
27
|
checkWhenDefined,
|
|
@@ -36,7 +38,6 @@ import {
|
|
|
36
38
|
definePolicy,
|
|
37
39
|
encodeCursor,
|
|
38
40
|
handlerProblem,
|
|
39
|
-
idFilter,
|
|
40
41
|
inputKeys,
|
|
41
42
|
isAccessPolicy,
|
|
42
43
|
isCustomAccess,
|
|
@@ -47,16 +48,20 @@ import {
|
|
|
47
48
|
markGrantsEditor,
|
|
48
49
|
maxLevel,
|
|
49
50
|
meetsLevel,
|
|
51
|
+
members,
|
|
50
52
|
membersWhere,
|
|
53
|
+
membershipOf,
|
|
54
|
+
narrowKinds,
|
|
51
55
|
orderByOf,
|
|
52
56
|
projectRow,
|
|
53
57
|
projectedOutput,
|
|
54
58
|
registerRuntime,
|
|
59
|
+
resolver,
|
|
55
60
|
selectWith,
|
|
56
61
|
serviceGrant,
|
|
57
62
|
strip,
|
|
58
63
|
watchAdapterClients
|
|
59
|
-
} from "../chunk-
|
|
64
|
+
} from "../chunk-A6DJBGOG.js";
|
|
60
65
|
import {
|
|
61
66
|
DevWarningError,
|
|
62
67
|
N_PLUS_ONE_STATEMENTS,
|
|
@@ -64,13 +69,13 @@ import {
|
|
|
64
69
|
currentRev,
|
|
65
70
|
formatDevWarning,
|
|
66
71
|
runBeforeAnyDispatcher
|
|
67
|
-
} from "../chunk-
|
|
72
|
+
} from "../chunk-4DIQHASK.js";
|
|
68
73
|
import {
|
|
69
74
|
crud,
|
|
70
75
|
crudSpecOf,
|
|
71
76
|
sharing,
|
|
72
77
|
sharingSpecOf
|
|
73
|
-
} from "../chunk-
|
|
78
|
+
} from "../chunk-2UELNHDD.js";
|
|
74
79
|
import "../chunk-QEKVYG5F.js";
|
|
75
80
|
import {
|
|
76
81
|
abortError,
|
|
@@ -79,7 +84,7 @@ import {
|
|
|
79
84
|
kitRuntimeOf,
|
|
80
85
|
outputSchemaOf,
|
|
81
86
|
schemaOutputOf
|
|
82
|
-
} from "../chunk-
|
|
87
|
+
} from "../chunk-2PWMUVC7.js";
|
|
83
88
|
import "../chunk-DD4SKCNO.js";
|
|
84
89
|
import {
|
|
85
90
|
recordSocketSession,
|
|
@@ -98,19 +103,19 @@ import {
|
|
|
98
103
|
adminSpecOf,
|
|
99
104
|
search,
|
|
100
105
|
searchSpecOf
|
|
101
|
-
} from "../chunk-
|
|
106
|
+
} from "../chunk-CDKG4TFA.js";
|
|
102
107
|
import {
|
|
103
108
|
ACCESS_LEVELS,
|
|
104
109
|
entityRoom,
|
|
105
110
|
isAccessLevel,
|
|
106
111
|
reservedRoomPrefix
|
|
107
|
-
} from "../chunk-
|
|
112
|
+
} from "../chunk-VJTFXHOK.js";
|
|
108
113
|
import {
|
|
109
114
|
CHANNEL_DEFAULT_RATE,
|
|
110
115
|
DEFAULT_COLLECTION_LIMIT,
|
|
111
116
|
DEFAULT_COLLECTION_MAX_LIMIT,
|
|
112
117
|
isScopedStream
|
|
113
|
-
} from "../chunk-
|
|
118
|
+
} from "../chunk-RT6OILOI.js";
|
|
114
119
|
import "../chunk-47AEXVDM.js";
|
|
115
120
|
import "../chunk-Z2LZ7IZ7.js";
|
|
116
121
|
import {
|
|
@@ -381,7 +386,7 @@ function checkChannelAccess(owner2, access, fail5) {
|
|
|
381
386
|
}
|
|
382
387
|
return Object.freeze({ service: access.service });
|
|
383
388
|
}
|
|
384
|
-
function checkChannel(service, name, def, entry, fail5) {
|
|
389
|
+
function checkChannel(service, name, def, entry, kinds, fail5) {
|
|
385
390
|
const owner2 = `channel "${name}"`;
|
|
386
391
|
const implementation = typeof entry === "function" ? { handler: entry } : entry;
|
|
387
392
|
if (!isRecord2(implementation) || typeof implementation.handler !== "function") {
|
|
@@ -403,10 +408,11 @@ function checkChannel(service, name, def, entry, fail5) {
|
|
|
403
408
|
burst: def.burst ?? Math.max(1, ratePerSecond * 2),
|
|
404
409
|
requires: compileRequires(def),
|
|
405
410
|
access: checkChannelAccess(owner2, implementation.access, fail5),
|
|
411
|
+
kinds,
|
|
406
412
|
handler: implementation.handler
|
|
407
413
|
});
|
|
408
414
|
}
|
|
409
|
-
function compileChannels(contract, value, fail5) {
|
|
415
|
+
function compileChannels(contract, value, kinds, fail5) {
|
|
410
416
|
const declared = Object.entries(contract.channels);
|
|
411
417
|
if (value === void 0 && declared.length === 0) {
|
|
412
418
|
return /* @__PURE__ */ new Map();
|
|
@@ -419,12 +425,13 @@ function compileChannels(contract, value, fail5) {
|
|
|
419
425
|
fail5(`channels has no implementation for ${missing2.map((name) => `"${name}"`).join(", ")}`);
|
|
420
426
|
}
|
|
421
427
|
const channels = /* @__PURE__ */ new Map();
|
|
428
|
+
const admitted = kinds === void 0 ? void 0 : new Set(kinds);
|
|
422
429
|
for (const [name, entry] of Object.entries(value)) {
|
|
423
430
|
const def = Object.hasOwn(contract.channels, name) ? contract.channels[name] : void 0;
|
|
424
431
|
if (def === void 0) {
|
|
425
432
|
fail5(`"${name}" is not a channel of the contract`);
|
|
426
433
|
}
|
|
427
|
-
channels.set(name, checkChannel(contract.name, name, def, entry, fail5));
|
|
434
|
+
channels.set(name, checkChannel(contract.name, name, def, entry, admitted, fail5));
|
|
428
435
|
}
|
|
429
436
|
return channels;
|
|
430
437
|
}
|
|
@@ -611,7 +618,24 @@ function checkWatchAccess(value, fail5) {
|
|
|
611
618
|
}
|
|
612
619
|
return Object.freeze({ service: value.service });
|
|
613
620
|
}
|
|
614
|
-
function
|
|
621
|
+
function checkAppKinds(value) {
|
|
622
|
+
return narrowKinds(value, void 0, (message) => {
|
|
623
|
+
throw new TypeError(`initQuickdraw: ${message}`);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function checkMethodKinds(owner2, entry, data, fail5) {
|
|
627
|
+
if (entry.kinds !== void 0 && entry.access === "public") {
|
|
628
|
+
fail5(
|
|
629
|
+
`${owner2} is "public", so kinds cannot narrow who may call it: a caller of a kind it left out would call it signed out. Leave kinds out, or give it a form that needs a principal`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
return narrowKinds(
|
|
633
|
+
entry.kinds,
|
|
634
|
+
{ kinds: data.kinds, by: "its service" },
|
|
635
|
+
(message) => fail5(`${owner2}: ${message}`)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function checkServiceData(definition, appKinds, fail5) {
|
|
615
639
|
const { model, access, versionColumn } = definition;
|
|
616
640
|
if (model !== void 0 && !isName(model)) {
|
|
617
641
|
fail5('model must be the database model the rows live in, as the client names it ("task")');
|
|
@@ -631,7 +655,8 @@ function checkServiceData(definition, fail5) {
|
|
|
631
655
|
writes: checkWrites(definition.writes, fail5),
|
|
632
656
|
affects: Object.freeze(checkAffects(definition.affects, model, fail5)),
|
|
633
657
|
versionColumn,
|
|
634
|
-
watchAccess: checkWatchAccess(definition.watchAccess, fail5)
|
|
658
|
+
watchAccess: checkWatchAccess(definition.watchAccess, fail5),
|
|
659
|
+
kinds: narrowKinds(definition.kinds, { kinds: appKinds, by: "initQuickdraw" }, fail5)
|
|
635
660
|
};
|
|
636
661
|
}
|
|
637
662
|
function checkWatchedModels(contract, name, models, data, fail5) {
|
|
@@ -667,7 +692,8 @@ var DEFINITION_KEYS = /* @__PURE__ */ new Set([
|
|
|
667
692
|
"channels",
|
|
668
693
|
"streams",
|
|
669
694
|
"onRoomLeave",
|
|
670
|
-
"adminBypass"
|
|
695
|
+
"adminBypass",
|
|
696
|
+
"kinds"
|
|
671
697
|
]);
|
|
672
698
|
var METHOD_KEYS = /* @__PURE__ */ new Set([
|
|
673
699
|
"access",
|
|
@@ -676,7 +702,8 @@ var METHOD_KEYS = /* @__PURE__ */ new Set([
|
|
|
676
702
|
"ttlMs",
|
|
677
703
|
"timeoutMs",
|
|
678
704
|
"version",
|
|
679
|
-
"rowless"
|
|
705
|
+
"rowless",
|
|
706
|
+
"kinds"
|
|
680
707
|
]);
|
|
681
708
|
function isRecord4(value) {
|
|
682
709
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -723,7 +750,7 @@ function checkQueryOptions(owner2, entry, kind, fail5) {
|
|
|
723
750
|
fail5(`${owner2}: version must be a function of (input, ctx)`);
|
|
724
751
|
}
|
|
725
752
|
}
|
|
726
|
-
function checkMethod(contract, projections, name, entry, fail5) {
|
|
753
|
+
function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
727
754
|
const owner2 = `method "${name}"`;
|
|
728
755
|
const def = contract.methods[name];
|
|
729
756
|
if (def === void 0) {
|
|
@@ -749,6 +776,7 @@ function checkMethod(contract, projections, name, entry, fail5) {
|
|
|
749
776
|
fail5(`${owner2}: rowless must be true, or left out`);
|
|
750
777
|
}
|
|
751
778
|
checkQueryOptions(owner2, entry, def.kind, fail5);
|
|
779
|
+
const kinds = checkMethodKinds(owner2, entry, data, fail5);
|
|
752
780
|
const projection = projectedOutput(def.output, projections);
|
|
753
781
|
return Object.freeze({
|
|
754
782
|
name,
|
|
@@ -758,6 +786,7 @@ function checkMethod(contract, projections, name, entry, fail5) {
|
|
|
758
786
|
projection,
|
|
759
787
|
schemaOutput: projection === void 0 ? schemaOutputOf(def.output) : void 0,
|
|
760
788
|
access: entry.access,
|
|
789
|
+
kinds,
|
|
761
790
|
handler: entry.handler,
|
|
762
791
|
share: entry.share,
|
|
763
792
|
ttlMs: entry.ttlMs,
|
|
@@ -766,7 +795,7 @@ function checkMethod(contract, projections, name, entry, fail5) {
|
|
|
766
795
|
rowless: entry.rowless === true
|
|
767
796
|
});
|
|
768
797
|
}
|
|
769
|
-
function checkMethods(contract, projections, value, fail5) {
|
|
798
|
+
function checkMethods(contract, projections, data, value, fail5) {
|
|
770
799
|
if (!isRecord4(value)) {
|
|
771
800
|
fail5("methods must be an object with one implementation per contract method");
|
|
772
801
|
}
|
|
@@ -776,7 +805,7 @@ function checkMethods(contract, projections, value, fail5) {
|
|
|
776
805
|
}
|
|
777
806
|
const methods = {};
|
|
778
807
|
for (const [name, entry] of Object.entries(value)) {
|
|
779
|
-
methods[name] = checkMethod(contract, projections, name, entry, fail5);
|
|
808
|
+
methods[name] = checkMethod(contract, projections, data, name, entry, fail5);
|
|
780
809
|
}
|
|
781
810
|
return methods;
|
|
782
811
|
}
|
|
@@ -847,6 +876,23 @@ function checkHandlers(service, fail5) {
|
|
|
847
876
|
}
|
|
848
877
|
}
|
|
849
878
|
}
|
|
879
|
+
function createRuntime(options, adopt) {
|
|
880
|
+
if (options === void 0) {
|
|
881
|
+
return Object.freeze({ extendContext: void 0, kinds: void 0, adopt });
|
|
882
|
+
}
|
|
883
|
+
if (typeof options !== "object" || options === null) {
|
|
884
|
+
throw new TypeError("initQuickdraw: options must be an object");
|
|
885
|
+
}
|
|
886
|
+
const { context, kinds } = options;
|
|
887
|
+
if (context !== void 0 && typeof context !== "function") {
|
|
888
|
+
throw new TypeError("initQuickdraw: context must be a function of the base context");
|
|
889
|
+
}
|
|
890
|
+
return Object.freeze({
|
|
891
|
+
extendContext: context,
|
|
892
|
+
kinds: checkAppKinds(kinds),
|
|
893
|
+
adopt
|
|
894
|
+
});
|
|
895
|
+
}
|
|
850
896
|
function buildService(runtime, contract, definition) {
|
|
851
897
|
const checked2 = checkContract(contract);
|
|
852
898
|
const fail5 = (message) => {
|
|
@@ -863,7 +909,7 @@ function buildService(runtime, contract, definition) {
|
|
|
863
909
|
if (onRoomLeave !== void 0 && typeof onRoomLeave !== "function") {
|
|
864
910
|
fail5("onRoomLeave must be a function of the leave and a run context");
|
|
865
911
|
}
|
|
866
|
-
const data = checkServiceData(definition, fail5);
|
|
912
|
+
const data = checkServiceData(definition, runtime.kinds, fail5);
|
|
867
913
|
const projections = compileProjections(checked2, definition.project, fail5);
|
|
868
914
|
const collections = compileCollections(
|
|
869
915
|
checked2,
|
|
@@ -873,7 +919,7 @@ function buildService(runtime, contract, definition) {
|
|
|
873
919
|
fail5
|
|
874
920
|
);
|
|
875
921
|
checkWatches(checked2, collections, data, fail5);
|
|
876
|
-
const methods = checkMethods(checked2, projections, definition.methods, fail5);
|
|
922
|
+
const methods = checkMethods(checked2, projections, data, definition.methods, fail5);
|
|
877
923
|
checkRowForms(methods, data, fail5);
|
|
878
924
|
checkRowless(methods, data, fail5);
|
|
879
925
|
const service = Object.freeze({
|
|
@@ -884,7 +930,7 @@ function buildService(runtime, contract, definition) {
|
|
|
884
930
|
collections,
|
|
885
931
|
adminBypass,
|
|
886
932
|
methods: Object.freeze(methods),
|
|
887
|
-
channels: compileChannels(checked2, definition.channels, fail5),
|
|
933
|
+
channels: compileChannels(checked2, definition.channels, data.kinds, fail5),
|
|
888
934
|
streams: compileStreams(
|
|
889
935
|
checked2,
|
|
890
936
|
{ model: data.model, hasPolicy: data.access !== void 0 },
|
|
@@ -899,19 +945,6 @@ function buildService(runtime, contract, definition) {
|
|
|
899
945
|
}
|
|
900
946
|
|
|
901
947
|
// src/server/init.ts
|
|
902
|
-
function contextOption(options) {
|
|
903
|
-
if (options === void 0) {
|
|
904
|
-
return void 0;
|
|
905
|
-
}
|
|
906
|
-
if (typeof options !== "object" || options === null) {
|
|
907
|
-
throw new TypeError("initQuickdraw: options must be an object");
|
|
908
|
-
}
|
|
909
|
-
const { context } = options;
|
|
910
|
-
if (context !== void 0 && typeof context !== "function") {
|
|
911
|
-
throw new TypeError("initQuickdraw: context must be a function of the base context");
|
|
912
|
-
}
|
|
913
|
-
return context;
|
|
914
|
-
}
|
|
915
948
|
var NEEDS = Object.freeze({
|
|
916
949
|
"qd.collections.reset": "send through",
|
|
917
950
|
"qd.stream": "push through",
|
|
@@ -972,11 +1005,8 @@ function roomsOf(current) {
|
|
|
972
1005
|
}
|
|
973
1006
|
function initQuickdraw(...args) {
|
|
974
1007
|
let current;
|
|
975
|
-
const runtime =
|
|
976
|
-
|
|
977
|
-
adopt: (dispatcher) => {
|
|
978
|
-
current = dispatcher;
|
|
979
|
-
}
|
|
1008
|
+
const runtime = createRuntime(args[0], (dispatcher) => {
|
|
1009
|
+
current = dispatcher;
|
|
980
1010
|
});
|
|
981
1011
|
const qd = {
|
|
982
1012
|
defineService: ((contract, definition) => buildService(runtime, contract, definition)),
|
|
@@ -1084,60 +1114,6 @@ function jsonAcl(field, options = {}) {
|
|
|
1084
1114
|
return policy;
|
|
1085
1115
|
}
|
|
1086
1116
|
|
|
1087
|
-
// src/server/access/policies/members.ts
|
|
1088
|
-
function membershipOf(policy) {
|
|
1089
|
-
return policy.kind === "members" ? policy.reads.memberships[0] : void 0;
|
|
1090
|
-
}
|
|
1091
|
-
function checkLevels(levels) {
|
|
1092
|
-
if (levels === void 0) {
|
|
1093
|
-
return void 0;
|
|
1094
|
-
}
|
|
1095
|
-
const valid = typeof levels === "object" && levels !== null && !Array.isArray(levels) && Object.values(levels).every(isAccessLevel);
|
|
1096
|
-
if (!valid) {
|
|
1097
|
-
throw new TypeError(
|
|
1098
|
-
'members({ levels }): levels must map stored roles to access levels, as in { editor: "Moderate" }'
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
return Object.freeze({ ...levels });
|
|
1102
|
-
}
|
|
1103
|
-
function rolesAtLeast(read, level) {
|
|
1104
|
-
const enough = new Set(levelsAtLeast(level));
|
|
1105
|
-
if (read.levels === void 0) {
|
|
1106
|
-
return [...enough];
|
|
1107
|
-
}
|
|
1108
|
-
return Object.entries(read.levels).filter(([, mapped]) => enough.has(mapped)).map(([role]) => role);
|
|
1109
|
-
}
|
|
1110
|
-
function members(options) {
|
|
1111
|
-
if (typeof options !== "object" || options === null) {
|
|
1112
|
-
throw new TypeError("members(options): options must be { model, entry, user, level }");
|
|
1113
|
-
}
|
|
1114
|
-
const read = Object.freeze({
|
|
1115
|
-
model: checkName("members({ model })", "model", options.model),
|
|
1116
|
-
entry: checkName("members({ entry })", "entry", options.entry),
|
|
1117
|
-
user: checkName("members({ user })", "user", options.user),
|
|
1118
|
-
level: checkName("members({ level })", "level", options.level),
|
|
1119
|
-
levels: checkLevels(options.levels)
|
|
1120
|
-
});
|
|
1121
|
-
return definePolicy({
|
|
1122
|
-
kind: "members",
|
|
1123
|
-
membership: read,
|
|
1124
|
-
reads: { columns: [], memberships: [read], inherits: [], storage: true },
|
|
1125
|
-
levelsFor: (principal, ids, tools) => tools.memberships(read, principal.userId, ids),
|
|
1126
|
-
async accessWhere(principal, level, tools) {
|
|
1127
|
-
const roles = rolesAtLeast(read, level);
|
|
1128
|
-
if (roles.length === 0) {
|
|
1129
|
-
return "none";
|
|
1130
|
-
}
|
|
1131
|
-
const rows = await tools.storage.findMany(read.model, {
|
|
1132
|
-
where: { [read.user]: principal.userId, [read.level]: { in: roles } },
|
|
1133
|
-
select: { [read.entry]: true }
|
|
1134
|
-
});
|
|
1135
|
-
const entries = rows.map((row) => row[read.entry]);
|
|
1136
|
-
return idFilter(entries.filter((entry) => typeof entry === "string"));
|
|
1137
|
-
}
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1117
|
// src/server/access/policies/inherit.ts
|
|
1142
1118
|
function isContract3(value) {
|
|
1143
1119
|
return typeof value === "object" && value !== null && typeof value.name === "string" && Object.isFrozen(value);
|
|
@@ -1185,101 +1161,6 @@ function inherit(options) {
|
|
|
1185
1161
|
});
|
|
1186
1162
|
}
|
|
1187
1163
|
|
|
1188
|
-
// src/server/access/policies/anyOf.ts
|
|
1189
|
-
function unionReads(policies) {
|
|
1190
|
-
const memberships = /* @__PURE__ */ new Set();
|
|
1191
|
-
for (const policy of policies) {
|
|
1192
|
-
for (const read of policy.reads.memberships) {
|
|
1193
|
-
memberships.add(read);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return {
|
|
1197
|
-
columns: [...new Set(policies.flatMap((policy) => policy.reads.columns))],
|
|
1198
|
-
memberships: [...memberships],
|
|
1199
|
-
inherits: [...new Set(policies.flatMap((policy) => policy.reads.inherits))],
|
|
1200
|
-
parents: policies.flatMap((policy) => policy.reads.parents ?? []),
|
|
1201
|
-
storage: policies.some((policy) => policy.reads.storage)
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
var MEMBERS = /* @__PURE__ */ new WeakMap();
|
|
1205
|
-
function anyOfPolicies(policy) {
|
|
1206
|
-
return MEMBERS.get(policy);
|
|
1207
|
-
}
|
|
1208
|
-
function anyOf(...policies) {
|
|
1209
|
-
if (policies.length === 0 || !policies.every(isAccessPolicy)) {
|
|
1210
|
-
throw new TypeError("anyOf(...policies): pass one or more access policies");
|
|
1211
|
-
}
|
|
1212
|
-
const all = Object.freeze([...policies]);
|
|
1213
|
-
const combined = definePolicy({
|
|
1214
|
-
kind: "anyOf",
|
|
1215
|
-
policies: all,
|
|
1216
|
-
reads: unionReads(all),
|
|
1217
|
-
async levelsFor(principal, ids, tools) {
|
|
1218
|
-
const each = await Promise.all(all.map((policy) => policy.levelsFor(principal, ids, tools)));
|
|
1219
|
-
const levels = /* @__PURE__ */ new Map();
|
|
1220
|
-
for (const id of ids) {
|
|
1221
|
-
levels.set(
|
|
1222
|
-
id,
|
|
1223
|
-
each.reduce((level, granted) => maxLevel(level, granted.get(id)), null)
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
return levels;
|
|
1227
|
-
},
|
|
1228
|
-
async accessWhere(principal, level, tools) {
|
|
1229
|
-
const filters = await Promise.all(
|
|
1230
|
-
all.map((policy) => policy.accessWhere(principal, level, tools))
|
|
1231
|
-
);
|
|
1232
|
-
const matching = filters.filter((filter) => filter !== "none");
|
|
1233
|
-
if (matching.some((filter) => Object.keys(filter).length === 0)) {
|
|
1234
|
-
return {};
|
|
1235
|
-
}
|
|
1236
|
-
if (matching.length <= 1) {
|
|
1237
|
-
return matching[0] ?? "none";
|
|
1238
|
-
}
|
|
1239
|
-
return { OR: matching };
|
|
1240
|
-
}
|
|
1241
|
-
});
|
|
1242
|
-
MEMBERS.set(combined, all);
|
|
1243
|
-
return combined;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// src/server/access/policies/resolver.ts
|
|
1247
|
-
function levelIn(levels, id) {
|
|
1248
|
-
let level;
|
|
1249
|
-
if (levels instanceof Map) {
|
|
1250
|
-
level = levels.get(id);
|
|
1251
|
-
} else if (Object.hasOwn(levels, id)) {
|
|
1252
|
-
level = levels[id];
|
|
1253
|
-
}
|
|
1254
|
-
return isAccessLevel(level) ? level : null;
|
|
1255
|
-
}
|
|
1256
|
-
function resolver(options) {
|
|
1257
|
-
const valid = typeof options === "object" && options !== null && typeof options.levelsFor === "function" && (options.where === void 0 || typeof options.where === "function");
|
|
1258
|
-
if (!valid) {
|
|
1259
|
-
throw new TypeError("resolver({ levelsFor, where? }): levelsFor and where must be functions");
|
|
1260
|
-
}
|
|
1261
|
-
const { levelsFor, where } = options;
|
|
1262
|
-
return definePolicy({
|
|
1263
|
-
kind: "resolver",
|
|
1264
|
-
reads: { columns: [], memberships: [], inherits: [], storage: false },
|
|
1265
|
-
async levelsFor(principal, ids, tools) {
|
|
1266
|
-
const levels = await levelsFor(principal, ids, tools);
|
|
1267
|
-
if (typeof levels !== "object" || levels === null) {
|
|
1268
|
-
return levelsById(ids, () => null);
|
|
1269
|
-
}
|
|
1270
|
-
return levelsById(ids, (id) => levelIn(levels, id));
|
|
1271
|
-
},
|
|
1272
|
-
async accessWhere(principal, level, tools) {
|
|
1273
|
-
if (where === void 0) {
|
|
1274
|
-
return "none";
|
|
1275
|
-
}
|
|
1276
|
-
const filter = await where(principal, level, tools);
|
|
1277
|
-
const isFilter = typeof filter === "object" && filter !== null && !Array.isArray(filter);
|
|
1278
|
-
return isFilter ? filter : "none";
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
1164
|
// src/server/access/policies/everyone.ts
|
|
1284
1165
|
function everyone(level) {
|
|
1285
1166
|
if (!isAccessLevel(level) || level === "Public") {
|
|
@@ -2483,7 +2364,7 @@ function parseList(value) {
|
|
|
2483
2364
|
}
|
|
2484
2365
|
return Array.isArray(value) && value.every(isEntry) ? value : void 0;
|
|
2485
2366
|
}
|
|
2486
|
-
function
|
|
2367
|
+
function levelIn(entries, userId) {
|
|
2487
2368
|
return entries.filter((entry) => entry.userId === userId).reduce((level, entry) => maxLevel(level, entry.level), null);
|
|
2488
2369
|
}
|
|
2489
2370
|
function entriesOf(entries, userId) {
|
|
@@ -2792,7 +2673,7 @@ function refusal2(listed, change, model) {
|
|
|
2792
2673
|
`That user owns this ${model}: an owner's access cannot be shared, changed or taken away`
|
|
2793
2674
|
);
|
|
2794
2675
|
}
|
|
2795
|
-
if (change.listed &&
|
|
2676
|
+
if (change.listed && levelIn(listed.entries, change.userId) === null) {
|
|
2796
2677
|
return new QuickdrawError("NOT_FOUND", `This ${model} is not shared with that user`);
|
|
2797
2678
|
}
|
|
2798
2679
|
return void 0;
|
|
@@ -2819,7 +2700,7 @@ async function changeList(call, context, change) {
|
|
|
2819
2700
|
throw problem;
|
|
2820
2701
|
}
|
|
2821
2702
|
await checkLastAdmin(call, tx, listed, change);
|
|
2822
|
-
const before =
|
|
2703
|
+
const before = levelIn(listed.entries, change.userId);
|
|
2823
2704
|
if (before === change.level && entriesOf(listed.entries, change.userId) === 1) {
|
|
2824
2705
|
return sharesOf(listed.entries);
|
|
2825
2706
|
}
|
|
@@ -3335,6 +3216,53 @@ function checkOverrides(fieldOverrides, names, hidden, fail5) {
|
|
|
3335
3216
|
}
|
|
3336
3217
|
return fieldOverrides;
|
|
3337
3218
|
}
|
|
3219
|
+
function quoted(names) {
|
|
3220
|
+
return names.map((name) => `"${name}"`).join(", ");
|
|
3221
|
+
}
|
|
3222
|
+
function checkEditable(editable, declared, shown, fail5) {
|
|
3223
|
+
const named = (label, list2) => {
|
|
3224
|
+
for (const name of list2) {
|
|
3225
|
+
if (ADMIN_NEVER_WRITABLE.includes(name)) {
|
|
3226
|
+
fail5(`${label}: "${name}" is never editable; the database sets it`);
|
|
3227
|
+
}
|
|
3228
|
+
if (!shown(name)) {
|
|
3229
|
+
fail5(`${label}: "${name}" is not a field of the entity the kit shows`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
return new Set(list2);
|
|
3233
|
+
};
|
|
3234
|
+
if (editable === void 0) {
|
|
3235
|
+
return declared === void 0 ? void 0 : named("admin.contract's editable", declared);
|
|
3236
|
+
}
|
|
3237
|
+
const valid = Array.isArray(editable) && editable.every((name) => typeof name === "string") && new Set(editable).size === editable.length;
|
|
3238
|
+
if (!valid) {
|
|
3239
|
+
fail5("editable must be a list of distinct field names");
|
|
3240
|
+
}
|
|
3241
|
+
const list = named("editable", editable);
|
|
3242
|
+
if (declared !== void 0 && (declared.length !== list.size || !declared.every((name) => list.has(name)))) {
|
|
3243
|
+
fail5(
|
|
3244
|
+
`editable names ${quoted(editable)}, but admin.contract was given ${quoted(declared)}: give both halves the same fields`
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
return list;
|
|
3248
|
+
}
|
|
3249
|
+
function checkAgreement(overrides, editable, fail5) {
|
|
3250
|
+
if (editable === void 0) {
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
3254
|
+
if (override.editable === true && !editable.has(name)) {
|
|
3255
|
+
fail5(
|
|
3256
|
+
`fieldOverrides: "${name}" is editable: true, which editable leaves out; name it in editable instead`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
if (override.editable === false && editable.has(name)) {
|
|
3260
|
+
fail5(
|
|
3261
|
+
`fieldOverrides: "${name}" is editable: false, which editable names; leave it out of editable instead`
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3338
3266
|
function derived(field, grants) {
|
|
3339
3267
|
const fixed = Object.hasOwn(DEFAULT_FIELDS, field.name) ? DEFAULT_FIELDS[field.name] : void 0;
|
|
3340
3268
|
if (fixed !== void 0) {
|
|
@@ -3372,10 +3300,18 @@ function adminFieldsOf(serviceName, spec, options, fail5) {
|
|
|
3372
3300
|
fail5(`"${listed}" is hidden, so adminList may not filter or sort on it`);
|
|
3373
3301
|
}
|
|
3374
3302
|
const overrides = checkOverrides(options.fieldOverrides, names, hidden, fail5);
|
|
3303
|
+
const editable = checkEditable(
|
|
3304
|
+
options.editable,
|
|
3305
|
+
spec.editable,
|
|
3306
|
+
(name) => names.includes(name) && !hidden.has(name),
|
|
3307
|
+
fail5
|
|
3308
|
+
);
|
|
3309
|
+
checkAgreement(overrides, editable, fail5);
|
|
3375
3310
|
const fields = ordered(spec.fields).filter((field) => !hidden.has(field.name)).map(
|
|
3376
3311
|
(field) => frozenConfig({
|
|
3377
3312
|
...derived(field, grants),
|
|
3378
3313
|
...overrides[field.name],
|
|
3314
|
+
...editable === void 0 ? {} : { editable: editable.has(field.name) },
|
|
3379
3315
|
sortable: spec.sort.includes(field.name),
|
|
3380
3316
|
filterable: spec.filter.includes(field.name)
|
|
3381
3317
|
})
|
|
@@ -3701,6 +3637,7 @@ var OPTION_KEYS4 = [
|
|
|
3701
3637
|
"access",
|
|
3702
3638
|
"displayName",
|
|
3703
3639
|
"hiddenFields",
|
|
3640
|
+
"editable",
|
|
3704
3641
|
"fieldOverrides",
|
|
3705
3642
|
"rowless",
|
|
3706
3643
|
"grants",
|
|
@@ -3741,7 +3678,7 @@ function checkOptions3(options) {
|
|
|
3741
3678
|
}
|
|
3742
3679
|
if (!isRecord11(options)) {
|
|
3743
3680
|
fail4(
|
|
3744
|
-
"options must be { access?, displayName?, hiddenFields?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
|
|
3681
|
+
"options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
|
|
3745
3682
|
);
|
|
3746
3683
|
}
|
|
3747
3684
|
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS4.includes(key));
|
|
@@ -3768,13 +3705,28 @@ function checkAccess3(access, names) {
|
|
|
3768
3705
|
}
|
|
3769
3706
|
return access;
|
|
3770
3707
|
}
|
|
3708
|
+
function contractEditable(contract, specs) {
|
|
3709
|
+
const lists = /* @__PURE__ */ new Map();
|
|
3710
|
+
for (const { editable } of specs) {
|
|
3711
|
+
if (editable !== void 0) {
|
|
3712
|
+
lists.set([...editable].sort().join("\0"), editable);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
if (lists.size > 1) {
|
|
3716
|
+
fail4(
|
|
3717
|
+
`admin.contract was given two editable lists for ${contract.name}: give its write methods one`
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3720
|
+
return [...lists.values()][0];
|
|
3721
|
+
}
|
|
3771
3722
|
function fieldsOf(contract, kit, options) {
|
|
3772
3723
|
const specs = kit.map(([, spec2]) => spec2);
|
|
3773
3724
|
const [first] = specs;
|
|
3774
3725
|
const spec = {
|
|
3775
3726
|
fields: first?.fields ?? [],
|
|
3776
3727
|
filter: [...new Set(specs.flatMap((one) => one.filter))],
|
|
3777
|
-
sort: [...new Set(specs.flatMap((one) => one.sort))]
|
|
3728
|
+
sort: [...new Set(specs.flatMap((one) => one.sort))],
|
|
3729
|
+
editable: contractEditable(contract, specs)
|
|
3778
3730
|
};
|
|
3779
3731
|
return adminFieldsOf(
|
|
3780
3732
|
contract.name,
|
|
@@ -3782,6 +3734,7 @@ function fieldsOf(contract, kit, options) {
|
|
|
3782
3734
|
{
|
|
3783
3735
|
displayName: options.displayName,
|
|
3784
3736
|
hiddenFields: options.hiddenFields,
|
|
3737
|
+
editable: options.editable,
|
|
3785
3738
|
fieldOverrides: options.fieldOverrides,
|
|
3786
3739
|
grants: options.grants
|
|
3787
3740
|
},
|