@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/testing/index.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
accessIds,
|
|
2
3
|
createServer,
|
|
4
|
+
inputKeys,
|
|
5
|
+
isCustomAccess,
|
|
3
6
|
runtimeOf
|
|
4
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-A6DJBGOG.js";
|
|
5
8
|
import {
|
|
6
9
|
DevWarningError,
|
|
7
10
|
STRICT_WARNINGS,
|
|
8
11
|
statementsIssued
|
|
9
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-4DIQHASK.js";
|
|
10
13
|
import "../chunk-QEKVYG5F.js";
|
|
11
14
|
import {
|
|
12
15
|
createCaller,
|
|
13
16
|
lazyMembers
|
|
14
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-2PWMUVC7.js";
|
|
15
18
|
import "../chunk-DD4SKCNO.js";
|
|
16
19
|
import {
|
|
17
20
|
isPrincipal,
|
|
@@ -24,8 +27,11 @@ import {
|
|
|
24
27
|
PROTOCOL_VERSION,
|
|
25
28
|
QUICKDRAW_VERSION,
|
|
26
29
|
SERVER_EVENTS
|
|
27
|
-
} from "../chunk-
|
|
28
|
-
import
|
|
30
|
+
} from "../chunk-VJTFXHOK.js";
|
|
31
|
+
import {
|
|
32
|
+
hasJsonSchema,
|
|
33
|
+
validate
|
|
34
|
+
} from "../chunk-47AEXVDM.js";
|
|
29
35
|
import {
|
|
30
36
|
utf8ByteLength
|
|
31
37
|
} from "../chunk-Z2LZ7IZ7.js";
|
|
@@ -232,8 +238,8 @@ function addBudgetSource(source) {
|
|
|
232
238
|
sources.delete(source);
|
|
233
239
|
};
|
|
234
240
|
}
|
|
235
|
-
function recordBudgetCall(
|
|
236
|
-
recording?.push(
|
|
241
|
+
function recordBudgetCall(record2) {
|
|
242
|
+
recording?.push(record2);
|
|
237
243
|
}
|
|
238
244
|
function totalsOf(live) {
|
|
239
245
|
const counted = /* @__PURE__ */ new Set();
|
|
@@ -249,15 +255,15 @@ function totalsOf(live) {
|
|
|
249
255
|
}
|
|
250
256
|
return { statements, bytes };
|
|
251
257
|
}
|
|
252
|
-
function viaSocket(
|
|
253
|
-
return
|
|
258
|
+
function viaSocket(record2) {
|
|
259
|
+
return record2.transport === "socket" || record2.transport === "legacy";
|
|
254
260
|
}
|
|
255
|
-
function callOf(
|
|
261
|
+
function callOf(record2) {
|
|
256
262
|
return {
|
|
257
|
-
call: `${
|
|
258
|
-
statements:
|
|
259
|
-
bytes:
|
|
260
|
-
...
|
|
263
|
+
call: `${record2.service}.${record2.method}`,
|
|
264
|
+
statements: record2.sqlStatements ?? 0,
|
|
265
|
+
bytes: record2.bytes,
|
|
266
|
+
...record2.outcome === "ok" ? {} : { outcome: record2.outcome }
|
|
261
267
|
};
|
|
262
268
|
}
|
|
263
269
|
function byCodeUnit(a, b) {
|
|
@@ -270,7 +276,7 @@ function byCall(a, b) {
|
|
|
270
276
|
return byCodeUnit(a.call, b.call) || byCodeUnit(a.outcome ?? "", b.outcome ?? "") || a.statements - b.statements || a.bytes - b.bytes;
|
|
271
277
|
}
|
|
272
278
|
function budgetOf(before, after, records) {
|
|
273
|
-
const replies = records.filter((
|
|
279
|
+
const replies = records.filter((record2) => !viaSocket(record2)).reduce((sum, record2) => sum + record2.bytes, 0);
|
|
274
280
|
return {
|
|
275
281
|
statements: after.statements - before.statements,
|
|
276
282
|
bytes: after.bytes - before.bytes + replies,
|
|
@@ -455,9 +461,9 @@ function instrumentOptions(options) {
|
|
|
455
461
|
const { strictWarnings, onCall, ...rest } = options;
|
|
456
462
|
return {
|
|
457
463
|
...rest,
|
|
458
|
-
onCall: (
|
|
459
|
-
recordBudgetCall(
|
|
460
|
-
onCall?.(
|
|
464
|
+
onCall: (record2) => {
|
|
465
|
+
recordBudgetCall(record2);
|
|
466
|
+
onCall?.(record2);
|
|
461
467
|
},
|
|
462
468
|
[STRICT_WARNINGS]: strictWarnings === true && underTestRunner()
|
|
463
469
|
};
|
|
@@ -712,6 +718,601 @@ function createRecordingSink() {
|
|
|
712
718
|
};
|
|
713
719
|
}
|
|
714
720
|
|
|
721
|
+
// src/testing/accessInputs.ts
|
|
722
|
+
function isRecord(value) {
|
|
723
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
724
|
+
}
|
|
725
|
+
var MAX_DEPTH = 12;
|
|
726
|
+
var UNREPRESENTABLE_AS_ANY = Object.freeze({ unrepresentable: "any" });
|
|
727
|
+
var FORMATS = Object.freeze({
|
|
728
|
+
email: "snapshot@example.com",
|
|
729
|
+
"idn-email": "snapshot@example.com",
|
|
730
|
+
uri: "https://example.com/",
|
|
731
|
+
url: "https://example.com/",
|
|
732
|
+
"uri-reference": "https://example.com/",
|
|
733
|
+
hostname: "example.com",
|
|
734
|
+
uuid: "00000000-0000-4000-8000-000000000000",
|
|
735
|
+
guid: "00000000-0000-4000-8000-000000000000",
|
|
736
|
+
"date-time": "2026-01-01T00:00:00.000Z",
|
|
737
|
+
date: "2026-01-01",
|
|
738
|
+
time: "00:00:00",
|
|
739
|
+
duration: "P1D",
|
|
740
|
+
ipv4: "127.0.0.1",
|
|
741
|
+
ipv6: "::1",
|
|
742
|
+
cuid: "cjld2cjxh0000qzrmn831i7rn",
|
|
743
|
+
cuid2: "tz4a98xxat96iws9zmbrgj3a",
|
|
744
|
+
ulid: "01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
745
|
+
nanoid: "V1StGXR8_Z5jdHi6B-myT",
|
|
746
|
+
e164: "+15555550100"
|
|
747
|
+
});
|
|
748
|
+
var PATTERN_CANDIDATES = ["x", ...new Set(Object.values(FORMATS))];
|
|
749
|
+
function referenced(node, root) {
|
|
750
|
+
const ref = node.$ref;
|
|
751
|
+
if (typeof ref !== "string") {
|
|
752
|
+
return node;
|
|
753
|
+
}
|
|
754
|
+
if (ref === "#") {
|
|
755
|
+
return root;
|
|
756
|
+
}
|
|
757
|
+
const match = /^#\/(definitions|\$defs)\/(.+)$/.exec(ref);
|
|
758
|
+
const defs = match === null ? void 0 : root[match[1] ?? ""];
|
|
759
|
+
const target = isRecord(defs) ? defs[match?.[2] ?? ""] : void 0;
|
|
760
|
+
return isRecord(target) ? target : void 0;
|
|
761
|
+
}
|
|
762
|
+
function typeOf(node) {
|
|
763
|
+
const { type } = node;
|
|
764
|
+
if (typeof type === "string") {
|
|
765
|
+
return type;
|
|
766
|
+
}
|
|
767
|
+
if (Array.isArray(type)) {
|
|
768
|
+
const types = type.filter((each) => typeof each === "string");
|
|
769
|
+
return types.includes("null") ? "null" : types[0];
|
|
770
|
+
}
|
|
771
|
+
return isRecord(node.properties) ? "object" : void 0;
|
|
772
|
+
}
|
|
773
|
+
function isNullBranch(node) {
|
|
774
|
+
return isRecord(node) && typeOf(node) === "null";
|
|
775
|
+
}
|
|
776
|
+
function patternOf(node) {
|
|
777
|
+
if (typeof node.pattern !== "string") {
|
|
778
|
+
return void 0;
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const regex = new RegExp(node.pattern, "u");
|
|
782
|
+
return (value) => regex.test(value);
|
|
783
|
+
} catch {
|
|
784
|
+
return void 0;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function sampleString(node) {
|
|
788
|
+
const format = typeof node.format === "string" ? FORMATS[node.format] : void 0;
|
|
789
|
+
const matches2 = patternOf(node);
|
|
790
|
+
let value = format ?? "x";
|
|
791
|
+
if (matches2 !== void 0 && !matches2(value)) {
|
|
792
|
+
value = PATTERN_CANDIDATES.find(matches2) ?? value;
|
|
793
|
+
}
|
|
794
|
+
const min = typeof node.minLength === "number" ? node.minLength : 0;
|
|
795
|
+
if (value.length < min) {
|
|
796
|
+
value += "x".repeat(min - value.length);
|
|
797
|
+
}
|
|
798
|
+
return typeof node.maxLength === "number" ? value.slice(0, node.maxLength) : value;
|
|
799
|
+
}
|
|
800
|
+
function sampleNumber(node, integer) {
|
|
801
|
+
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = node;
|
|
802
|
+
let value = 0;
|
|
803
|
+
if (typeof minimum === "number" && value < minimum) {
|
|
804
|
+
value = integer ? Math.ceil(minimum) : minimum;
|
|
805
|
+
}
|
|
806
|
+
if (typeof exclusiveMinimum === "number" && value <= exclusiveMinimum) {
|
|
807
|
+
value = Math.floor(exclusiveMinimum) + 1;
|
|
808
|
+
}
|
|
809
|
+
if (typeof maximum === "number" && value > maximum) {
|
|
810
|
+
value = integer ? Math.floor(maximum) : maximum;
|
|
811
|
+
}
|
|
812
|
+
if (typeof exclusiveMaximum === "number" && value >= exclusiveMaximum) {
|
|
813
|
+
value = Math.ceil(exclusiveMaximum) - 1;
|
|
814
|
+
}
|
|
815
|
+
if (typeof multipleOf === "number" && multipleOf > 0 && value % multipleOf !== 0) {
|
|
816
|
+
value = Math.ceil(value / multipleOf) * multipleOf;
|
|
817
|
+
}
|
|
818
|
+
return value;
|
|
819
|
+
}
|
|
820
|
+
function sampleObject(node, root, depth) {
|
|
821
|
+
const properties = isRecord(node.properties) ? node.properties : {};
|
|
822
|
+
const required = Array.isArray(node.required) ? node.required : [];
|
|
823
|
+
const value = {};
|
|
824
|
+
for (const key of required) {
|
|
825
|
+
if (typeof key === "string") {
|
|
826
|
+
value[key] = sample(properties[key], root, depth + 1) ?? null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return value;
|
|
830
|
+
}
|
|
831
|
+
function sampleArray(node, root, depth) {
|
|
832
|
+
let tuple;
|
|
833
|
+
if (Array.isArray(node.prefixItems)) {
|
|
834
|
+
tuple = node.prefixItems;
|
|
835
|
+
} else if (Array.isArray(node.items)) {
|
|
836
|
+
tuple = node.items;
|
|
837
|
+
}
|
|
838
|
+
if (tuple !== void 0) {
|
|
839
|
+
return tuple.map((item) => sample(item, root, depth + 1) ?? null);
|
|
840
|
+
}
|
|
841
|
+
const length = typeof node.minItems === "number" ? node.minItems : 0;
|
|
842
|
+
return Array.from({ length }, () => sample(node.items, root, depth + 1) ?? null);
|
|
843
|
+
}
|
|
844
|
+
function sampleAll(parts, root, depth) {
|
|
845
|
+
const values = parts.map((part) => sample(part, root, depth + 1));
|
|
846
|
+
if (values.every((value) => isRecord(value) || value === void 0)) {
|
|
847
|
+
return Object.assign({}, ...values);
|
|
848
|
+
}
|
|
849
|
+
return values.find((value) => value !== void 0);
|
|
850
|
+
}
|
|
851
|
+
function sampleType(node, root, depth) {
|
|
852
|
+
switch (typeOf(node)) {
|
|
853
|
+
case "object":
|
|
854
|
+
return sampleObject(node, root, depth);
|
|
855
|
+
case "array":
|
|
856
|
+
return sampleArray(node, root, depth);
|
|
857
|
+
case "string":
|
|
858
|
+
return sampleString(node);
|
|
859
|
+
case "integer":
|
|
860
|
+
return sampleNumber(node, true);
|
|
861
|
+
case "number":
|
|
862
|
+
return sampleNumber(node, false);
|
|
863
|
+
case "boolean":
|
|
864
|
+
return false;
|
|
865
|
+
case "null":
|
|
866
|
+
return null;
|
|
867
|
+
default:
|
|
868
|
+
return void 0;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function sample(node, root, depth) {
|
|
872
|
+
const target = isRecord(node) && depth <= MAX_DEPTH ? referenced(node, root) : void 0;
|
|
873
|
+
if (target === void 0) {
|
|
874
|
+
return void 0;
|
|
875
|
+
}
|
|
876
|
+
if (Object.hasOwn(target, "const")) {
|
|
877
|
+
return target.const;
|
|
878
|
+
}
|
|
879
|
+
if (Array.isArray(target.enum) && target.enum.length > 0) {
|
|
880
|
+
return target.enum[0];
|
|
881
|
+
}
|
|
882
|
+
const branches = Array.isArray(target.anyOf) ? target.anyOf : target.oneOf;
|
|
883
|
+
if (Array.isArray(branches) && branches.length > 0) {
|
|
884
|
+
return branches.some(isNullBranch) ? null : sample(branches[0], root, depth + 1);
|
|
885
|
+
}
|
|
886
|
+
if (Array.isArray(target.allOf) && target.allOf.length > 0) {
|
|
887
|
+
return sampleAll(target.allOf, root, depth);
|
|
888
|
+
}
|
|
889
|
+
return sampleType(target, root, depth);
|
|
890
|
+
}
|
|
891
|
+
var shapes = /* @__PURE__ */ new WeakMap();
|
|
892
|
+
function jsonSchemaOf(method) {
|
|
893
|
+
if (!hasJsonSchema(method.input)) {
|
|
894
|
+
return void 0;
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
const json = method.input["~standard"].jsonSchema.input({
|
|
898
|
+
target: "draft-07",
|
|
899
|
+
libraryOptions: UNREPRESENTABLE_AS_ANY
|
|
900
|
+
});
|
|
901
|
+
return isRecord(json) ? json : void 0;
|
|
902
|
+
} catch {
|
|
903
|
+
return void 0;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function shapeOf(method) {
|
|
907
|
+
const known = shapes.get(method);
|
|
908
|
+
if (known !== void 0) {
|
|
909
|
+
return known;
|
|
910
|
+
}
|
|
911
|
+
const root = jsonSchemaOf(method);
|
|
912
|
+
const described = root === void 0 ? void 0 : referenced(root, root);
|
|
913
|
+
const shape = {
|
|
914
|
+
skeleton: root === void 0 ? void 0 : sample(root, root, 0),
|
|
915
|
+
root: root ?? {},
|
|
916
|
+
properties: isRecord(described?.properties) ? described.properties : void 0,
|
|
917
|
+
isString: described !== void 0 && typeOf(described) === "string"
|
|
918
|
+
};
|
|
919
|
+
shapes.set(method, shape);
|
|
920
|
+
return shape;
|
|
921
|
+
}
|
|
922
|
+
function rowForm(form) {
|
|
923
|
+
if (typeof form !== "object" || isCustomAccess(form)) {
|
|
924
|
+
return void 0;
|
|
925
|
+
}
|
|
926
|
+
return form.entry === void 0 && form.scope === void 0 ? void 0 : form;
|
|
927
|
+
}
|
|
928
|
+
function rowServiceOf(service, method) {
|
|
929
|
+
const form = rowForm(method.access);
|
|
930
|
+
if (form?.scope !== void 0) {
|
|
931
|
+
return form.of.name;
|
|
932
|
+
}
|
|
933
|
+
if (form !== void 0) {
|
|
934
|
+
return service.name;
|
|
935
|
+
}
|
|
936
|
+
return inputKeys(method.input)?.includes("id") === true ? service.name : void 0;
|
|
937
|
+
}
|
|
938
|
+
function mayHoldId(node) {
|
|
939
|
+
if (!isRecord(node)) {
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
const type = typeOf(node);
|
|
943
|
+
return type === void 0 || type === "string" || type === "array" || type === "null";
|
|
944
|
+
}
|
|
945
|
+
function keyRank(key) {
|
|
946
|
+
if (key === "id" || key === "ids") {
|
|
947
|
+
return 0;
|
|
948
|
+
}
|
|
949
|
+
return /Ids?$/.test(key) ? 1 : 2;
|
|
950
|
+
}
|
|
951
|
+
function selectorKeys(shape) {
|
|
952
|
+
const { properties } = shape;
|
|
953
|
+
if (properties === void 0) {
|
|
954
|
+
return shape.isString ? [] : ["id"];
|
|
955
|
+
}
|
|
956
|
+
return Object.keys(properties).filter((key) => mayHoldId(properties[key])).sort((a, b) => keyRank(a) - keyRank(b));
|
|
957
|
+
}
|
|
958
|
+
function idValues(shape, key, row) {
|
|
959
|
+
const node = shape.properties?.[key];
|
|
960
|
+
const target = isRecord(node) ? referenced(node, shape.root) : void 0;
|
|
961
|
+
return target !== void 0 && typeOf(target) === "array" ? [[row], row] : [row, [row]];
|
|
962
|
+
}
|
|
963
|
+
function rowKeys(form, shape) {
|
|
964
|
+
if (form === void 0) {
|
|
965
|
+
return ["id"];
|
|
966
|
+
}
|
|
967
|
+
if (typeof form.id === "function") {
|
|
968
|
+
return selectorKeys(shape);
|
|
969
|
+
}
|
|
970
|
+
return [form.id ?? "id"];
|
|
971
|
+
}
|
|
972
|
+
function candidates(method, row) {
|
|
973
|
+
const shape = shapeOf(method);
|
|
974
|
+
const form = rowForm(method.access);
|
|
975
|
+
const hasId = form !== void 0 || inputKeys(method.input)?.includes("id") === true;
|
|
976
|
+
if (row === void 0 || !hasId) {
|
|
977
|
+
return [shape.skeleton];
|
|
978
|
+
}
|
|
979
|
+
const base = isRecord(shape.skeleton) ? shape.skeleton : {};
|
|
980
|
+
const placed = rowKeys(form, shape).flatMap(
|
|
981
|
+
(key) => idValues(shape, key, row).map((value) => ({ ...base, [key]: value }))
|
|
982
|
+
);
|
|
983
|
+
const bare = typeof form?.id === "function" && (shape.isString || shape.properties === void 0);
|
|
984
|
+
return bare ? [...placed, row] : placed;
|
|
985
|
+
}
|
|
986
|
+
async function isValidInput(method, input) {
|
|
987
|
+
const result = await validate(method.input, input);
|
|
988
|
+
return result.issues === void 0;
|
|
989
|
+
}
|
|
990
|
+
function namesRow(form, parsed, row) {
|
|
991
|
+
try {
|
|
992
|
+
return accessIds(form, parsed).includes(row);
|
|
993
|
+
} catch {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function generatedInput(method, row) {
|
|
998
|
+
const form = rowForm(method.access);
|
|
999
|
+
for (const input of candidates(method, row)) {
|
|
1000
|
+
const result = await validate(method.input, input);
|
|
1001
|
+
if (result.issues !== void 0) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (form === void 0 || row !== void 0 && namesRow(form, result.value, row)) {
|
|
1005
|
+
return { input };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return void 0;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/testing/accessSnapshotFile.ts
|
|
1012
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1013
|
+
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1014
|
+
|
|
1015
|
+
// src/testing/accessSnapshotCompare.ts
|
|
1016
|
+
var ACCESS_SECTIONS = ["methods", "subscriptions", "collections"];
|
|
1017
|
+
var SECTION_PREFIX = Object.freeze({
|
|
1018
|
+
methods: "",
|
|
1019
|
+
subscriptions: "qd:sub ",
|
|
1020
|
+
collections: "qd:col:sub "
|
|
1021
|
+
});
|
|
1022
|
+
function only(items, other) {
|
|
1023
|
+
return [...items].filter((item) => !other.has(item)).sort();
|
|
1024
|
+
}
|
|
1025
|
+
function samePrincipal(a, b) {
|
|
1026
|
+
return a === null || b === null ? a === b : a.kind === b.kind;
|
|
1027
|
+
}
|
|
1028
|
+
function cellOf(row, principal) {
|
|
1029
|
+
return Object.hasOwn(row, principal) ? row[principal] : void 0;
|
|
1030
|
+
}
|
|
1031
|
+
function compareRow(section, row, sides, principals) {
|
|
1032
|
+
return principals.flatMap((principal) => {
|
|
1033
|
+
const before = cellOf(sides.before, principal);
|
|
1034
|
+
const after = cellOf(sides.after, principal);
|
|
1035
|
+
return before === after ? [] : [{ section, row, principal, before, after }];
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
function comparePrincipals(before, after, shared) {
|
|
1039
|
+
return shared.flatMap((name) => {
|
|
1040
|
+
const was = before.principals[name] ?? null;
|
|
1041
|
+
const is = after.principals[name] ?? null;
|
|
1042
|
+
return samePrincipal(was, is) ? [] : [{ name, before: was, after: is }];
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
function compareAccessSnapshots(before, after) {
|
|
1046
|
+
const namesBefore = new Set(Object.keys(before.principals));
|
|
1047
|
+
const namesAfter = new Set(Object.keys(after.principals));
|
|
1048
|
+
const shared = [...namesAfter].filter((name) => namesBefore.has(name)).sort();
|
|
1049
|
+
const differing = [];
|
|
1050
|
+
const rowsAdded = [];
|
|
1051
|
+
const rowsRemoved = [];
|
|
1052
|
+
for (const section of ACCESS_SECTIONS) {
|
|
1053
|
+
const rowsBefore = new Set(Object.keys(before[section]));
|
|
1054
|
+
const rowsAfter = new Set(Object.keys(after[section]));
|
|
1055
|
+
const prefix = SECTION_PREFIX[section];
|
|
1056
|
+
rowsAdded.push(...only(rowsAfter, rowsBefore).map((row) => prefix + row));
|
|
1057
|
+
rowsRemoved.push(...only(rowsBefore, rowsAfter).map((row) => prefix + row));
|
|
1058
|
+
for (const row of [...rowsAfter].filter((key) => rowsBefore.has(key)).sort()) {
|
|
1059
|
+
const sides = { before: before[section][row] ?? {}, after: after[section][row] ?? {} };
|
|
1060
|
+
differing.push(...compareRow(section, row, sides, shared));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const excludedBefore = new Set(before.excluded);
|
|
1064
|
+
const excludedAfter = new Set(after.excluded);
|
|
1065
|
+
const oneSided = (change2) => change2.before === void 0 || change2.after === void 0;
|
|
1066
|
+
return {
|
|
1067
|
+
changed: differing.filter((change2) => !oneSided(change2)),
|
|
1068
|
+
principals: comparePrincipals(before, after, shared),
|
|
1069
|
+
rowsAdded,
|
|
1070
|
+
rowsRemoved,
|
|
1071
|
+
principalsAdded: only(namesAfter, namesBefore),
|
|
1072
|
+
principalsRemoved: only(namesBefore, namesAfter),
|
|
1073
|
+
cells: differing.filter(oneSided),
|
|
1074
|
+
excludedAdded: only(excludedAfter, excludedBefore),
|
|
1075
|
+
excludedRemoved: only(excludedBefore, excludedAfter)
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function hasDifferences(comparison) {
|
|
1079
|
+
return comparison.changed.length > 0 || comparison.principals.length > 0;
|
|
1080
|
+
}
|
|
1081
|
+
function isReshaped(comparison) {
|
|
1082
|
+
return [
|
|
1083
|
+
comparison.rowsAdded,
|
|
1084
|
+
comparison.rowsRemoved,
|
|
1085
|
+
comparison.principalsAdded,
|
|
1086
|
+
comparison.principalsRemoved,
|
|
1087
|
+
comparison.cells,
|
|
1088
|
+
comparison.excludedAdded,
|
|
1089
|
+
comparison.excludedRemoved
|
|
1090
|
+
].some((list) => list.length > 0);
|
|
1091
|
+
}
|
|
1092
|
+
function denies(outcome) {
|
|
1093
|
+
return outcome === "UNAUTHENTICATED" || outcome === "FORBIDDEN";
|
|
1094
|
+
}
|
|
1095
|
+
function passes(outcome) {
|
|
1096
|
+
return outcome !== void 0 && outcome !== "VALIDATION" && !denies(outcome);
|
|
1097
|
+
}
|
|
1098
|
+
function accessFlip(change2) {
|
|
1099
|
+
if (denies(change2.before) && passes(change2.after)) {
|
|
1100
|
+
return "opens access";
|
|
1101
|
+
}
|
|
1102
|
+
if (passes(change2.before) && denies(change2.after)) {
|
|
1103
|
+
return "closes access";
|
|
1104
|
+
}
|
|
1105
|
+
return void 0;
|
|
1106
|
+
}
|
|
1107
|
+
function cellName(cell) {
|
|
1108
|
+
return `${SECTION_PREFIX[cell.section]}${cell.row} as ${cell.principal}`;
|
|
1109
|
+
}
|
|
1110
|
+
var MAX_LINES = 50;
|
|
1111
|
+
function capped(lines) {
|
|
1112
|
+
if (lines.length <= MAX_LINES) {
|
|
1113
|
+
return [...lines];
|
|
1114
|
+
}
|
|
1115
|
+
return [...lines.slice(0, MAX_LINES), ` ... and ${lines.length - MAX_LINES} more`];
|
|
1116
|
+
}
|
|
1117
|
+
function shownPrincipal(principal) {
|
|
1118
|
+
if (principal === null) {
|
|
1119
|
+
return "anonymous";
|
|
1120
|
+
}
|
|
1121
|
+
return principal.kind === null ? "no kind" : `kind ${principal.kind}`;
|
|
1122
|
+
}
|
|
1123
|
+
function flipRank(change2) {
|
|
1124
|
+
const flip = accessFlip(change2);
|
|
1125
|
+
if (flip === "opens access") {
|
|
1126
|
+
return 0;
|
|
1127
|
+
}
|
|
1128
|
+
return flip === "closes access" ? 1 : 2;
|
|
1129
|
+
}
|
|
1130
|
+
function describeDifferences(comparison) {
|
|
1131
|
+
const principals = comparison.principals.map(
|
|
1132
|
+
(change2) => ` principal ${change2.name}: ${shownPrincipal(change2.before)} \u2192 ${shownPrincipal(change2.after)}`
|
|
1133
|
+
);
|
|
1134
|
+
const cells = [...comparison.changed].sort((a, b) => flipRank(a) - flipRank(b)).map((change2) => {
|
|
1135
|
+
const flip = accessFlip(change2);
|
|
1136
|
+
const line = ` ${cellName(change2)}: ${String(change2.before)} \u2192 ${String(change2.after)}`;
|
|
1137
|
+
return flip === void 0 ? line : `${line} (${flip})`;
|
|
1138
|
+
});
|
|
1139
|
+
return [...capped(principals), ...capped(cells)];
|
|
1140
|
+
}
|
|
1141
|
+
function describeReshape(comparison) {
|
|
1142
|
+
const lines = (label, items) => capped(items.map((item) => ` ${label}: ${item}`));
|
|
1143
|
+
const cells = comparison.cells.map(
|
|
1144
|
+
(cell) => `${cellName(cell)}: ${cell.before ?? "none"} \u2192 ${cell.after ?? "none"}`
|
|
1145
|
+
);
|
|
1146
|
+
return [
|
|
1147
|
+
...lines("added", comparison.rowsAdded),
|
|
1148
|
+
...lines("removed", comparison.rowsRemoved),
|
|
1149
|
+
...lines("added principal", comparison.principalsAdded),
|
|
1150
|
+
...lines("removed principal", comparison.principalsRemoved),
|
|
1151
|
+
...lines("now excluded", comparison.excludedAdded),
|
|
1152
|
+
...lines("no longer excluded", comparison.excludedRemoved),
|
|
1153
|
+
...lines("cell", cells)
|
|
1154
|
+
];
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/testing/accessSnapshotFile.ts
|
|
1158
|
+
var ACCESS_SNAPSHOT_ENV = "QD_UPDATE_ACCESS_SNAPSHOT";
|
|
1159
|
+
function updateRequested(value = process.env[ACCESS_SNAPSHOT_ENV]) {
|
|
1160
|
+
return value === "1" || value === "true";
|
|
1161
|
+
}
|
|
1162
|
+
function accessSnapshotFileOf(testFile) {
|
|
1163
|
+
return join2(dirname2(testFile), "__access__", `${basename2(testFile)}.json`);
|
|
1164
|
+
}
|
|
1165
|
+
var claimed2 = /* @__PURE__ */ new WeakMap();
|
|
1166
|
+
var NO_RUNNER2 = {};
|
|
1167
|
+
function claimFile(path, state) {
|
|
1168
|
+
const runner = state.runner ?? NO_RUNNER2;
|
|
1169
|
+
const files = claimed2.get(runner) ?? /* @__PURE__ */ new Map();
|
|
1170
|
+
claimed2.set(runner, files);
|
|
1171
|
+
const test = state.testName ?? "";
|
|
1172
|
+
const owner = files.get(path);
|
|
1173
|
+
if (owner !== void 0 && owner !== test) {
|
|
1174
|
+
throw new TypeError(
|
|
1175
|
+
`snapshotAccessMatrix: two tests take an access snapshot into ${shownPath(path)} ("${owner}" and "${test}"); take one snapshot per test file (one test file per service, or pass { file })`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
files.set(path, test);
|
|
1179
|
+
}
|
|
1180
|
+
function snapshotPathOf(file) {
|
|
1181
|
+
const state = runnerState();
|
|
1182
|
+
const testFile = file ?? state.testPath;
|
|
1183
|
+
if (testFile === void 0) {
|
|
1184
|
+
throw new TypeError(
|
|
1185
|
+
"snapshotAccessMatrix: no test file is running to put the snapshot beside; pass { file }"
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const path = accessSnapshotFileOf(testFile);
|
|
1189
|
+
claimFile(path, state);
|
|
1190
|
+
return path;
|
|
1191
|
+
}
|
|
1192
|
+
function isRecord2(value) {
|
|
1193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1194
|
+
}
|
|
1195
|
+
function isRow(value) {
|
|
1196
|
+
return isRecord2(value) && Object.values(value).every((cell) => typeof cell === "string");
|
|
1197
|
+
}
|
|
1198
|
+
function isPrincipal2(value) {
|
|
1199
|
+
return value === null || isRecord2(value) && (value.kind === null || typeof value.kind === "string");
|
|
1200
|
+
}
|
|
1201
|
+
function isSnapshot(value) {
|
|
1202
|
+
if (!isRecord2(value) || value.version !== 1 || !isRecord2(value.principals)) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
const { principals, excluded } = value;
|
|
1206
|
+
return ACCESS_SECTIONS.every((section) => {
|
|
1207
|
+
const rows = value[section];
|
|
1208
|
+
return isRecord2(rows) && Object.values(rows).every(isRow);
|
|
1209
|
+
}) && Object.values(principals).every(isPrincipal2) && Array.isArray(excluded) && excluded.every((name) => typeof name === "string");
|
|
1210
|
+
}
|
|
1211
|
+
function readAccessSnapshot(path) {
|
|
1212
|
+
if (!existsSync2(path)) {
|
|
1213
|
+
return void 0;
|
|
1214
|
+
}
|
|
1215
|
+
let parsed;
|
|
1216
|
+
try {
|
|
1217
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1218
|
+
} catch {
|
|
1219
|
+
parsed = void 0;
|
|
1220
|
+
}
|
|
1221
|
+
if (!isSnapshot(parsed)) {
|
|
1222
|
+
throw new Error(
|
|
1223
|
+
`snapshotAccessMatrix: ${shownPath(path)} is not an access snapshot ({ "version": 1, "principals", "methods", "subscriptions", "collections", "excluded" }); delete it to record the matrix again`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
return parsed;
|
|
1227
|
+
}
|
|
1228
|
+
function sortKeys(value) {
|
|
1229
|
+
if (Array.isArray(value)) {
|
|
1230
|
+
return value.map(sortKeys);
|
|
1231
|
+
}
|
|
1232
|
+
if (!isRecord2(value)) {
|
|
1233
|
+
return value;
|
|
1234
|
+
}
|
|
1235
|
+
return Object.fromEntries(
|
|
1236
|
+
Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
function writeAccessSnapshot(path, snapshot) {
|
|
1240
|
+
const sorted = { ...snapshot, excluded: [...snapshot.excluded].sort() };
|
|
1241
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1242
|
+
writeFileSync2(path, `${JSON.stringify(sortKeys(sorted), null, 2)}
|
|
1243
|
+
`);
|
|
1244
|
+
}
|
|
1245
|
+
function plural(count, one, many) {
|
|
1246
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
1247
|
+
}
|
|
1248
|
+
function inconclusiveNote(inconclusive) {
|
|
1249
|
+
if (inconclusive.length === 0) {
|
|
1250
|
+
return [];
|
|
1251
|
+
}
|
|
1252
|
+
return [
|
|
1253
|
+
`${plural(inconclusive.length, "cell was", "cells were")} never called (VALIDATION): no input passed the method's input schema; give their inputs with inputs.`
|
|
1254
|
+
];
|
|
1255
|
+
}
|
|
1256
|
+
function differencesError(shown, comparison, inconclusive) {
|
|
1257
|
+
const counts = [
|
|
1258
|
+
...comparison.changed.length > 0 ? [plural(comparison.changed.length, "cell differs", "cells differ")] : [],
|
|
1259
|
+
...comparison.principals.length > 0 ? [plural(comparison.principals.length, "principal differs", "principals differ")] : []
|
|
1260
|
+
];
|
|
1261
|
+
return new Error(
|
|
1262
|
+
[
|
|
1263
|
+
`snapshotAccessMatrix: ${counts.join(" and ")} from the access snapshot (${shown}):`,
|
|
1264
|
+
...describeDifferences(comparison),
|
|
1265
|
+
...isReshaped(comparison) ? describeReshape(comparison) : [],
|
|
1266
|
+
`If every change is meant, run the tests again with ${ACCESS_SNAPSHOT_ENV}=1, review the file and commit it.`,
|
|
1267
|
+
...inconclusiveNote(inconclusive)
|
|
1268
|
+
].join("\n")
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
function settleSnapshot(path, snapshot, inconclusive) {
|
|
1272
|
+
const shown = shownPath(path);
|
|
1273
|
+
const ci = underCi();
|
|
1274
|
+
const update = updateRequested();
|
|
1275
|
+
if (update && ci) {
|
|
1276
|
+
throw new Error(
|
|
1277
|
+
`snapshotAccessMatrix: ${ACCESS_SNAPSHOT_ENV} is set under CI, where an access snapshot is only compared: rewrite ${shown} locally, review it and commit it`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
const stored = readAccessSnapshot(path);
|
|
1281
|
+
if (stored === void 0) {
|
|
1282
|
+
if (ci) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
[
|
|
1285
|
+
`snapshotAccessMatrix: there is no access snapshot at ${shown}, and under CI none is written: a snapshot that writes itself pins nothing.`,
|
|
1286
|
+
"Run the tests locally to record it, review it and commit it.",
|
|
1287
|
+
...inconclusiveNote(inconclusive)
|
|
1288
|
+
].join("\n")
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
writeAccessSnapshot(path, snapshot);
|
|
1292
|
+
return "written";
|
|
1293
|
+
}
|
|
1294
|
+
const comparison = compareAccessSnapshots(stored, snapshot);
|
|
1295
|
+
const differs = hasDifferences(comparison);
|
|
1296
|
+
if (!differs && !isReshaped(comparison)) {
|
|
1297
|
+
return "unchanged";
|
|
1298
|
+
}
|
|
1299
|
+
if (differs && !update) {
|
|
1300
|
+
throw differencesError(shown, comparison, inconclusive);
|
|
1301
|
+
}
|
|
1302
|
+
if (ci) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
[
|
|
1305
|
+
`snapshotAccessMatrix: the access matrix has other cells than its snapshot (${shown}); rerun locally to accept:`,
|
|
1306
|
+
...describeReshape(comparison),
|
|
1307
|
+
"Run the tests without CI to write the snapshot, review it and commit it.",
|
|
1308
|
+
...inconclusiveNote(inconclusive)
|
|
1309
|
+
].join("\n")
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
writeAccessSnapshot(path, snapshot);
|
|
1313
|
+
return "updated";
|
|
1314
|
+
}
|
|
1315
|
+
|
|
715
1316
|
// src/testing/accessMatrix.ts
|
|
716
1317
|
var ANONYMOUS = "anonymous";
|
|
717
1318
|
function matches(expected, actual, anonymous) {
|
|
@@ -733,9 +1334,10 @@ async function outcomeOf(call) {
|
|
|
733
1334
|
}
|
|
734
1335
|
function report(serviceName, cells) {
|
|
735
1336
|
const failed = cells.filter((cell) => !cell.pass);
|
|
736
|
-
const lines = failed.map(
|
|
737
|
-
|
|
738
|
-
|
|
1337
|
+
const lines = failed.map((cell) => {
|
|
1338
|
+
const kind = cell.kind === void 0 ? "" : ` (kind ${cell.kind})`;
|
|
1339
|
+
return ` ${cell.case} as ${cell.principal}${kind}: expected ${cell.expected}, got ${cell.actual}`;
|
|
1340
|
+
});
|
|
739
1341
|
return `describeAccessMatrix(${serviceName}): ${failed.length} of ${cells.length} cells differ
|
|
740
1342
|
${lines.join("\n")}`;
|
|
741
1343
|
}
|
|
@@ -789,7 +1391,14 @@ async function describeAccessMatrix(app, options) {
|
|
|
789
1391
|
const made = typeof input === "function" ? await input({ name, principal }) : input;
|
|
790
1392
|
const actual = await outcomeOf(() => method(made));
|
|
791
1393
|
const pass = matches(expected, actual, principal === null);
|
|
792
|
-
cells.push({
|
|
1394
|
+
cells.push({
|
|
1395
|
+
case: entry.label ?? entry.method,
|
|
1396
|
+
principal: name,
|
|
1397
|
+
kind: principal?.kind,
|
|
1398
|
+
expected,
|
|
1399
|
+
actual,
|
|
1400
|
+
pass
|
|
1401
|
+
});
|
|
793
1402
|
}
|
|
794
1403
|
}
|
|
795
1404
|
} finally {
|
|
@@ -800,11 +1409,416 @@ async function describeAccessMatrix(app, options) {
|
|
|
800
1409
|
}
|
|
801
1410
|
return { cells };
|
|
802
1411
|
}
|
|
1412
|
+
|
|
1413
|
+
// src/testing/accessSnapshotPlan.ts
|
|
1414
|
+
function isRecord3(value) {
|
|
1415
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1416
|
+
}
|
|
1417
|
+
function fail(message) {
|
|
1418
|
+
throw new TypeError(`snapshotAccessMatrix: ${message}`);
|
|
1419
|
+
}
|
|
1420
|
+
function byName(a, b) {
|
|
1421
|
+
if (a === b) {
|
|
1422
|
+
return 0;
|
|
1423
|
+
}
|
|
1424
|
+
return a < b ? -1 : 1;
|
|
1425
|
+
}
|
|
1426
|
+
function keyOf(base, variant) {
|
|
1427
|
+
return variant === void 0 ? base : `${base} (${variant})`;
|
|
1428
|
+
}
|
|
1429
|
+
function readPrincipals(given, fixture) {
|
|
1430
|
+
const record2 = typeof given === "function" ? given(fixture) : given;
|
|
1431
|
+
if (!isRecord3(record2)) {
|
|
1432
|
+
fail("principals must map names to principals (null for an anonymous caller)");
|
|
1433
|
+
}
|
|
1434
|
+
const principals = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const [name, principal] of Object.entries(record2)) {
|
|
1436
|
+
if (principal !== null && !isRecord3(principal)) {
|
|
1437
|
+
fail(`principals.${name} must be a principal, or null for an anonymous caller`);
|
|
1438
|
+
}
|
|
1439
|
+
principals.set(name, principal);
|
|
1440
|
+
}
|
|
1441
|
+
if (![...principals.values()].includes(null)) {
|
|
1442
|
+
if (principals.has(ANONYMOUS)) {
|
|
1443
|
+
fail(
|
|
1444
|
+
`a principal is named "${ANONYMOUS}", the name of the anonymous caller added for you: name your anonymous caller with null, or rename that principal`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
principals.set(ANONYMOUS, null);
|
|
1448
|
+
}
|
|
1449
|
+
return principals;
|
|
1450
|
+
}
|
|
1451
|
+
function variantsIn(service, value) {
|
|
1452
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1453
|
+
return /* @__PURE__ */ new Map([[void 0, value]]);
|
|
1454
|
+
}
|
|
1455
|
+
const entries = isRecord3(value) ? Object.entries(value) : [];
|
|
1456
|
+
const ids = [];
|
|
1457
|
+
for (const [variant, id] of entries) {
|
|
1458
|
+
if (typeof id === "string" && id.length > 0) {
|
|
1459
|
+
ids.push([variant, id]);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (ids.length === 0 || ids.length !== entries.length) {
|
|
1463
|
+
fail(`rows.${service} must be a row id, or named row ids ({ own: id, other: id })`);
|
|
1464
|
+
}
|
|
1465
|
+
return new Map(ids.sort(([a], [b]) => byName(a, b)));
|
|
1466
|
+
}
|
|
1467
|
+
function readRows(given, served) {
|
|
1468
|
+
if (!isRecord3(given)) {
|
|
1469
|
+
fail("rows(fixture) must map service names to a row id, or to named row ids");
|
|
1470
|
+
}
|
|
1471
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1472
|
+
for (const [service, value] of Object.entries(given)) {
|
|
1473
|
+
if (!served.has(service)) {
|
|
1474
|
+
fail(`rows names ${service}, which the app does not serve`);
|
|
1475
|
+
}
|
|
1476
|
+
rows.set(service, variantsIn(service, value));
|
|
1477
|
+
}
|
|
1478
|
+
return rows;
|
|
1479
|
+
}
|
|
1480
|
+
function sameShape(a, b) {
|
|
1481
|
+
const names = (map) => [...map.keys()].join("\n");
|
|
1482
|
+
const rows = (of) => [...of].map(([service, ids]) => `${service}:${names(ids)}`).join("\n\n");
|
|
1483
|
+
return names(a.principals) === names(b.principals) && rows(a.rows) === rows(b.rows);
|
|
1484
|
+
}
|
|
1485
|
+
function variantsOf(rows, service) {
|
|
1486
|
+
const ids = service === void 0 ? void 0 : rows.get(service);
|
|
1487
|
+
return ids === void 0 ? [void 0] : [...ids.keys()];
|
|
1488
|
+
}
|
|
1489
|
+
function servicesOf(app, given) {
|
|
1490
|
+
const { services } = app.server.dispatcher.registry;
|
|
1491
|
+
if (given === void 0) {
|
|
1492
|
+
return [...services.values()].sort((a, b) => byName(a.name, b.name));
|
|
1493
|
+
}
|
|
1494
|
+
if (!Array.isArray(given)) {
|
|
1495
|
+
fail("services must be a list of services the app serves");
|
|
1496
|
+
}
|
|
1497
|
+
for (const service of given) {
|
|
1498
|
+
if (services.get(service.name) !== service) {
|
|
1499
|
+
fail(`the app does not serve ${service.name}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return [...new Set(given)].sort((a, b) => byName(a.name, b.name));
|
|
1503
|
+
}
|
|
1504
|
+
function excludedOf(services, given) {
|
|
1505
|
+
const known = new Set(
|
|
1506
|
+
services.flatMap(
|
|
1507
|
+
(service) => Object.keys(service.methods).map((name) => `${service.name}.${name}`)
|
|
1508
|
+
)
|
|
1509
|
+
);
|
|
1510
|
+
if (given !== void 0 && !Array.isArray(given)) {
|
|
1511
|
+
fail("exclude must be a list of methods, as <service>.<method>");
|
|
1512
|
+
}
|
|
1513
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
1514
|
+
for (const name of given ?? []) {
|
|
1515
|
+
if (typeof name !== "string" || !known.has(name)) {
|
|
1516
|
+
fail(
|
|
1517
|
+
`exclude names ${String(name)}, which is no method (<service>.<method>) of the matrix's services`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
excluded.add(name);
|
|
1521
|
+
}
|
|
1522
|
+
return excluded;
|
|
1523
|
+
}
|
|
1524
|
+
function methodRows(services, excluded, rows) {
|
|
1525
|
+
const list = [];
|
|
1526
|
+
for (const service of services) {
|
|
1527
|
+
for (const name of Object.keys(service.methods).sort(byName)) {
|
|
1528
|
+
const method = service.methods[name];
|
|
1529
|
+
const base = `${service.name}.${name}`;
|
|
1530
|
+
if (method === void 0 || excluded.has(base)) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
const rowService = rowServiceOf(service, method);
|
|
1534
|
+
for (const variant of variantsOf(rows, rowService)) {
|
|
1535
|
+
list.push({ key: keyOf(base, variant), service, method, rowService, variant });
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return list;
|
|
1540
|
+
}
|
|
1541
|
+
function subscribeRows(services, rows) {
|
|
1542
|
+
return services.filter((service) => service.model !== void 0 && service.contract.entity !== void 0).map((service) => ({
|
|
1543
|
+
service: service.name,
|
|
1544
|
+
keys: new Map(
|
|
1545
|
+
variantsOf(rows, service.name).map((variant) => [variant, keyOf(service.name, variant)])
|
|
1546
|
+
)
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1549
|
+
function scopeRows(services, rows) {
|
|
1550
|
+
const list = [];
|
|
1551
|
+
for (const service of services) {
|
|
1552
|
+
for (const collection of [...service.collections.keys()].sort(byName)) {
|
|
1553
|
+
const anchor = service.collections.get(collection)?.anchor?.name;
|
|
1554
|
+
const base = `${service.name}.${collection}`;
|
|
1555
|
+
if (anchor === void 0) {
|
|
1556
|
+
list.push({
|
|
1557
|
+
key: keyOf(base, "self"),
|
|
1558
|
+
service: service.name,
|
|
1559
|
+
collection,
|
|
1560
|
+
anchor,
|
|
1561
|
+
variant: void 0
|
|
1562
|
+
});
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
for (const variant of variantsOf(rows, anchor)) {
|
|
1566
|
+
list.push({
|
|
1567
|
+
key: keyOf(base, variant),
|
|
1568
|
+
service: service.name,
|
|
1569
|
+
collection,
|
|
1570
|
+
anchor,
|
|
1571
|
+
variant
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return list;
|
|
1577
|
+
}
|
|
1578
|
+
function checkRowsCover(plan, rows) {
|
|
1579
|
+
const needs = /* @__PURE__ */ new Map();
|
|
1580
|
+
const need = (service, what) => {
|
|
1581
|
+
if (service !== void 0 && !rows.has(service)) {
|
|
1582
|
+
needs.set(service, (needs.get(service) ?? /* @__PURE__ */ new Set()).add(what));
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
for (const row of [...plan.queries, ...plan.mutations]) {
|
|
1586
|
+
need(row.rowService, `${row.service.name}.${row.method.name}`);
|
|
1587
|
+
}
|
|
1588
|
+
for (const row of plan.subscribes) {
|
|
1589
|
+
need(row.service, `qd:sub ${row.service}`);
|
|
1590
|
+
}
|
|
1591
|
+
for (const row of plan.scopes) {
|
|
1592
|
+
need(row.anchor, `qd:col:sub ${row.service}.${row.collection}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (needs.size > 0) {
|
|
1595
|
+
const missing = [...needs].map(([service, what]) => `${service} (for ${[...what].join(", ")})`);
|
|
1596
|
+
fail(
|
|
1597
|
+
`rows names no row of ${missing.join("; ")}. Give rows(fixture) a row id of each, or named row ids ({ own: id, other: id }); leave a method out with exclude`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function planOf(services, excluded, rows) {
|
|
1602
|
+
const methods = methodRows(services, excluded, rows);
|
|
1603
|
+
const plan = {
|
|
1604
|
+
subscribes: subscribeRows(services, rows),
|
|
1605
|
+
scopes: scopeRows(services, rows),
|
|
1606
|
+
queries: methods.filter((row) => row.method.kind === "query"),
|
|
1607
|
+
mutations: methods.filter((row) => row.method.kind === "mutation"),
|
|
1608
|
+
excluded: [...excluded].sort(byName)
|
|
1609
|
+
};
|
|
1610
|
+
checkRowsCover(plan, rows);
|
|
1611
|
+
return plan;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/testing/accessSnapshot.ts
|
|
1615
|
+
var REFUSALS = /* @__PURE__ */ new Set(["UNAUTHENTICATED", "FORBIDDEN", "VALIDATION"]);
|
|
1616
|
+
var ANONYMOUS_SCOPE = "anonymous";
|
|
1617
|
+
function record(cells, key, principal, outcome) {
|
|
1618
|
+
const row = cells.get(key) ?? /* @__PURE__ */ new Map();
|
|
1619
|
+
cells.set(key, row.set(principal, outcome));
|
|
1620
|
+
}
|
|
1621
|
+
async function reset(run) {
|
|
1622
|
+
const fixture = await run.options.reset();
|
|
1623
|
+
const next = {
|
|
1624
|
+
principals: readPrincipals(run.options.principals, fixture),
|
|
1625
|
+
rows: readRows(run.options.rows(fixture), run.served)
|
|
1626
|
+
};
|
|
1627
|
+
if (!sameShape(run, next)) {
|
|
1628
|
+
fail(
|
|
1629
|
+
"principals and rows must name the same principals, services and variants after every reset"
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
run.fixture = fixture;
|
|
1633
|
+
run.principals = next.principals;
|
|
1634
|
+
run.rows = next.rows;
|
|
1635
|
+
}
|
|
1636
|
+
function rowIdOf(rows, service, variant) {
|
|
1637
|
+
return rows.get(service)?.get(variant);
|
|
1638
|
+
}
|
|
1639
|
+
async function outcomeOf2(call) {
|
|
1640
|
+
try {
|
|
1641
|
+
await call();
|
|
1642
|
+
return "ok";
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (error instanceof QuickdrawError) {
|
|
1645
|
+
return error.code;
|
|
1646
|
+
}
|
|
1647
|
+
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function outcomeOfReply(reply, event) {
|
|
1651
|
+
if (isRecord3(reply) && reply.ok === true) {
|
|
1652
|
+
return "ok";
|
|
1653
|
+
}
|
|
1654
|
+
const code = isRecord3(reply) && isRecord3(reply.e) ? reply.e.code : void 0;
|
|
1655
|
+
if (typeof code !== "string") {
|
|
1656
|
+
throw new Error(`snapshotAccessMatrix: ${event} answered ${JSON.stringify(reply)}`);
|
|
1657
|
+
}
|
|
1658
|
+
return code;
|
|
1659
|
+
}
|
|
1660
|
+
async function inputOf(run, row, name) {
|
|
1661
|
+
const id = row.rowService === void 0 ? void 0 : rowIdOf(run.rows, row.rowService, row.variant);
|
|
1662
|
+
const ref = {
|
|
1663
|
+
service: row.service.name,
|
|
1664
|
+
method: row.method.name,
|
|
1665
|
+
kind: row.method.kind,
|
|
1666
|
+
variant: row.variant,
|
|
1667
|
+
row: id,
|
|
1668
|
+
principal: name
|
|
1669
|
+
};
|
|
1670
|
+
const given = await run.options.inputs?.(ref, run.fixture);
|
|
1671
|
+
if (given !== void 0) {
|
|
1672
|
+
return await isValidInput(row.method, given) ? { value: given } : void 0;
|
|
1673
|
+
}
|
|
1674
|
+
const made = await generatedInput(row.method, id);
|
|
1675
|
+
return made === void 0 ? void 0 : { value: made.input };
|
|
1676
|
+
}
|
|
1677
|
+
function methodOf(app, row, principal) {
|
|
1678
|
+
const caller = app.as(principal);
|
|
1679
|
+
const method = caller[row.service.name]?.[row.method.name];
|
|
1680
|
+
if (method === void 0) {
|
|
1681
|
+
throw new TypeError(`snapshotAccessMatrix: ${row.service.name} has no ${row.method.name}`);
|
|
1682
|
+
}
|
|
1683
|
+
return method;
|
|
1684
|
+
}
|
|
1685
|
+
async function callCell(run, row, name) {
|
|
1686
|
+
const input = await inputOf(run, row, name);
|
|
1687
|
+
if (input === void 0) {
|
|
1688
|
+
record(run.methods, row.key, name, "VALIDATION");
|
|
1689
|
+
run.inconclusive.push(`${row.key} as ${name}`);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const method = methodOf(run.app, row, run.principals.get(name) ?? null);
|
|
1693
|
+
const outcome = await outcomeOf2(() => method(input.value));
|
|
1694
|
+
record(run.methods, row.key, name, outcome);
|
|
1695
|
+
if (row.method.kind === "mutation" && !REFUSALS.has(outcome)) {
|
|
1696
|
+
await reset(run);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
async function subscribeEntities(run, connection, name) {
|
|
1700
|
+
for (const row of run.plan.subscribes) {
|
|
1701
|
+
const variants = [...row.keys.keys()];
|
|
1702
|
+
const ids = variants.map((variant) => rowIdOf(run.rows, row.service, variant) ?? "");
|
|
1703
|
+
const reply = await emitWithAck(connection.socket, "qd:sub", { s: row.service, ids });
|
|
1704
|
+
const results = isRecord3(reply) && reply.ok === true && Array.isArray(reply.r) ? reply.r : void 0;
|
|
1705
|
+
variants.forEach((variant, index) => {
|
|
1706
|
+
const outcome = outcomeOfReply(results === void 0 ? reply : results[index], "qd:sub");
|
|
1707
|
+
record(run.subscriptions, row.keys.get(variant) ?? row.service, name, outcome);
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
async function subscribeScopes(run, connection, name) {
|
|
1712
|
+
const principal = run.principals.get(name) ?? null;
|
|
1713
|
+
for (const row of run.plan.scopes) {
|
|
1714
|
+
const scope = row.anchor === void 0 ? principal?.userId ?? ANONYMOUS_SCOPE : rowIdOf(run.rows, row.anchor, row.variant) ?? "";
|
|
1715
|
+
const frame = { s: row.service, c: row.collection, scope, limit: 1 };
|
|
1716
|
+
const reply = await emitWithAck(connection.socket, "qd:col:sub", frame);
|
|
1717
|
+
record(run.collections, row.key, name, outcomeOfReply(reply, "qd:col:sub"));
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function refusalCode(error) {
|
|
1721
|
+
const data = error instanceof Error ? error.data : void 0;
|
|
1722
|
+
const code = isRecord3(data) ? data.code : void 0;
|
|
1723
|
+
return typeof code === "string" ? code : void 0;
|
|
1724
|
+
}
|
|
1725
|
+
function refuseAll(run, name, code) {
|
|
1726
|
+
for (const row of run.plan.subscribes) {
|
|
1727
|
+
for (const key of row.keys.values()) {
|
|
1728
|
+
record(run.subscriptions, key, name, code);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
for (const row of run.plan.scopes) {
|
|
1732
|
+
record(run.collections, row.key, name, code);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function subscribeAll(run, name) {
|
|
1736
|
+
if (run.plan.subscribes.length === 0 && run.plan.scopes.length === 0) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
let connection;
|
|
1740
|
+
try {
|
|
1741
|
+
connection = await run.app.connect(run.principals.get(name) ?? null);
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
const code = refusalCode(error);
|
|
1744
|
+
if (code === void 0) {
|
|
1745
|
+
throw error;
|
|
1746
|
+
}
|
|
1747
|
+
refuseAll(run, name, code);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
try {
|
|
1751
|
+
await subscribeEntities(run, connection, name);
|
|
1752
|
+
await subscribeScopes(run, connection, name);
|
|
1753
|
+
} finally {
|
|
1754
|
+
connection.close();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function rowsOf(cells) {
|
|
1758
|
+
return Object.fromEntries([...cells].map(([key, row]) => [key, Object.fromEntries(row)]));
|
|
1759
|
+
}
|
|
1760
|
+
function snapshotOf(run, principals) {
|
|
1761
|
+
const recorded = [...principals].map(([name, principal]) => [
|
|
1762
|
+
name,
|
|
1763
|
+
principal === null ? null : { kind: principal.kind ?? null }
|
|
1764
|
+
]);
|
|
1765
|
+
return {
|
|
1766
|
+
version: 1,
|
|
1767
|
+
principals: Object.fromEntries(recorded),
|
|
1768
|
+
methods: rowsOf(run.methods),
|
|
1769
|
+
subscriptions: rowsOf(run.subscriptions),
|
|
1770
|
+
collections: rowsOf(run.collections),
|
|
1771
|
+
excluded: run.plan.excluded
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function checkOptions2(options) {
|
|
1775
|
+
if (!isRecord3(options) || typeof options.reset !== "function" || typeof options.rows !== "function") {
|
|
1776
|
+
fail("pass { principals, reset, rows }: reset() seeds the rows, rows(fixture) names them");
|
|
1777
|
+
}
|
|
1778
|
+
if (options.inputs !== void 0 && typeof options.inputs !== "function") {
|
|
1779
|
+
fail("inputs must be a function of the cell and the fixture");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function snapshotAccessMatrix(app, options) {
|
|
1783
|
+
checkOptions2(options);
|
|
1784
|
+
const path = snapshotPathOf(options.file);
|
|
1785
|
+
const generic = app;
|
|
1786
|
+
const served = generic.server.dispatcher.registry.services;
|
|
1787
|
+
const services = servicesOf(generic, options.services);
|
|
1788
|
+
const excluded = excludedOf(services, options.exclude);
|
|
1789
|
+
const fixture = await options.reset();
|
|
1790
|
+
const principals = readPrincipals(options.principals, fixture);
|
|
1791
|
+
const rows = readRows(options.rows(fixture), served);
|
|
1792
|
+
const run = {
|
|
1793
|
+
app: generic,
|
|
1794
|
+
options,
|
|
1795
|
+
served,
|
|
1796
|
+
plan: planOf(services, excluded, rows),
|
|
1797
|
+
fixture,
|
|
1798
|
+
principals,
|
|
1799
|
+
rows,
|
|
1800
|
+
methods: /* @__PURE__ */ new Map(),
|
|
1801
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
1802
|
+
collections: /* @__PURE__ */ new Map(),
|
|
1803
|
+
inconclusive: []
|
|
1804
|
+
};
|
|
1805
|
+
for (const name of principals.keys()) {
|
|
1806
|
+
await subscribeAll(run, name);
|
|
1807
|
+
for (const row of [...run.plan.queries, ...run.plan.mutations]) {
|
|
1808
|
+
await callCell(run, row, name);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
const snapshot = snapshotOf(run, principals);
|
|
1812
|
+
const change2 = settleSnapshot(path, snapshot, run.inconclusive);
|
|
1813
|
+
return { path, change: change2, snapshot, inconclusive: run.inconclusive };
|
|
1814
|
+
}
|
|
803
1815
|
export {
|
|
1816
|
+
ACCESS_SNAPSHOT_ENV,
|
|
804
1817
|
ANONYMOUS,
|
|
805
1818
|
BUDGET_BYTES_TOLERANCE,
|
|
806
1819
|
BUDGET_GROWTH_ENV,
|
|
807
1820
|
DevWarningError,
|
|
1821
|
+
accessSnapshotFileOf,
|
|
808
1822
|
budgetFileOf,
|
|
809
1823
|
createRecordingSink,
|
|
810
1824
|
createTestApp,
|
|
@@ -812,6 +1826,7 @@ export {
|
|
|
812
1826
|
emitWithAck,
|
|
813
1827
|
eventFrames,
|
|
814
1828
|
expectBudget,
|
|
1829
|
+
snapshotAccessMatrix,
|
|
815
1830
|
streamFrames,
|
|
816
1831
|
waitForEvent
|
|
817
1832
|
};
|