@fitzzero/quickdraw-core 5.0.1 → 5.1.1

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.
Files changed (60) hide show
  1. package/README.md +303 -32
  2. package/dist/{admin-CBlTY0do.d.ts → admin-DKwkIh5i.d.ts} +2 -2
  3. package/dist/{adminSchemas-zji11V74.d.ts → adminSchemas-2DUA_hZ8.d.ts} +1 -1
  4. package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
  5. package/dist/{caller-BqCYnpmC.d.ts → caller-CXTFswBO.d.ts} +27 -5
  6. package/dist/{chunk-2XJFNVHW.js → chunk-2PWMUVC7.js} +1 -1
  7. package/dist/{chunk-2XJFNVHW.js.map → chunk-2PWMUVC7.js.map} +1 -1
  8. package/dist/{chunk-O2PWC2YW.js → chunk-OECZPRPW.js} +3 -3
  9. package/dist/{chunk-3JNMVB2V.js → chunk-QCHKWIRS.js} +2 -2
  10. package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
  11. package/dist/chunk-RT6OILOI.js.map +1 -0
  12. package/dist/{chunk-NS5PUHIZ.js → chunk-RZTP4IEW.js} +130 -23
  13. package/dist/chunk-RZTP4IEW.js.map +1 -0
  14. package/dist/{chunk-HQ6WIRE2.js → chunk-SHEGLZEP.js} +4 -4
  15. package/dist/{chunk-WVIRXYI4.js → chunk-VSQBHPWH.js} +2 -2
  16. package/dist/{chunk-WVIRXYI4.js.map → chunk-VSQBHPWH.js.map} +1 -1
  17. package/dist/chunk-XNLWBZIG.js +9 -0
  18. package/dist/chunk-XNLWBZIG.js.map +1 -0
  19. package/dist/{chunk-ZACRRYWL.js → chunk-ZIVPHYRG.js} +4 -4
  20. package/dist/cli/quickdraw-docs.js +241 -27
  21. package/dist/cli/quickdraw-docs.js.map +1 -1
  22. package/dist/client/index.d.ts +7 -7
  23. package/dist/client/index.js +4 -4
  24. package/dist/{clientTypes-DuZW_8mb.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
  25. package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
  26. package/dist/{createServer-CGsJlxM8.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
  27. package/dist/{createTestApp-Cg2CZa1g.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
  28. package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
  29. package/dist/{http-D3fSFQ57.d.ts → http-Djj2Inuu.d.ts} +6 -6
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +26 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
  34. package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
  35. package/dist/prisma/index.d.ts +4 -4
  36. package/dist/server/auth/index.d.ts +3 -3
  37. package/dist/server/index.d.ts +47 -26
  38. package/dist/server/index.js +62 -36
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp/index.d.ts +51 -11
  41. package/dist/server/mcp/index.js +129 -8
  42. package/dist/server/mcp/index.js.map +1 -1
  43. package/dist/server/otel.d.ts +3 -3
  44. package/dist/{sharing-CoreezAq.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
  45. package/dist/testing/client.d.ts +13 -13
  46. package/dist/testing/client.js +5 -5
  47. package/dist/testing/index.d.ts +156 -12
  48. package/dist/testing/index.js +1036 -21
  49. package/dist/testing/index.js.map +1 -1
  50. package/dist/testing/mock.d.ts +5 -5
  51. package/dist/testing/mock.js +4 -4
  52. package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
  53. package/dist/utils/index.d.ts +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-5YRQ26LF.js.map +0 -1
  56. package/dist/chunk-NS5PUHIZ.js.map +0 -1
  57. /package/dist/{chunk-O2PWC2YW.js.map → chunk-OECZPRPW.js.map} +0 -0
  58. /package/dist/{chunk-3JNMVB2V.js.map → chunk-QCHKWIRS.js.map} +0 -0
  59. /package/dist/{chunk-HQ6WIRE2.js.map → chunk-SHEGLZEP.js.map} +0 -0
  60. /package/dist/{chunk-ZACRRYWL.js.map → chunk-ZIVPHYRG.js.map} +0 -0
@@ -1,7 +1,10 @@
1
1
  import {
2
+ accessIds,
2
3
  createServer,
4
+ inputKeys,
5
+ isCustomAccess,
3
6
  runtimeOf
4
- } from "../chunk-NS5PUHIZ.js";
7
+ } from "../chunk-RZTP4IEW.js";
5
8
  import {
6
9
  DevWarningError,
7
10
  STRICT_WARNINGS,
@@ -11,7 +14,7 @@ import "../chunk-QEKVYG5F.js";
11
14
  import {
12
15
  createCaller,
13
16
  lazyMembers
14
- } from "../chunk-2XJFNVHW.js";
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-WVIRXYI4.js";
28
- import "../chunk-47AEXVDM.js";
30
+ } from "../chunk-VSQBHPWH.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(record) {
236
- recording?.push(record);
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(record) {
253
- return record.transport === "socket" || record.transport === "legacy";
258
+ function viaSocket(record2) {
259
+ return record2.transport === "socket" || record2.transport === "legacy";
254
260
  }
255
- function callOf(record) {
261
+ function callOf(record2) {
256
262
  return {
257
- call: `${record.service}.${record.method}`,
258
- statements: record.sqlStatements ?? 0,
259
- bytes: record.bytes,
260
- ...record.outcome === "ok" ? {} : { outcome: record.outcome }
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((record) => !viaSocket(record)).reduce((sum, record) => sum + record.bytes, 0);
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: (record) => {
459
- recordBudgetCall(record);
460
- onCall?.(record);
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
- (cell) => ` ${cell.case} as ${cell.principal}: expected ${cell.expected}, got ${cell.actual}`
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({ case: entry.label ?? entry.method, principal: name, expected, actual, pass });
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
  };