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