@fitzzero/quickdraw-core 5.1.1 → 5.1.3
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 +1308 -219
- package/dist/{admin-DKwkIh5i.d.ts → admin-CS7JB_UH.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-29cGgrg5.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DtdYjrZ9.d.ts} +51 -21
- package/dist/{caller-CXTFswBO.d.ts → caller-BTwGl7vm.d.ts} +296 -33
- package/dist/chunk-2FDSKLBO.js +82 -0
- package/dist/chunk-2FDSKLBO.js.map +1 -0
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/{chunk-OECZPRPW.js → chunk-64I6MYBC.js} +162 -775
- package/dist/chunk-64I6MYBC.js.map +1 -0
- package/dist/chunk-7473HK24.js +17 -0
- package/dist/chunk-7473HK24.js.map +1 -0
- package/dist/chunk-7Y5ROPXL.js +33 -0
- package/dist/chunk-7Y5ROPXL.js.map +1 -0
- package/dist/chunk-A2RBQD66.js +7 -0
- package/dist/chunk-A2RBQD66.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/chunk-ASAMUGHG.js +651 -0
- package/dist/chunk-ASAMUGHG.js.map +1 -0
- package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
- package/dist/chunk-CYE4UBOB.js.map +1 -0
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.js.map +1 -0
- package/dist/chunk-DNP65TD5.js +832 -0
- package/dist/chunk-DNP65TD5.js.map +1 -0
- package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
- package/dist/chunk-DUBU3BB3.js.map +1 -0
- package/dist/chunk-GYLWEJHW.js +19 -0
- package/dist/chunk-GYLWEJHW.js.map +1 -0
- package/dist/{chunk-SHEGLZEP.js → chunk-I3JCFS7H.js} +34 -13
- package/dist/chunk-I3JCFS7H.js.map +1 -0
- package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
- package/dist/chunk-IIQ3WMJK.js.map +1 -0
- package/dist/{chunk-RZTP4IEW.js → chunk-JVTONQFI.js} +6339 -4226
- package/dist/chunk-JVTONQFI.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-KKQANRLG.js} +2 -2
- package/dist/chunk-KKQANRLG.js.map +1 -0
- package/dist/{chunk-CDKG4TFA.js → chunk-MPQAT55F.js} +38 -205
- package/dist/chunk-MPQAT55F.js.map +1 -0
- package/dist/chunk-NO4M6PO5.js +101 -0
- package/dist/chunk-NO4M6PO5.js.map +1 -0
- package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
- package/dist/chunk-NW6U45HX.js.map +1 -0
- package/dist/chunk-NX2V6SIL.js +202 -0
- package/dist/chunk-NX2V6SIL.js.map +1 -0
- package/dist/{chunk-ZIVPHYRG.js → chunk-NZHA4TZD.js} +72 -175
- package/dist/chunk-NZHA4TZD.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-OCZJ4EYV.js} +518 -27
- package/dist/chunk-OCZJ4EYV.js.map +1 -0
- package/dist/{chunk-REX5YPHX.js → chunk-PHCURHC7.js} +76 -21
- package/dist/chunk-PHCURHC7.js.map +1 -0
- package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
- package/dist/chunk-Q4OGQM5I.js.map +1 -0
- package/dist/chunk-QSU47XEX.js +13 -0
- package/dist/chunk-QSU47XEX.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-RZXJTDH5.js} +13 -2
- package/dist/chunk-RZXJTDH5.js.map +1 -0
- package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
- package/dist/chunk-SN3OZVEQ.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-SQ5VUG77.js} +28 -8
- package/dist/chunk-SQ5VUG77.js.map +1 -0
- package/dist/chunk-STH5DLQP.js +1192 -0
- package/dist/chunk-STH5DLQP.js.map +1 -0
- package/dist/{chunk-QCHKWIRS.js → chunk-VZFJTKIW.js} +13 -13
- package/dist/{chunk-QCHKWIRS.js.map → chunk-VZFJTKIW.js.map} +1 -1
- package/dist/{chunk-VSQBHPWH.js → chunk-X4ZU4QH5.js} +54 -22
- package/dist/chunk-X4ZU4QH5.js.map +1 -0
- package/dist/chunk-XD7SRDUY.js +188 -0
- package/dist/chunk-XD7SRDUY.js.map +1 -0
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/chunk-Z7I6SONO.js +825 -0
- package/dist/chunk-Z7I6SONO.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +132 -112
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/core.d.ts +387 -0
- package/dist/client/core.js +109 -0
- package/dist/client/index.d.ts +52 -203
- package/dist/client/index.js +59 -218
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-2EeQ_Et0.d.ts} +22 -11
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Bx5HOcYK.d.ts} +67 -17
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-DAMANT79.d.ts} +46 -13
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-BMBaxppV.d.ts} +216 -10
- package/dist/{http-Djj2Inuu.d.ts → http-YzLAar3w.d.ts} +76 -60
- package/dist/index.d.ts +8 -7
- package/dist/index.js +82 -25
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-Br62cnyW.d.ts} +82 -11
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/parser.js +2 -1
- package/dist/prisma/index.d.ts +14 -7
- package/dist/prisma/index.js +279 -70
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/rateLimit-5RKBFLUR.js.map +1 -0
- package/dist/remote-N7OUAGOW.js +15 -0
- package/dist/remote-N7OUAGOW.js.map +1 -0
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +277 -270
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/express/index.d.ts +9 -2
- package/dist/server/express/index.js +1 -1
- package/dist/server/index.d.ts +268 -84
- package/dist/server/index.js +664 -418
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +89 -27
- package/dist/server/mcp/index.js +281 -62
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-DVa8W-yR.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +19 -10
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +50 -13
- package/dist/testing/index.js +113 -21
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +17 -9
- package/dist/tracked-BR5b2O-U.d.ts +43 -0
- package/dist/{types-Cu0vmx7c.d.ts → types-CLRCQGnO.d.ts} +4 -3
- package/dist/types-tMagHbjy.d.ts +151 -0
- package/dist/utils/index.d.ts +16 -4
- package/dist/utils/index.js +6 -3
- package/package.json +5 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-4YO5H5ON.js.map +0 -1
- package/dist/chunk-B2P7AZ25.js.map +0 -1
- package/dist/chunk-CDKG4TFA.js.map +0 -1
- package/dist/chunk-DD4SKCNO.js.map +0 -1
- package/dist/chunk-H23KEFBA.js.map +0 -1
- package/dist/chunk-OECZPRPW.js.map +0 -1
- package/dist/chunk-QEKVYG5F.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-RZTP4IEW.js.map +0 -1
- package/dist/chunk-SHEGLZEP.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VSQBHPWH.js.map +0 -1
- package/dist/chunk-X5CWPX7H.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
- package/dist/chunk-ZIVPHYRG.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → client/core.js.map} +0 -0
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findNestedWrites
|
|
3
|
+
} from "../chunk-XH2IZVLT.js";
|
|
1
4
|
import {
|
|
2
5
|
CHANNEL_ABUSE_MULTIPLIER,
|
|
3
6
|
CHANNEL_ABUSE_WINDOW_MS,
|
|
7
|
+
DEFAULT_BULK_THRESHOLD,
|
|
4
8
|
DEFAULT_LIMITS,
|
|
5
9
|
DEFAULT_STALL_INTERVAL_MS,
|
|
6
10
|
DEFAULT_STALL_MAX_MS,
|
|
@@ -16,7 +20,6 @@ import {
|
|
|
16
20
|
RESUME_MAX_DELTAS,
|
|
17
21
|
STALL_RESOLUTION_MS,
|
|
18
22
|
STREAM_MAX_SCOPES,
|
|
19
|
-
SUBSCRIBER_LEVELS,
|
|
20
23
|
accessFormProblem,
|
|
21
24
|
afterCursor,
|
|
22
25
|
allowedScopes,
|
|
@@ -24,9 +27,8 @@ import {
|
|
|
24
27
|
anyOfPolicies,
|
|
25
28
|
applyRateLimitMiddleware,
|
|
26
29
|
checkName,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
compileProjections,
|
|
30
|
+
claimsOption,
|
|
31
|
+
compileCollections,
|
|
30
32
|
createBasicAccessEngine,
|
|
31
33
|
createDispatcher,
|
|
32
34
|
createHttpRouter,
|
|
@@ -36,32 +38,21 @@ import {
|
|
|
36
38
|
custom,
|
|
37
39
|
decodeCursor,
|
|
38
40
|
definePolicy,
|
|
41
|
+
deploymentOf,
|
|
39
42
|
encodeCursor,
|
|
40
|
-
handlerProblem,
|
|
41
|
-
inputKeys,
|
|
42
43
|
isAccessPolicy,
|
|
43
44
|
isCustomAccess,
|
|
44
|
-
isKitHandler,
|
|
45
|
-
isRowChecked,
|
|
46
|
-
levelsAtLeast,
|
|
47
45
|
levelsById,
|
|
48
|
-
markGrantsEditor,
|
|
49
|
-
maxLevel,
|
|
50
|
-
meetsLevel,
|
|
51
46
|
members,
|
|
52
47
|
membersWhere,
|
|
53
48
|
membershipOf,
|
|
54
|
-
narrowKinds,
|
|
55
49
|
orderByOf,
|
|
56
|
-
projectRow,
|
|
57
|
-
projectedOutput,
|
|
58
|
-
registerRuntime,
|
|
59
50
|
resolver,
|
|
51
|
+
rowlessProblem,
|
|
52
|
+
seedRoomHosts,
|
|
60
53
|
selectWith,
|
|
61
|
-
serviceGrant,
|
|
62
|
-
strip,
|
|
63
54
|
watchAdapterClients
|
|
64
|
-
} from "../chunk-
|
|
55
|
+
} from "../chunk-JVTONQFI.js";
|
|
65
56
|
import {
|
|
66
57
|
DevWarningError,
|
|
67
58
|
N_PLUS_ONE_STATEMENTS,
|
|
@@ -69,273 +60,102 @@ import {
|
|
|
69
60
|
currentRev,
|
|
70
61
|
formatDevWarning,
|
|
71
62
|
runBeforeAnyDispatcher
|
|
72
|
-
} from "../chunk-
|
|
63
|
+
} from "../chunk-SQ5VUG77.js";
|
|
73
64
|
import {
|
|
74
65
|
crud,
|
|
75
66
|
crudSpecOf,
|
|
76
67
|
sharing,
|
|
77
68
|
sharingSpecOf
|
|
78
|
-
} from "../chunk-
|
|
79
|
-
import "../chunk-
|
|
69
|
+
} from "../chunk-VZFJTKIW.js";
|
|
70
|
+
import "../chunk-KKQANRLG.js";
|
|
80
71
|
import {
|
|
72
|
+
SUBSCRIBER_LEVELS,
|
|
81
73
|
abortError,
|
|
82
74
|
callerGrantsOf,
|
|
75
|
+
checkWhenDefined,
|
|
76
|
+
checkWhenServed,
|
|
77
|
+
checksRowsItself,
|
|
78
|
+
compileProjections,
|
|
83
79
|
createCaller,
|
|
80
|
+
dependsOnCaller,
|
|
81
|
+
handlerProblem,
|
|
82
|
+
isCallerDependent,
|
|
84
83
|
kitRuntimeOf,
|
|
84
|
+
levelsAtLeast,
|
|
85
|
+
markGrantsEditor,
|
|
86
|
+
maxLevel,
|
|
87
|
+
meetsLevel,
|
|
88
|
+
narrowKinds,
|
|
85
89
|
outputSchemaOf,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
projectRow,
|
|
91
|
+
projectedOutput,
|
|
92
|
+
registerRuntime,
|
|
93
|
+
schemaOutputOf,
|
|
94
|
+
serviceGrant,
|
|
95
|
+
strip
|
|
96
|
+
} from "../chunk-OCZJ4EYV.js";
|
|
97
|
+
import "../chunk-RZXJTDH5.js";
|
|
89
98
|
import {
|
|
90
99
|
recordSocketSession,
|
|
91
100
|
toCallReply,
|
|
92
101
|
unreadable
|
|
93
|
-
} from "../chunk-
|
|
102
|
+
} from "../chunk-PHCURHC7.js";
|
|
94
103
|
import {
|
|
95
104
|
ANY_FIELD,
|
|
96
105
|
modelKey,
|
|
97
106
|
storageOf
|
|
98
|
-
} from "../chunk-
|
|
107
|
+
} from "../chunk-DUBU3BB3.js";
|
|
108
|
+
import {
|
|
109
|
+
search,
|
|
110
|
+
searchSpecOf
|
|
111
|
+
} from "../chunk-XD7SRDUY.js";
|
|
112
|
+
import "../chunk-4D2DOGAW.js";
|
|
113
|
+
import {
|
|
114
|
+
ACCESS_LEVELS,
|
|
115
|
+
isAccessLevel
|
|
116
|
+
} from "../chunk-AK23XSWD.js";
|
|
117
|
+
import {
|
|
118
|
+
consoleLogger,
|
|
119
|
+
errorText
|
|
120
|
+
} from "../chunk-DKE6QO26.js";
|
|
121
|
+
import "../chunk-7473HK24.js";
|
|
122
|
+
import "../chunk-2FDSKLBO.js";
|
|
99
123
|
import {
|
|
100
124
|
ADMIN_FIELD_TYPES,
|
|
101
125
|
ADMIN_NEVER_WRITABLE,
|
|
126
|
+
MAX_CURSOR_LENGTH,
|
|
102
127
|
admin,
|
|
103
128
|
adminSpecOf,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
writtenData
|
|
130
|
+
} from "../chunk-MPQAT55F.js";
|
|
131
|
+
import "../chunk-A2RBQD66.js";
|
|
107
132
|
import {
|
|
108
|
-
|
|
133
|
+
MAX_ENTITY_IDS_PER_SOCKET,
|
|
134
|
+
MAX_SCOPES_PER_SOCKET,
|
|
135
|
+
MAX_TOPICS_PER_SOCKET,
|
|
109
136
|
entityRoom,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
} from "../chunk-
|
|
137
|
+
reservedRoomPrefix,
|
|
138
|
+
socketIdShaped
|
|
139
|
+
} from "../chunk-X4ZU4QH5.js";
|
|
113
140
|
import {
|
|
114
141
|
CHANNEL_DEFAULT_RATE,
|
|
115
|
-
|
|
116
|
-
DEFAULT_COLLECTION_MAX_LIMIT,
|
|
142
|
+
isHostServed,
|
|
117
143
|
isScopedStream
|
|
118
|
-
} from "../chunk-
|
|
144
|
+
} from "../chunk-GYLWEJHW.js";
|
|
119
145
|
import "../chunk-47AEXVDM.js";
|
|
120
|
-
import "../chunk-
|
|
121
|
-
import
|
|
122
|
-
consoleLogger
|
|
123
|
-
} from "../chunk-YSWH776Y.js";
|
|
146
|
+
import "../chunk-SN3OZVEQ.js";
|
|
147
|
+
import "../chunk-7Y5ROPXL.js";
|
|
124
148
|
import {
|
|
125
149
|
QuickdrawError
|
|
126
|
-
} from "../chunk-
|
|
127
|
-
|
|
128
|
-
// src/server/access/rowless.ts
|
|
129
|
-
function checksNoRow(form) {
|
|
130
|
-
if (form === "public" || form === "authenticated") {
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
return typeof form === "object" && !isCustomAccess(form) && form.entry === void 0 && form.scope === void 0 && form.service !== void 0 && form.service !== "Admin";
|
|
134
|
-
}
|
|
135
|
-
function described(form) {
|
|
136
|
-
if (form === "public") {
|
|
137
|
-
return { code: '"public"', who: "anyone, signed in or not," };
|
|
138
|
-
}
|
|
139
|
-
if (form === "authenticated") {
|
|
140
|
-
return { code: '"authenticated"', who: "every signed-in user" };
|
|
141
|
-
}
|
|
142
|
-
const level = typeof form === "object" && !isCustomAccess(form) ? form.service : void 0;
|
|
143
|
-
return {
|
|
144
|
-
code: `{ service: "${String(level)}" }`,
|
|
145
|
-
who: `everyone with a service-wide ${String(level)} grant`
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
function suggestion(method) {
|
|
149
|
-
const { access } = method;
|
|
150
|
-
if (typeof access === "object" && !isCustomAccess(access) && access.service !== void 0) {
|
|
151
|
-
return `{ service: "${access.service}", entry: "${access.service}" }`;
|
|
152
|
-
}
|
|
153
|
-
return `{ entry: "${method.kind === "query" ? "Read" : "Moderate"}" }`;
|
|
154
|
-
}
|
|
155
|
-
function rowlessProblem(method) {
|
|
156
|
-
const { access, handler } = method;
|
|
157
|
-
if (method.rowless || !checksNoRow(access) || isRowChecked(handler)) {
|
|
158
|
-
return void 0;
|
|
159
|
-
}
|
|
160
|
-
if (inputKeys(method.input)?.includes("id") !== true) {
|
|
161
|
-
return void 0;
|
|
162
|
-
}
|
|
163
|
-
const optOut = isKitHandler(handler) ? `name it in the kit's rowless option (rowless: ["${method.name}"])` : "set rowless: true on the method";
|
|
164
|
-
const form = described(access);
|
|
165
|
-
return `method "${method.name}" takes a row id (its input has id), but its access ${form.code} checks no row: on a service with an access policy, that lets ${form.who} reach any row by its id. Give it ${suggestion(method)} so the policy decides, or, if every caller its access admits may reach any row, ${optOut}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// src/server/collections/define.ts
|
|
169
|
-
var DEFAULT_BULK_THRESHOLD = 200;
|
|
170
|
-
var OPTION_KEYS = /* @__PURE__ */ new Set(["anchor", "scopeAccess", "bulkThreshold"]);
|
|
171
|
-
function isRecord(value) {
|
|
172
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
|
-
}
|
|
174
|
-
function isContract(value) {
|
|
175
|
-
return isRecord(value) && typeof value.name === "string" && value.name.length > 0 && Object.isFrozen(value);
|
|
176
|
-
}
|
|
177
|
-
function checkOption(owner2, option, fail5) {
|
|
178
|
-
if (!isRecord(option)) {
|
|
179
|
-
fail5(`${owner2} must be { anchor: contract } or { scopeAccess: "self" }`);
|
|
180
|
-
}
|
|
181
|
-
const unknownKey = Object.keys(option).find((key) => !OPTION_KEYS.has(key));
|
|
182
|
-
if (unknownKey !== void 0) {
|
|
183
|
-
fail5(
|
|
184
|
-
`${owner2} has an unknown option "${unknownKey}"; the options are anchor, scopeAccess and bulkThreshold`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
const { anchor, scopeAccess, bulkThreshold = DEFAULT_BULK_THRESHOLD } = option;
|
|
188
|
-
if (anchor === void 0 === (scopeAccess === void 0)) {
|
|
189
|
-
fail5(
|
|
190
|
-
`${owner2} needs exactly one of anchor (the contract whose rows its scope values are ids of) and scopeAccess: "self"`
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
if (anchor !== void 0 && !isContract(anchor)) {
|
|
194
|
-
fail5(`${owner2}: anchor must be a contract from defineContract`);
|
|
195
|
-
}
|
|
196
|
-
if (scopeAccess !== void 0 && scopeAccess !== "self") {
|
|
197
|
-
fail5(
|
|
198
|
-
`${owner2}: scopeAccess must be "self", for a scope whose value is the subscriber's user id`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
if (typeof bulkThreshold !== "number" || !Number.isSafeInteger(bulkThreshold) || bulkThreshold < 1) {
|
|
202
|
-
fail5(`${owner2}: bulkThreshold must be a positive whole number`);
|
|
203
|
-
}
|
|
204
|
-
return { anchor, bulkThreshold };
|
|
205
|
-
}
|
|
206
|
-
function checkIndex(name, def, item, access, fail5) {
|
|
207
|
-
const { index, views } = def;
|
|
208
|
-
if (index === void 0) {
|
|
209
|
-
if (views !== void 0) {
|
|
210
|
-
fail5(`collection "${name}" declares views but no index; views read index rows`);
|
|
211
|
-
}
|
|
212
|
-
return void 0;
|
|
213
|
-
}
|
|
214
|
-
if (!Array.isArray(index) || !index.every((field) => typeof field === "string")) {
|
|
215
|
-
fail5(`collection "${name}": index must be a list of item field names`);
|
|
216
|
-
}
|
|
217
|
-
const owner2 = `collection "${name}": index field`;
|
|
218
|
-
const hidden = item.tiers.hidden(access);
|
|
219
|
-
for (const [position, field] of index.entries()) {
|
|
220
|
-
if (field === "id") {
|
|
221
|
-
fail5(`${owner2} "id" is not needed; every index row starts with the member's id`);
|
|
222
|
-
}
|
|
223
|
-
if (!item.keys.includes(field)) {
|
|
224
|
-
fail5(
|
|
225
|
-
`${owner2} "${field}" is not a key of its item "${item.name}"; index fields are item fields`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
if (hidden.has(field)) {
|
|
229
|
-
fail5(
|
|
230
|
-
`${owner2} "${field}" is reserved by the contract's fields for a level above the collection's access (${access}), so its items never carry it`
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
if (index.indexOf(field) !== position) {
|
|
234
|
-
fail5(`${owner2} "${field}" is named twice`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
checkIndexOrder(name, def.order, index, fail5);
|
|
238
|
-
return Object.freeze([...index]);
|
|
239
|
-
}
|
|
240
|
-
function checkOrderTiers(name, order, fields, access, fail5) {
|
|
241
|
-
for (const [column] of order) {
|
|
242
|
-
const required = Object.hasOwn(fields, column) ? fields[column] : void 0;
|
|
243
|
-
if (required !== void 0 && !meetsLevel(access, required)) {
|
|
244
|
-
fail5(
|
|
245
|
-
`collection "${name}": order column "${column}" is reserved by the contract's fields for ${required}, above the collection's access (${access}); a page's cursor and order would tell its subscribers what it holds`
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function checkIndexOrder(name, order, index, fail5) {
|
|
251
|
-
const missing2 = order.map(([column]) => column).find((c) => c !== "id" && !index.includes(c));
|
|
252
|
-
if (missing2 !== void 0) {
|
|
253
|
-
fail5(
|
|
254
|
-
`collection "${name}": order column "${missing2}" is not an index field; a client keeps the index in order by it`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function accessOf(name, def, anchor, fail5) {
|
|
259
|
-
if (anchor !== void 0) {
|
|
260
|
-
return def.access ?? "Read";
|
|
261
|
-
}
|
|
262
|
-
if (def.access !== void 0 && meetsLevel(def.access, "Moderate")) {
|
|
263
|
-
fail5(
|
|
264
|
-
`collection "${name}" has scopeAccess "self", which is authorized by the subscriber's user id and not by a level, so its access may not be above Read (it declares ${def.access})`
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
return "Read";
|
|
268
|
-
}
|
|
269
|
-
function scopeOf(def) {
|
|
270
|
-
const { scope } = def;
|
|
271
|
-
if (typeof scope === "string") {
|
|
272
|
-
return Object.freeze({ kind: "column", column: scope });
|
|
273
|
-
}
|
|
274
|
-
return Object.freeze({
|
|
275
|
-
kind: "via",
|
|
276
|
-
model: scope.model,
|
|
277
|
-
entry: scope.entry,
|
|
278
|
-
scope: scope.scope,
|
|
279
|
-
refreshEntry: scope.refreshEntry === true
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
function compileOne([name, def], contract, projections, option, fail5) {
|
|
283
|
-
if (option === void 0) {
|
|
284
|
-
fail5(
|
|
285
|
-
`collection "${name}" needs its scope's access: collections: { ${name}: { anchor: contract } }, or { scopeAccess: "self" } for a scope that is the subscriber's user id`
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
const { anchor, bulkThreshold } = checkOption(`collections.${name}`, option, fail5);
|
|
289
|
-
const item = projections.get(def.item);
|
|
290
|
-
if (item === void 0) {
|
|
291
|
-
fail5(`collection "${name}": item "${def.item}" is not a projection of the contract`);
|
|
292
|
-
}
|
|
293
|
-
const access = accessOf(name, def, anchor, fail5);
|
|
294
|
-
checkOrderTiers(name, def.order, contract.fields, access, fail5);
|
|
295
|
-
return Object.freeze({
|
|
296
|
-
name,
|
|
297
|
-
scope: scopeOf(def),
|
|
298
|
-
item,
|
|
299
|
-
where: Object.freeze({ ...def.where }),
|
|
300
|
-
order: def.order,
|
|
301
|
-
limit: def.limit ?? DEFAULT_COLLECTION_LIMIT,
|
|
302
|
-
maxLimit: def.maxLimit ?? DEFAULT_COLLECTION_MAX_LIMIT,
|
|
303
|
-
access,
|
|
304
|
-
anchor,
|
|
305
|
-
bulkThreshold,
|
|
306
|
-
index: checkIndex(name, def, item, access, fail5)
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
function compileCollections(contract, projections, model, option, fail5) {
|
|
310
|
-
if (option !== void 0 && !isRecord(option)) {
|
|
311
|
-
fail5(
|
|
312
|
-
'collections must map each collection of the contract to { anchor } or { scopeAccess: "self" }'
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
const options = option ?? {};
|
|
316
|
-
const unknownName = Object.keys(options).find(
|
|
317
|
-
(name) => !Object.hasOwn(contract.collections, name)
|
|
318
|
-
);
|
|
319
|
-
if (unknownName !== void 0) {
|
|
320
|
-
fail5(`collections names "${unknownName}", which is not a collection of the contract`);
|
|
321
|
-
}
|
|
322
|
-
const declared = Object.entries(contract.collections);
|
|
323
|
-
if (declared.length > 0 && model === void 0) {
|
|
324
|
-
fail5("collections need model: their items are rows of the service's model");
|
|
325
|
-
}
|
|
326
|
-
const compiled = /* @__PURE__ */ new Map();
|
|
327
|
-
for (const [name, def] of declared) {
|
|
328
|
-
compiled.set(name, compileOne([name, def], contract, projections, options[name], fail5));
|
|
329
|
-
}
|
|
330
|
-
return compiled;
|
|
331
|
-
}
|
|
150
|
+
} from "../chunk-Q4OGQM5I.js";
|
|
151
|
+
import "../chunk-QSU47XEX.js";
|
|
332
152
|
|
|
333
153
|
// src/server/realtime/define.ts
|
|
334
|
-
function
|
|
154
|
+
function isRecord(value) {
|
|
335
155
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
336
156
|
}
|
|
337
157
|
function compileSelector(selector) {
|
|
338
|
-
const read = typeof selector === "function" ? selector : (payload) =>
|
|
158
|
+
const read = typeof selector === "function" ? selector : (payload) => isRecord(payload) ? payload[selector] : void 0;
|
|
339
159
|
return (payload) => {
|
|
340
160
|
try {
|
|
341
161
|
const value = read(payload);
|
|
@@ -352,7 +172,7 @@ function compileRoom(room) {
|
|
|
352
172
|
const computed = compileSelector(room);
|
|
353
173
|
return (payload) => {
|
|
354
174
|
const name = computed(payload);
|
|
355
|
-
return name === void 0 || reservedRoomPrefix(name) !== void 0 ? void 0 : name;
|
|
175
|
+
return name === void 0 || reservedRoomPrefix(name) !== void 0 || socketIdShaped(name) ? void 0 : name;
|
|
356
176
|
};
|
|
357
177
|
}
|
|
358
178
|
function compileRequires(def) {
|
|
@@ -380,8 +200,8 @@ function checkChannelAccess(owner2, access, fail5) {
|
|
|
380
200
|
if (access === void 0 || access === "authenticated") {
|
|
381
201
|
return "authenticated";
|
|
382
202
|
}
|
|
383
|
-
const keys =
|
|
384
|
-
if (!
|
|
203
|
+
const keys = isRecord(access) ? Object.keys(access) : [];
|
|
204
|
+
if (!isRecord(access) || keys.length !== 1 || !isAccessLevel(access.service)) {
|
|
385
205
|
fail5(`${owner2}: access must be "authenticated" or { service: level }`);
|
|
386
206
|
}
|
|
387
207
|
return Object.freeze({ service: access.service });
|
|
@@ -389,7 +209,7 @@ function checkChannelAccess(owner2, access, fail5) {
|
|
|
389
209
|
function checkChannel(service, name, def, entry, kinds, fail5) {
|
|
390
210
|
const owner2 = `channel "${name}"`;
|
|
391
211
|
const implementation = typeof entry === "function" ? { handler: entry } : entry;
|
|
392
|
-
if (!
|
|
212
|
+
if (!isRecord(implementation) || typeof implementation.handler !== "function") {
|
|
393
213
|
fail5(`${owner2} must be a handler function or { access, handler }`);
|
|
394
214
|
}
|
|
395
215
|
const unknownKey = Object.keys(implementation).find(
|
|
@@ -408,16 +228,17 @@ function checkChannel(service, name, def, entry, kinds, fail5) {
|
|
|
408
228
|
burst: def.burst ?? Math.max(1, ratePerSecond * 2),
|
|
409
229
|
requires: compileRequires(def),
|
|
410
230
|
access: checkChannelAccess(owner2, implementation.access, fail5),
|
|
231
|
+
accessDeclared: implementation.access !== void 0,
|
|
411
232
|
kinds,
|
|
412
233
|
handler: implementation.handler
|
|
413
234
|
});
|
|
414
235
|
}
|
|
415
236
|
function compileChannels(contract, value, kinds, fail5) {
|
|
416
|
-
const declared = Object.entries(contract.channels);
|
|
237
|
+
const declared = Object.entries(contract.channels).filter(([, def]) => !isHostServed(def));
|
|
417
238
|
if (value === void 0 && declared.length === 0) {
|
|
418
239
|
return /* @__PURE__ */ new Map();
|
|
419
240
|
}
|
|
420
|
-
if (!
|
|
241
|
+
if (!isRecord(value)) {
|
|
421
242
|
fail5("channels must be an object with one implementation per contract channel");
|
|
422
243
|
}
|
|
423
244
|
const missing2 = declared.filter(([name]) => !Object.hasOwn(value, name)).map(([name]) => name);
|
|
@@ -431,6 +252,11 @@ function compileChannels(contract, value, kinds, fail5) {
|
|
|
431
252
|
if (def === void 0) {
|
|
432
253
|
fail5(`"${name}" is not a channel of the contract`);
|
|
433
254
|
}
|
|
255
|
+
if (isHostServed(def)) {
|
|
256
|
+
fail5(
|
|
257
|
+
`channel "${name}" is served by the room's host (serve: "host"): its messages never reach the server, so it takes no handler`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
434
260
|
channels.set(name, checkChannel(contract.name, name, def, entry, admitted, fail5));
|
|
435
261
|
}
|
|
436
262
|
return channels;
|
|
@@ -474,7 +300,7 @@ function checkStreamOptions(name, def, entry, fail5) {
|
|
|
474
300
|
if (entry === void 0) {
|
|
475
301
|
return { computeSeed: void 0, validate: "always" };
|
|
476
302
|
}
|
|
477
|
-
if (!
|
|
303
|
+
if (!isRecord(entry)) {
|
|
478
304
|
fail5(`${owner2} must be an object: { seed?, validate? }`);
|
|
479
305
|
}
|
|
480
306
|
const unknownKey = Object.keys(entry).find((key) => !STREAM_OPTION_KEYS.includes(key));
|
|
@@ -499,6 +325,11 @@ function checkStreamOptions(name, def, entry, fail5) {
|
|
|
499
325
|
return { computeSeed: seed, validate };
|
|
500
326
|
}
|
|
501
327
|
function checkStream(name, def, needs, options, fail5) {
|
|
328
|
+
if (isHostServed(def) && options !== void 0) {
|
|
329
|
+
fail5(
|
|
330
|
+
`streams.${name}: the stream is served by the room's host (serve: "host"), whose client pushes its items and keeps its seed: the server takes no options for it`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
502
333
|
const access = engineForm(def.access);
|
|
503
334
|
if (typeof access === "object" && "entry" in access && !needs.hasPolicy) {
|
|
504
335
|
fail5(
|
|
@@ -518,11 +349,12 @@ function checkStream(name, def, needs, options, fail5) {
|
|
|
518
349
|
...checkStreamOptions(name, def, options, fail5),
|
|
519
350
|
volatile: def.volatile === true,
|
|
520
351
|
access,
|
|
521
|
-
room: roomAccess(def.access)
|
|
352
|
+
room: roomAccess(def.access),
|
|
353
|
+
host: isHostServed(def)
|
|
522
354
|
});
|
|
523
355
|
}
|
|
524
356
|
function compileStreams(contract, needs, value, fail5) {
|
|
525
|
-
if (value !== void 0 && !
|
|
357
|
+
if (value !== void 0 && !isRecord(value)) {
|
|
526
358
|
fail5(
|
|
527
359
|
"streams must be an object of options per contract stream: { <stream>: { seed?, validate? } }"
|
|
528
360
|
);
|
|
@@ -541,14 +373,14 @@ function compileStreams(contract, needs, value, fail5) {
|
|
|
541
373
|
}
|
|
542
374
|
|
|
543
375
|
// src/server/serviceData.ts
|
|
544
|
-
function
|
|
376
|
+
function isRecord2(value) {
|
|
545
377
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
546
378
|
}
|
|
547
379
|
function isName(value) {
|
|
548
380
|
return typeof value === "string" && value.length > 0;
|
|
549
381
|
}
|
|
550
|
-
function
|
|
551
|
-
return
|
|
382
|
+
function isContract(value) {
|
|
383
|
+
return isRecord2(value) && isName(value.name) && Object.isFrozen(value);
|
|
552
384
|
}
|
|
553
385
|
function idsIn(value) {
|
|
554
386
|
if (isName(value)) {
|
|
@@ -558,7 +390,7 @@ function idsIn(value) {
|
|
|
558
390
|
}
|
|
559
391
|
function checkLink(index, entry, fail5) {
|
|
560
392
|
const owner2 = `affects[${index}]`;
|
|
561
|
-
if (!
|
|
393
|
+
if (!isRecord2(entry) || !isContract(entry.service)) {
|
|
562
394
|
fail5(`${owner2} must be { service: contract, id }`);
|
|
563
395
|
}
|
|
564
396
|
const { id, columns } = entry;
|
|
@@ -613,7 +445,7 @@ function checkWatchAccess(value, fail5) {
|
|
|
613
445
|
if (value === "public" || value === "authenticated") {
|
|
614
446
|
return value;
|
|
615
447
|
}
|
|
616
|
-
if (!
|
|
448
|
+
if (!isRecord2(value) || Object.keys(value).length !== 1 || !isAccessLevel(value.service)) {
|
|
617
449
|
fail5('watchAccess must be "public", "authenticated" or { service: level }');
|
|
618
450
|
}
|
|
619
451
|
return Object.freeze({ service: value.service });
|
|
@@ -635,6 +467,9 @@ function checkMethodKinds(owner2, entry, data, fail5) {
|
|
|
635
467
|
(message) => fail5(`${owner2}: ${message}`)
|
|
636
468
|
);
|
|
637
469
|
}
|
|
470
|
+
function checkMethodClaims(owner2, entry, fail5) {
|
|
471
|
+
return claimsOption(owner2, entry, fail5);
|
|
472
|
+
}
|
|
638
473
|
function checkServiceData(definition, appKinds, fail5) {
|
|
639
474
|
const { model, access, versionColumn } = definition;
|
|
640
475
|
if (model !== void 0 && !isName(model)) {
|
|
@@ -692,6 +527,7 @@ var DEFINITION_KEYS = /* @__PURE__ */ new Set([
|
|
|
692
527
|
"channels",
|
|
693
528
|
"streams",
|
|
694
529
|
"onRoomLeave",
|
|
530
|
+
"onHostLeave",
|
|
695
531
|
"adminBypass",
|
|
696
532
|
"kinds"
|
|
697
533
|
]);
|
|
@@ -703,9 +539,10 @@ var METHOD_KEYS = /* @__PURE__ */ new Set([
|
|
|
703
539
|
"timeoutMs",
|
|
704
540
|
"version",
|
|
705
541
|
"rowless",
|
|
706
|
-
"kinds"
|
|
542
|
+
"kinds",
|
|
543
|
+
"claims"
|
|
707
544
|
]);
|
|
708
|
-
function
|
|
545
|
+
function isRecord3(value) {
|
|
709
546
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
710
547
|
}
|
|
711
548
|
function checkKeys(value, allowed, owner2, fail5) {
|
|
@@ -720,7 +557,7 @@ function isDuration(value, max) {
|
|
|
720
557
|
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max;
|
|
721
558
|
}
|
|
722
559
|
function checkContract(contract) {
|
|
723
|
-
const valid =
|
|
560
|
+
const valid = isRecord3(contract) && typeof contract.name === "string" && contract.name.length > 0 && isRecord3(contract.methods) && Object.isFrozen(contract);
|
|
724
561
|
if (!valid) {
|
|
725
562
|
throw new TypeError("defineService: the first argument must be a contract from defineContract");
|
|
726
563
|
}
|
|
@@ -731,11 +568,16 @@ function checkShare(owner2, entry, fail5) {
|
|
|
731
568
|
if (share !== void 0 && share !== "caller" && share !== "all") {
|
|
732
569
|
fail5(`${owner2}: share must be "caller" or "all"`);
|
|
733
570
|
}
|
|
734
|
-
if (share === "all" &&
|
|
571
|
+
if (share === "all" && isRecord3(entry.access) && entry.access.kind === "custom") {
|
|
735
572
|
fail5(
|
|
736
573
|
`${owner2} has custom access, so it cannot share: "all"; its result may depend on who asks`
|
|
737
574
|
);
|
|
738
575
|
}
|
|
576
|
+
if (share === "all" && typeof entry.handler === "function" && isCallerDependent(entry.handler)) {
|
|
577
|
+
fail5(
|
|
578
|
+
`${owner2} is a kit method whose result depends on who asks, so it cannot share: "all"; share: "caller" runs one principal's identical calls once`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
739
581
|
if (ttlMs !== void 0 && (share === void 0 || !isDuration(ttlMs, MAX_TIMEOUT_MS))) {
|
|
740
582
|
fail5(`${owner2}: ttlMs needs share and must be a positive number of milliseconds`);
|
|
741
583
|
}
|
|
@@ -756,7 +598,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
756
598
|
if (def === void 0) {
|
|
757
599
|
fail5(`"${name}" is not a method of the contract`);
|
|
758
600
|
}
|
|
759
|
-
if (!
|
|
601
|
+
if (!isRecord3(entry)) {
|
|
760
602
|
fail5(`${owner2} must be { access, handler }`);
|
|
761
603
|
}
|
|
762
604
|
checkKeys(entry, METHOD_KEYS, owner2, fail5);
|
|
@@ -777,6 +619,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
777
619
|
}
|
|
778
620
|
checkQueryOptions(owner2, entry, def.kind, fail5);
|
|
779
621
|
const kinds = checkMethodKinds(owner2, entry, data, fail5);
|
|
622
|
+
const claims = checkMethodClaims(owner2, entry, fail5);
|
|
780
623
|
const projection = projectedOutput(def.output, projections);
|
|
781
624
|
return Object.freeze({
|
|
782
625
|
name,
|
|
@@ -787,6 +630,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
787
630
|
schemaOutput: projection === void 0 ? schemaOutputOf(def.output) : void 0,
|
|
788
631
|
access: entry.access,
|
|
789
632
|
kinds,
|
|
633
|
+
claims,
|
|
790
634
|
handler: entry.handler,
|
|
791
635
|
share: entry.share,
|
|
792
636
|
ttlMs: entry.ttlMs,
|
|
@@ -796,7 +640,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
796
640
|
});
|
|
797
641
|
}
|
|
798
642
|
function checkMethods(contract, projections, data, value, fail5) {
|
|
799
|
-
if (!
|
|
643
|
+
if (!isRecord3(value)) {
|
|
800
644
|
fail5("methods must be an object with one implementation per contract method");
|
|
801
645
|
}
|
|
802
646
|
const missing2 = Object.keys(contract.methods).filter((name) => !Object.hasOwn(value, name));
|
|
@@ -818,7 +662,7 @@ function checkWatches(contract, collections, data, fail5) {
|
|
|
818
662
|
if (def.kind !== "query") {
|
|
819
663
|
fail5(`method "${name}" is a mutation; only a query can watch`);
|
|
820
664
|
}
|
|
821
|
-
if (watch === "service" ||
|
|
665
|
+
if (watch === "service" || isRecord3(watch) && Object.hasOwn(watch, "service")) {
|
|
822
666
|
if (data.watchAccess === void 0) {
|
|
823
667
|
fail5(
|
|
824
668
|
`method "${name}" watches the service's topic, which is closed without watchAccess: declare who may watch it ("public", "authenticated" or { service: level })`
|
|
@@ -829,7 +673,7 @@ function checkWatches(contract, collections, data, fail5) {
|
|
|
829
673
|
}
|
|
830
674
|
continue;
|
|
831
675
|
}
|
|
832
|
-
if (!
|
|
676
|
+
if (!isRecord3(watch) || typeof watch.scope !== "function") {
|
|
833
677
|
fail5(`method "${name}": watch needs a scope function, which finds the scope from the input`);
|
|
834
678
|
}
|
|
835
679
|
if (typeof watch.collection !== "string" || !collections.has(watch.collection)) {
|
|
@@ -857,12 +701,12 @@ function checkRowForms(methods, data, fail5) {
|
|
|
857
701
|
}
|
|
858
702
|
}
|
|
859
703
|
}
|
|
860
|
-
function checkRowless(methods, data, fail5) {
|
|
704
|
+
function checkRowless(contract, methods, data, fail5) {
|
|
861
705
|
if (data.access === void 0) {
|
|
862
706
|
return;
|
|
863
707
|
}
|
|
864
708
|
for (const method of Object.values(methods)) {
|
|
865
|
-
const problem = rowlessProblem(method);
|
|
709
|
+
const problem = rowlessProblem(method, contract.name);
|
|
866
710
|
if (problem !== void 0) {
|
|
867
711
|
fail5(problem);
|
|
868
712
|
}
|
|
@@ -898,17 +742,20 @@ function buildService(runtime, contract, definition) {
|
|
|
898
742
|
const fail5 = (message) => {
|
|
899
743
|
throw new TypeError(`defineService("${checked2.name}"): ${message}`);
|
|
900
744
|
};
|
|
901
|
-
if (!
|
|
745
|
+
if (!isRecord3(definition)) {
|
|
902
746
|
fail5("the definition must be an object");
|
|
903
747
|
}
|
|
904
748
|
checkKeys(definition, DEFINITION_KEYS, "the definition", fail5);
|
|
905
|
-
const { adminBypass = true, onRoomLeave } = definition;
|
|
749
|
+
const { adminBypass = true, onRoomLeave, onHostLeave } = definition;
|
|
906
750
|
if (typeof adminBypass !== "boolean") {
|
|
907
751
|
fail5("adminBypass must be a boolean");
|
|
908
752
|
}
|
|
909
753
|
if (onRoomLeave !== void 0 && typeof onRoomLeave !== "function") {
|
|
910
754
|
fail5("onRoomLeave must be a function of the leave and a run context");
|
|
911
755
|
}
|
|
756
|
+
if (onHostLeave !== void 0 && typeof onHostLeave !== "function") {
|
|
757
|
+
fail5("onHostLeave must be a function of the leave and a run context");
|
|
758
|
+
}
|
|
912
759
|
const data = checkServiceData(definition, runtime.kinds, fail5);
|
|
913
760
|
const projections = compileProjections(checked2, definition.project, fail5);
|
|
914
761
|
const collections = compileCollections(
|
|
@@ -916,12 +763,13 @@ function buildService(runtime, contract, definition) {
|
|
|
916
763
|
projections,
|
|
917
764
|
data.model,
|
|
918
765
|
definition.collections,
|
|
919
|
-
fail5
|
|
766
|
+
fail5,
|
|
767
|
+
data.access
|
|
920
768
|
);
|
|
921
769
|
checkWatches(checked2, collections, data, fail5);
|
|
922
770
|
const methods = checkMethods(checked2, projections, data, definition.methods, fail5);
|
|
923
771
|
checkRowForms(methods, data, fail5);
|
|
924
|
-
checkRowless(methods, data, fail5);
|
|
772
|
+
checkRowless(checked2, methods, data, fail5);
|
|
925
773
|
const service = Object.freeze({
|
|
926
774
|
name: checked2.name,
|
|
927
775
|
contract: checked2,
|
|
@@ -937,7 +785,8 @@ function buildService(runtime, contract, definition) {
|
|
|
937
785
|
definition.streams,
|
|
938
786
|
fail5
|
|
939
787
|
),
|
|
940
|
-
onRoomLeave
|
|
788
|
+
onRoomLeave,
|
|
789
|
+
onHostLeave
|
|
941
790
|
});
|
|
942
791
|
checkHandlers(service, fail5);
|
|
943
792
|
registerRuntime(service, runtime);
|
|
@@ -978,6 +827,10 @@ function streamOf(current, contract, name) {
|
|
|
978
827
|
},
|
|
979
828
|
pushMany(...args) {
|
|
980
829
|
handle().pushMany(...args);
|
|
830
|
+
},
|
|
831
|
+
// Async, so every refusal (no dispatcher, a host-served stream, a bad scope) rejects.
|
|
832
|
+
async subscribers(...args) {
|
|
833
|
+
return await handle().subscribers(...args);
|
|
981
834
|
}
|
|
982
835
|
});
|
|
983
836
|
}
|
|
@@ -1060,7 +913,7 @@ var COLUMNS = /* @__PURE__ */ new WeakMap();
|
|
|
1060
913
|
function jsonAclColumnsOf(policy) {
|
|
1061
914
|
return COLUMNS.get(policy);
|
|
1062
915
|
}
|
|
1063
|
-
function
|
|
916
|
+
function isRecord4(value) {
|
|
1064
917
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1065
918
|
}
|
|
1066
919
|
function aclLevel(acl, userId) {
|
|
@@ -1069,7 +922,7 @@ function aclLevel(acl, userId) {
|
|
|
1069
922
|
}
|
|
1070
923
|
let level = null;
|
|
1071
924
|
for (const entry of acl) {
|
|
1072
|
-
if (
|
|
925
|
+
if (isRecord4(entry) && entry.userId === userId) {
|
|
1073
926
|
level = maxLevel(level, entry.level);
|
|
1074
927
|
}
|
|
1075
928
|
}
|
|
@@ -1115,11 +968,11 @@ function jsonAcl(field, options = {}) {
|
|
|
1115
968
|
}
|
|
1116
969
|
|
|
1117
970
|
// src/server/access/policies/inherit.ts
|
|
1118
|
-
function
|
|
971
|
+
function isContract2(value) {
|
|
1119
972
|
return typeof value === "object" && value !== null && typeof value.name === "string" && Object.isFrozen(value);
|
|
1120
973
|
}
|
|
1121
974
|
function inherit(options) {
|
|
1122
|
-
if (typeof options !== "object" || options === null || !
|
|
975
|
+
if (typeof options !== "object" || options === null || !isContract2(options.from)) {
|
|
1123
976
|
throw new TypeError("inherit({ from, via }): from must be the parent service's contract");
|
|
1124
977
|
}
|
|
1125
978
|
const { from } = options;
|
|
@@ -1202,6 +1055,10 @@ function rowLevel(form, fallback) {
|
|
|
1202
1055
|
}
|
|
1203
1056
|
return form.entry ?? form.scope ?? fallback;
|
|
1204
1057
|
}
|
|
1058
|
+
function bulkFallback(counterpart) {
|
|
1059
|
+
const level = counterpart === void 0 ? "Moderate" : rowLevel(counterpart, "Moderate");
|
|
1060
|
+
return meetsLevel(level, "Moderate") ? level : "Moderate";
|
|
1061
|
+
}
|
|
1205
1062
|
function unchecked(call, form, use) {
|
|
1206
1063
|
return call.runtime.service.access === void 0 || use === "read" && form === "public";
|
|
1207
1064
|
}
|
|
@@ -1228,8 +1085,8 @@ function grantRowLevel(call, form) {
|
|
|
1228
1085
|
function grantReaches(call, form, level) {
|
|
1229
1086
|
return meetsLevel(grantRowLevel(call, form), level);
|
|
1230
1087
|
}
|
|
1231
|
-
async function rowsWhere(call, form, level) {
|
|
1232
|
-
if (unchecked(call, form,
|
|
1088
|
+
async function rowsWhere(call, form, level, use = "read") {
|
|
1089
|
+
if (unchecked(call, form, use) || grantReaches(call, form, level)) {
|
|
1233
1090
|
return void 0;
|
|
1234
1091
|
}
|
|
1235
1092
|
if (call.principal === null) {
|
|
@@ -1291,29 +1148,85 @@ function guardedColumns(service) {
|
|
|
1291
1148
|
add(collection.scope.column, collection.anchor.name);
|
|
1292
1149
|
}
|
|
1293
1150
|
}
|
|
1294
|
-
const
|
|
1295
|
-
const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !
|
|
1151
|
+
const parents = new Set((reads?.parents ?? []).map(({ via }) => via));
|
|
1152
|
+
const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !parents.has(column));
|
|
1296
1153
|
return { locked: new Set(locked), anchors: [...anchors.values()] };
|
|
1297
1154
|
}
|
|
1155
|
+
var MEMBERSHIPS = /* @__PURE__ */ new WeakMap();
|
|
1156
|
+
function membershipColumnsOf(services, model) {
|
|
1157
|
+
let byModel = MEMBERSHIPS.get(services);
|
|
1158
|
+
if (byModel === void 0) {
|
|
1159
|
+
byModel = /* @__PURE__ */ new Map();
|
|
1160
|
+
MEMBERSHIPS.set(services, byModel);
|
|
1161
|
+
}
|
|
1162
|
+
const key = modelKey(model);
|
|
1163
|
+
const kept = byModel.get(key);
|
|
1164
|
+
if (kept !== void 0) {
|
|
1165
|
+
return kept;
|
|
1166
|
+
}
|
|
1167
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1168
|
+
for (const service of services.values()) {
|
|
1169
|
+
for (const read of service.access?.reads.memberships ?? []) {
|
|
1170
|
+
if (modelKey(read.model) !== key) {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
for (const column of [read.entry, read.user, read.level]) {
|
|
1174
|
+
if (!columns.has(column)) {
|
|
1175
|
+
columns.set(column, service.name);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
byModel.set(key, columns);
|
|
1181
|
+
return columns;
|
|
1182
|
+
}
|
|
1298
1183
|
function forbidden(message) {
|
|
1299
1184
|
return new QuickdrawError("FORBIDDEN", message);
|
|
1300
1185
|
}
|
|
1301
1186
|
function bypasses(service, principal) {
|
|
1302
1187
|
return principal !== null && service.adminBypass && serviceGrant(principal, service.name) === "Admin";
|
|
1303
1188
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1189
|
+
function namedColumns(data) {
|
|
1190
|
+
return new Set(Object.keys(data).filter((column) => data[column] !== void 0));
|
|
1191
|
+
}
|
|
1192
|
+
function membershipNamed(runtime, columns) {
|
|
1193
|
+
const { service, services } = runtime;
|
|
1194
|
+
if (service.model === void 0) {
|
|
1195
|
+
return void 0;
|
|
1196
|
+
}
|
|
1197
|
+
const memberships = membershipColumnsOf(services, service.model);
|
|
1198
|
+
const column = columns.find((named) => memberships.has(named));
|
|
1199
|
+
return column === void 0 ? void 0 : [column, memberships.get(column) ?? service.name];
|
|
1200
|
+
}
|
|
1201
|
+
function checkLockedColumns(runtime, principal, data, options = {}) {
|
|
1202
|
+
const { service } = runtime;
|
|
1306
1203
|
if (bypasses(service, principal)) {
|
|
1307
1204
|
return;
|
|
1308
1205
|
}
|
|
1309
|
-
const { locked
|
|
1310
|
-
const named =
|
|
1311
|
-
const readOnly =
|
|
1206
|
+
const { locked } = guardedColumns(service);
|
|
1207
|
+
const named = [...namedColumns(data)];
|
|
1208
|
+
const readOnly = named.find((column) => locked.has(column));
|
|
1209
|
+
const creating = options.creating === true;
|
|
1312
1210
|
if (readOnly !== void 0) {
|
|
1313
1211
|
throw forbidden(
|
|
1314
|
-
`"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may change it`
|
|
1212
|
+
creating ? `"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may send it; set it in the kit's prepare` : `"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may change it`
|
|
1315
1213
|
);
|
|
1316
1214
|
}
|
|
1215
|
+
const membership = membershipNamed(runtime, named);
|
|
1216
|
+
if (membership !== void 0) {
|
|
1217
|
+
const [column, reader] = membership;
|
|
1218
|
+
throw forbidden(
|
|
1219
|
+
creating ? `"${column}" holds a membership in ${reader}'s policy: only a service-wide Admin may send it through a kit; add members with the sharing kit` : `"${column}" holds a membership in ${reader}'s policy: only a service-wide Admin may change it through a kit; change memberships with the sharing kit`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function checkAnchorColumns(runtime, principal, data, level, options = {}) {
|
|
1224
|
+
const { service, access } = runtime;
|
|
1225
|
+
if (bypasses(service, principal)) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const { anchors } = guardedColumns(service);
|
|
1229
|
+
const named = namedColumns(data);
|
|
1317
1230
|
for (const { column, anchor } of anchors) {
|
|
1318
1231
|
const target = data[column];
|
|
1319
1232
|
if (!named.has(column) || target === null) {
|
|
@@ -1323,11 +1236,75 @@ async function checkWritableColumns(runtime, principal, data, level) {
|
|
|
1323
1236
|
const resolved = valid ? await access.resolve(anchor, principal, [target], { grants: false }) : void 0;
|
|
1324
1237
|
if (resolved === void 0 || !meetsLevel(resolved.levels.get(target), level)) {
|
|
1325
1238
|
throw forbidden(
|
|
1326
|
-
`Moving a row of ${service.name} by "${column}" needs ${level} on the ${anchor} row it moves into`
|
|
1239
|
+
options.creating === true ? `Creating a row of ${service.name} with "${column}" needs ${level} on the ${anchor} row it names` : `Moving a row of ${service.name} by "${column}" needs ${level} on the ${anchor} row it moves into`
|
|
1327
1240
|
);
|
|
1328
1241
|
}
|
|
1329
1242
|
}
|
|
1330
1243
|
}
|
|
1244
|
+
var relations = /* @__PURE__ */ new WeakMap();
|
|
1245
|
+
function relationKeysOf(service) {
|
|
1246
|
+
let keys = relations.get(service);
|
|
1247
|
+
if (keys === void 0) {
|
|
1248
|
+
const found = /* @__PURE__ */ new Set();
|
|
1249
|
+
for (const projection of service.projections.values()) {
|
|
1250
|
+
for (const [key, value] of Object.entries(projection.select)) {
|
|
1251
|
+
if (typeof value === "object" && value !== null) {
|
|
1252
|
+
found.add(key);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
keys = found;
|
|
1257
|
+
relations.set(service, keys);
|
|
1258
|
+
}
|
|
1259
|
+
return keys;
|
|
1260
|
+
}
|
|
1261
|
+
function isPlainObject(value) {
|
|
1262
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1266
|
+
return prototype === Object.prototype || prototype === null;
|
|
1267
|
+
}
|
|
1268
|
+
function columnValueIssues(service, data, path = []) {
|
|
1269
|
+
const relationKeys = relationKeysOf(service);
|
|
1270
|
+
const nested = new Map(
|
|
1271
|
+
findNestedWrites({ ...data }).map(({ field, operation }) => [field, operation])
|
|
1272
|
+
);
|
|
1273
|
+
const issues = [];
|
|
1274
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1275
|
+
if (value === void 0) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const operation = nested.get(key) ?? (isPlainObject(value) && Array.isArray(value.set) ? "set" : void 0);
|
|
1279
|
+
if (relationKeys.has(key)) {
|
|
1280
|
+
issues.push({
|
|
1281
|
+
path: [...path, key],
|
|
1282
|
+
message: `"${key}" is a relation of ${service.name}'s model: kits write column values only; set its foreign key column instead`
|
|
1283
|
+
});
|
|
1284
|
+
} else if (operation !== void 0) {
|
|
1285
|
+
issues.push({
|
|
1286
|
+
path: [...path, key],
|
|
1287
|
+
message: `"${key}" holds a nested write ({ ${operation} }), which is not tracked: kits write column values only`
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return issues;
|
|
1292
|
+
}
|
|
1293
|
+
function checkColumnValues(service, data, path = []) {
|
|
1294
|
+
const issues = columnValueIssues(service, data, path);
|
|
1295
|
+
if (issues.length > 0) {
|
|
1296
|
+
throw new QuickdrawError(
|
|
1297
|
+
"VALIDATION",
|
|
1298
|
+
"The data holds a relation or a nested write, which kits do not write",
|
|
1299
|
+
{ issues }
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async function checkWritableColumns(runtime, principal, data, level, path = []) {
|
|
1304
|
+
checkColumnValues(runtime.service, data, path);
|
|
1305
|
+
checkLockedColumns(runtime, principal, data);
|
|
1306
|
+
await checkAnchorColumns(runtime, principal, data, level);
|
|
1307
|
+
}
|
|
1331
1308
|
|
|
1332
1309
|
// src/server/kits/transactions.ts
|
|
1333
1310
|
function isWriteConflict(error) {
|
|
@@ -1447,13 +1424,13 @@ function updateHandler(form) {
|
|
|
1447
1424
|
}
|
|
1448
1425
|
|
|
1449
1426
|
// src/server/kits/crud/bulk.ts
|
|
1450
|
-
async function writeAllowed(call, db, form, ids, write) {
|
|
1427
|
+
async function writeAllowed(call, db, [form, level], ids, write) {
|
|
1451
1428
|
const unique = uniqueIds(ids);
|
|
1452
1429
|
if (unique.length === 0) {
|
|
1453
1430
|
return { count: 0 };
|
|
1454
1431
|
}
|
|
1455
1432
|
return await inTransaction(db, async (tx) => {
|
|
1456
|
-
const allowed = await allowedIds(call, form, unique,
|
|
1433
|
+
const allowed = await allowedIds(call, form, unique, level, "write");
|
|
1457
1434
|
if (allowed.length === 0) {
|
|
1458
1435
|
return { count: 0 };
|
|
1459
1436
|
}
|
|
@@ -1461,7 +1438,8 @@ async function writeAllowed(call, db, form, ids, write) {
|
|
|
1461
1438
|
return { count };
|
|
1462
1439
|
});
|
|
1463
1440
|
}
|
|
1464
|
-
function bulkUpdateHandler(form) {
|
|
1441
|
+
function bulkUpdateHandler(form, fallback) {
|
|
1442
|
+
const level = rowLevel(form, fallback);
|
|
1465
1443
|
const handler = async ({ input, ctx, db }) => {
|
|
1466
1444
|
const call = crudCall(ctx, db);
|
|
1467
1445
|
const { ids, data } = input;
|
|
@@ -1469,24 +1447,25 @@ function bulkUpdateHandler(form) {
|
|
|
1469
1447
|
if (Object.keys(patch).length === 0) {
|
|
1470
1448
|
return { count: 0 };
|
|
1471
1449
|
}
|
|
1472
|
-
await checkWritableColumns(call.runtime, call.principal, patch,
|
|
1450
|
+
await checkWritableColumns(call.runtime, call.principal, patch, level, ["data"]);
|
|
1473
1451
|
return await writeAllowed(
|
|
1474
1452
|
call,
|
|
1475
1453
|
db,
|
|
1476
|
-
form,
|
|
1454
|
+
[form, level],
|
|
1477
1455
|
ids,
|
|
1478
1456
|
(table, allowed) => table.updateMany({ where: { id: { in: [...allowed] } }, data: patch })
|
|
1479
1457
|
);
|
|
1480
1458
|
};
|
|
1481
1459
|
return handler;
|
|
1482
1460
|
}
|
|
1483
|
-
function bulkDeleteHandler(form) {
|
|
1461
|
+
function bulkDeleteHandler(form, fallback) {
|
|
1462
|
+
const level = rowLevel(form, fallback);
|
|
1484
1463
|
const handler = async ({ input, ctx, db }) => {
|
|
1485
1464
|
const call = crudCall(ctx, db);
|
|
1486
1465
|
return await writeAllowed(
|
|
1487
1466
|
call,
|
|
1488
1467
|
db,
|
|
1489
|
-
form,
|
|
1468
|
+
[form, level],
|
|
1490
1469
|
input.ids,
|
|
1491
1470
|
(table, allowed) => table.deleteMany({ where: { id: { in: [...allowed] } } })
|
|
1492
1471
|
);
|
|
@@ -1495,16 +1474,40 @@ function bulkDeleteHandler(form) {
|
|
|
1495
1474
|
}
|
|
1496
1475
|
|
|
1497
1476
|
// src/server/kits/crud/create.ts
|
|
1498
|
-
function
|
|
1477
|
+
function isRecord5(value) {
|
|
1478
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1479
|
+
}
|
|
1480
|
+
function isPlainObject2(value) {
|
|
1481
|
+
if (!isRecord5(value)) {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1485
|
+
return prototype === Object.prototype || prototype === null;
|
|
1486
|
+
}
|
|
1487
|
+
function createHandler(form, prepare) {
|
|
1499
1488
|
const handler = async ({ input, ctx, db }) => {
|
|
1500
1489
|
const call = crudCall(ctx, db);
|
|
1490
|
+
const { service } = call.runtime;
|
|
1491
|
+
const sent = isRecord5(input) ? input : {};
|
|
1492
|
+
checkColumnValues(service, sent);
|
|
1493
|
+
checkLockedColumns(call.runtime, call.principal, sent, { creating: true });
|
|
1501
1494
|
const data = prepare === void 0 ? input : await checked(() => prepare(input, ctx, db));
|
|
1502
|
-
if (
|
|
1495
|
+
if (!isPlainObject2(data)) {
|
|
1503
1496
|
throw new QuickdrawError(
|
|
1504
1497
|
"INTERNAL",
|
|
1505
|
-
`The read/write kit's prepare for ${
|
|
1498
|
+
`The read/write kit's prepare for ${service.name} must return the new row's data`
|
|
1506
1499
|
);
|
|
1507
1500
|
}
|
|
1501
|
+
const issues = columnValueIssues(service, data);
|
|
1502
|
+
if (issues.length > 0) {
|
|
1503
|
+
throw new QuickdrawError(
|
|
1504
|
+
"INTERNAL",
|
|
1505
|
+
`The read/write kit's prepare for ${service.name} must return column values: ${issues.map((issue) => issue.message).join("; ")}`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
await checkAnchorColumns(call.runtime, call.principal, data, rowLevel(form, "Moderate"), {
|
|
1509
|
+
creating: true
|
|
1510
|
+
});
|
|
1508
1511
|
const { select } = projectionOf(call, "entity");
|
|
1509
1512
|
return await call.table.create({ data, select });
|
|
1510
1513
|
};
|
|
@@ -1602,20 +1605,33 @@ function signatureOf(order) {
|
|
|
1602
1605
|
function signed(order) {
|
|
1603
1606
|
return [[SIGNATURE, "asc"], ...order];
|
|
1604
1607
|
}
|
|
1608
|
+
var ROW = "row ";
|
|
1609
|
+
var ROW_CURSOR = [
|
|
1610
|
+
[SIGNATURE, "asc"],
|
|
1611
|
+
["id", "asc"]
|
|
1612
|
+
];
|
|
1605
1613
|
function cursorAfter(order, row) {
|
|
1606
|
-
|
|
1614
|
+
const cursor = encodeCursor(signed(order), { ...row, [SIGNATURE]: signatureOf(order) });
|
|
1615
|
+
return cursor.length <= MAX_CURSOR_LENGTH ? cursor : encodeCursor(ROW_CURSOR, { [SIGNATURE]: `${ROW}${signatureOf(order)}`, id: row.id });
|
|
1607
1616
|
}
|
|
1608
|
-
function
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1617
|
+
function readCursor(order, cursor) {
|
|
1618
|
+
const signature = signatureOf(order);
|
|
1619
|
+
const decoded = (shape) => {
|
|
1620
|
+
try {
|
|
1621
|
+
return decodeCursor(shape, cursor);
|
|
1622
|
+
} catch {
|
|
1623
|
+
return void 0;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
const values = decoded(signed(order));
|
|
1627
|
+
if (values?.[0] === signature) {
|
|
1628
|
+
return { values: values.slice(1) };
|
|
1614
1629
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1630
|
+
const named = decoded(ROW_CURSOR);
|
|
1631
|
+
if (named?.[0] === `${ROW}${signature}` && typeof named[1] === "string") {
|
|
1632
|
+
return { row: named[1] };
|
|
1617
1633
|
}
|
|
1618
|
-
|
|
1634
|
+
throw unreadable(NOT_A_CURSOR, ["cursor"]);
|
|
1619
1635
|
}
|
|
1620
1636
|
async function nullableColumns(read, values) {
|
|
1621
1637
|
const { storage, model, order } = read;
|
|
@@ -1652,8 +1668,27 @@ function refusal(error, read) {
|
|
|
1652
1668
|
refused.cause = error;
|
|
1653
1669
|
return refused;
|
|
1654
1670
|
}
|
|
1671
|
+
async function startValues(read, cursor) {
|
|
1672
|
+
const held = readCursor(read.order, cursor);
|
|
1673
|
+
if ("values" in held) {
|
|
1674
|
+
return held.values;
|
|
1675
|
+
}
|
|
1676
|
+
let row;
|
|
1677
|
+
try {
|
|
1678
|
+
row = await read.table.findFirst({
|
|
1679
|
+
where: { AND: [read.where, { id: held.row }] },
|
|
1680
|
+
select: Object.fromEntries(read.order.map(([column]) => [column, true]))
|
|
1681
|
+
});
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
throw refusal(error, read);
|
|
1684
|
+
}
|
|
1685
|
+
if (row === null) {
|
|
1686
|
+
throw unreadable(NOT_A_CURSOR, ["cursor"]);
|
|
1687
|
+
}
|
|
1688
|
+
return read.order.map(([column]) => row[column]);
|
|
1689
|
+
}
|
|
1655
1690
|
async function readPage(read) {
|
|
1656
|
-
const values = read.cursor === void 0 ? void 0 :
|
|
1691
|
+
const values = read.cursor === void 0 ? void 0 : await startValues(read, read.cursor);
|
|
1657
1692
|
const nullable = await nullableColumns(read, values);
|
|
1658
1693
|
const where = values === void 0 ? read.where : { AND: [read.where, afterCursor(read.order, values, nullable)] };
|
|
1659
1694
|
let rows;
|
|
@@ -1762,14 +1797,17 @@ async function nextOrdinal(db, model, where = {}, options = {}) {
|
|
|
1762
1797
|
function invalid(message, path) {
|
|
1763
1798
|
return new QuickdrawError("VALIDATION", message, { issues: [{ path: [...path], message }] });
|
|
1764
1799
|
}
|
|
1765
|
-
async function readNamed(table, spec, move, model) {
|
|
1800
|
+
async function readNamed(table, [spec, writable], move, model) {
|
|
1766
1801
|
const select = Object.fromEntries(["id", spec.column, ...spec.within].map((key) => [key, true]));
|
|
1767
|
-
const ids =
|
|
1768
|
-
|
|
1802
|
+
const ids = {
|
|
1803
|
+
id: { in: [move.id, move.beforeId, move.afterId].filter((id) => id !== void 0) }
|
|
1804
|
+
};
|
|
1805
|
+
const where = writable === void 0 ? ids : { AND: [ids, writable] };
|
|
1806
|
+
const rows = writable === "none" ? [] : await table.findMany({ where, select });
|
|
1769
1807
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
1770
1808
|
const moved = byId.get(move.id);
|
|
1771
1809
|
if (moved === void 0) {
|
|
1772
|
-
throw new QuickdrawError("NOT_FOUND", `No such ${model}`);
|
|
1810
|
+
throw writable === void 0 ? new QuickdrawError("NOT_FOUND", `No such ${model}`) : new QuickdrawError("FORBIDDEN", "Insufficient permissions");
|
|
1773
1811
|
}
|
|
1774
1812
|
const neighbor = (id, key) => {
|
|
1775
1813
|
const row = id === void 0 ? void 0 : byId.get(id);
|
|
@@ -1785,9 +1823,10 @@ async function readNamed(table, spec, move, model) {
|
|
|
1785
1823
|
after: neighbor(move.afterId, "afterId")
|
|
1786
1824
|
};
|
|
1787
1825
|
}
|
|
1788
|
-
function listOf(spec, moved) {
|
|
1826
|
+
function listOf(spec, moved, writable) {
|
|
1789
1827
|
const scope = Object.fromEntries(spec.within.map((column) => [column, moved[column] ?? null]));
|
|
1790
|
-
|
|
1828
|
+
const mine = writable === void 0 ? [] : [writable === "none" ? { id: { in: [] } } : writable];
|
|
1829
|
+
return { AND: [scope, { id: { not: moved.id } }, ...mine] };
|
|
1791
1830
|
}
|
|
1792
1831
|
async function adjacent(table, spec, list, row, side) {
|
|
1793
1832
|
const { column } = spec;
|
|
@@ -1805,10 +1844,10 @@ async function adjacent(table, spec, list, row, side) {
|
|
|
1805
1844
|
select: { id: true, [column]: true }
|
|
1806
1845
|
});
|
|
1807
1846
|
}
|
|
1808
|
-
async function bounds(table, spec, named) {
|
|
1847
|
+
async function bounds(table, spec, named, writable) {
|
|
1809
1848
|
const { column } = spec;
|
|
1810
1849
|
const { before, after } = named;
|
|
1811
|
-
const list = listOf(spec, named.moved);
|
|
1850
|
+
const list = listOf(spec, named.moved, writable);
|
|
1812
1851
|
const low = before ?? (after === void 0 ? null : await adjacent(table, spec, list, after, "previous"));
|
|
1813
1852
|
const high = after ?? (before === void 0 ? null : await adjacent(table, spec, list, before, "next"));
|
|
1814
1853
|
const lowValue = low === null ? null : low[column];
|
|
@@ -1822,14 +1861,21 @@ async function bounds(table, spec, named) {
|
|
|
1822
1861
|
}
|
|
1823
1862
|
return [lowValue, highValue];
|
|
1824
1863
|
}
|
|
1825
|
-
async function renumber(table, spec, named, move) {
|
|
1864
|
+
async function renumber(table, [spec, writable], named, move) {
|
|
1826
1865
|
const { column } = spec;
|
|
1827
1866
|
const rows = await table.findMany({
|
|
1828
|
-
where: listOf(spec, named.moved),
|
|
1867
|
+
where: listOf(spec, named.moved, writable),
|
|
1829
1868
|
select: { id: true, [column]: true },
|
|
1830
1869
|
orderBy: [{ [column]: "asc" }, { id: "asc" }]
|
|
1831
1870
|
});
|
|
1832
1871
|
const order = rows.map((row) => row.id);
|
|
1872
|
+
const neighbor = move.beforeId ?? move.afterId;
|
|
1873
|
+
if (neighbor !== void 0 && !order.includes(neighbor)) {
|
|
1874
|
+
throw new QuickdrawError(
|
|
1875
|
+
"INTERNAL",
|
|
1876
|
+
"A reorder's neighbor is missing from the list it renumbers"
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1833
1879
|
const at = move.beforeId === void 0 ? Math.max(order.indexOf(move.afterId), 0) : order.indexOf(move.beforeId) + 1;
|
|
1834
1880
|
order.splice(at, 0, move.id);
|
|
1835
1881
|
const current = new Map(rows.map((row) => [row.id, row[column]]));
|
|
@@ -1848,29 +1894,48 @@ var RENUMBER_BASE_MS = 5e3;
|
|
|
1848
1894
|
var RENUMBER_MS_PER_ROW = 10;
|
|
1849
1895
|
var OWNER = "The read/write kit's reorder";
|
|
1850
1896
|
var CONFLICT = "Another change to this list ran at the same time; try again";
|
|
1851
|
-
async function moveIn(tx, { call, spec, move, select }, mayRenumber) {
|
|
1897
|
+
async function moveIn(tx, { call, spec, move, writable, select }, mayRenumber) {
|
|
1852
1898
|
const table = delegateOf(tx, call.model);
|
|
1853
|
-
const named = await readNamed(table, spec, move, call.model);
|
|
1854
|
-
const range = await bounds(table, spec, named);
|
|
1899
|
+
const named = await readNamed(table, [spec, writable], move, call.model);
|
|
1900
|
+
const range = await bounds(table, spec, named, writable);
|
|
1855
1901
|
const between = range === void 0 ? void 0 : ordinalBetween(range[0] ?? void 0, range[1] ?? void 0);
|
|
1856
1902
|
if (between === void 0 && !mayRenumber) {
|
|
1857
|
-
return { list: listOf(spec, named.moved) };
|
|
1903
|
+
return { list: listOf(spec, named.moved, writable) };
|
|
1858
1904
|
}
|
|
1859
|
-
const value = between ?? await renumber(table, spec, named, move);
|
|
1905
|
+
const value = between ?? await renumber(table, [spec, writable], named, move);
|
|
1860
1906
|
return {
|
|
1861
1907
|
row: await table.update({ where: { id: move.id }, data: { [spec.column]: value }, select })
|
|
1862
1908
|
};
|
|
1863
1909
|
}
|
|
1910
|
+
async function checkNamed(call, [form, level], move) {
|
|
1911
|
+
const named = [move.beforeId, move.afterId].filter((id) => id !== void 0);
|
|
1912
|
+
const ids = uniqueIds([move.id, ...named]);
|
|
1913
|
+
const writable = new Set(await allowedIds(call, form, ids, level, "write"));
|
|
1914
|
+
if (!writable.has(move.id)) {
|
|
1915
|
+
throw new QuickdrawError("FORBIDDEN", "Insufficient permissions");
|
|
1916
|
+
}
|
|
1917
|
+
for (const [id, key] of [
|
|
1918
|
+
[move.beforeId, "beforeId"],
|
|
1919
|
+
[move.afterId, "afterId"]
|
|
1920
|
+
]) {
|
|
1921
|
+
if (id !== void 0 && !writable.has(id)) {
|
|
1922
|
+
throw new QuickdrawError("NOT_FOUND", `No such ${call.model} in this list as ${key}`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1864
1926
|
function reorderHandler(spec, form) {
|
|
1927
|
+
const level = rowLevel(form, "Moderate");
|
|
1865
1928
|
const handler = async ({ input, ctx, db }) => {
|
|
1866
1929
|
const call = crudCall(ctx, db);
|
|
1930
|
+
const asked = input;
|
|
1931
|
+
await checkNamed(call, [form, level], asked);
|
|
1867
1932
|
const move = {
|
|
1868
1933
|
call,
|
|
1869
1934
|
spec,
|
|
1870
|
-
move:
|
|
1935
|
+
move: asked,
|
|
1936
|
+
writable: await rowsWhere(call, form, level, "write"),
|
|
1871
1937
|
select: projectionOf(call, "entity").select
|
|
1872
1938
|
};
|
|
1873
|
-
await checkRowWrite(call, form, move.move.id);
|
|
1874
1939
|
const how = { owner: OWNER, conflict: CONFLICT };
|
|
1875
1940
|
const first = await inKitTransaction(db, async (tx) => await moveIn(tx, move, false), how);
|
|
1876
1941
|
if ("row" in first) {
|
|
@@ -1901,7 +1966,7 @@ function handlerOf(context) {
|
|
|
1901
1966
|
case "list":
|
|
1902
1967
|
return listHandler({ spec, form, projection: context.projection });
|
|
1903
1968
|
case "create":
|
|
1904
|
-
return createHandler(context.prepare);
|
|
1969
|
+
return createHandler(form, context.prepare);
|
|
1905
1970
|
case "update":
|
|
1906
1971
|
return updateHandler(form);
|
|
1907
1972
|
case "delete":
|
|
@@ -1909,9 +1974,9 @@ function handlerOf(context) {
|
|
|
1909
1974
|
case "reorder":
|
|
1910
1975
|
return reorderHandler(spec, form);
|
|
1911
1976
|
case "bulkUpdate":
|
|
1912
|
-
return bulkUpdateHandler(form);
|
|
1977
|
+
return bulkUpdateHandler(form, context.bulkLevel);
|
|
1913
1978
|
default:
|
|
1914
|
-
return bulkDeleteHandler(form);
|
|
1979
|
+
return bulkDeleteHandler(form, context.bulkLevel);
|
|
1915
1980
|
}
|
|
1916
1981
|
}
|
|
1917
1982
|
|
|
@@ -1989,7 +2054,14 @@ var CHECKS_ROWS = /* @__PURE__ */ new Set([
|
|
|
1989
2054
|
"delete",
|
|
1990
2055
|
"reorder"
|
|
1991
2056
|
]);
|
|
1992
|
-
var
|
|
2057
|
+
var CALLER_DEPENDENT = /* @__PURE__ */ new Set(["list", "getMany"]);
|
|
2058
|
+
var COUNTERPARTS = { bulkDelete: "delete", bulkUpdate: "update" };
|
|
2059
|
+
function bulkLevelOf(kit, access, spec) {
|
|
2060
|
+
const counterpart = spec.method === "bulkDelete" || spec.method === "bulkUpdate" ? COUNTERPARTS[spec.method] : void 0;
|
|
2061
|
+
const found = kit.find(([, other]) => other.method === counterpart);
|
|
2062
|
+
return bulkFallback(found === void 0 ? void 0 : access[found[0]]);
|
|
2063
|
+
}
|
|
2064
|
+
var OPTION_KEYS = ["access", "prepare", "rowless"];
|
|
1993
2065
|
function serviceProblem(service, contract, [name, spec], form) {
|
|
1994
2066
|
if (service.contract !== contract) {
|
|
1995
2067
|
return `its read/write kit handlers were made for another contract; pass ${service.name}'s own contract to crud.handlers`;
|
|
@@ -2004,9 +2076,9 @@ function handlers(contract, options) {
|
|
|
2004
2076
|
if (!isRecord6(options)) {
|
|
2005
2077
|
fail("options must be { access, prepare?, rowless? }");
|
|
2006
2078
|
}
|
|
2007
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
2079
|
+
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS.includes(key));
|
|
2008
2080
|
if (unknownKey !== void 0) {
|
|
2009
|
-
fail(`options has an unknown key "${unknownKey}"; the options are ${
|
|
2081
|
+
fail(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS.join(", ")}`);
|
|
2010
2082
|
}
|
|
2011
2083
|
const names = kit.map(([name]) => name);
|
|
2012
2084
|
const access = checkAccess(options.access, names);
|
|
@@ -2016,11 +2088,15 @@ function handlers(contract, options) {
|
|
|
2016
2088
|
for (const [name, spec] of kit) {
|
|
2017
2089
|
const form = access[name];
|
|
2018
2090
|
const projection = spec.method === "list" ? itemProjection(contract, spec) : "entity";
|
|
2019
|
-
const
|
|
2091
|
+
const bulkLevel = bulkLevelOf(kit, access, spec);
|
|
2092
|
+
const handler = handlerOf({ spec, form, projection, prepare, bulkLevel });
|
|
2020
2093
|
checkWhenDefined(handler, (service) => serviceProblem(service, contract, [name, spec], form));
|
|
2021
2094
|
if (CHECKS_ROWS.has(spec.method)) {
|
|
2022
2095
|
checksRowsItself(handler);
|
|
2023
2096
|
}
|
|
2097
|
+
if (CALLER_DEPENDENT.has(spec.method)) {
|
|
2098
|
+
dependsOnCaller(handler);
|
|
2099
|
+
}
|
|
2024
2100
|
entries[name] = kitEntry(name, form, handler, rowless);
|
|
2025
2101
|
}
|
|
2026
2102
|
return Object.freeze(entries);
|
|
@@ -2087,7 +2163,8 @@ async function authorizeScope(run, collection, scope) {
|
|
|
2087
2163
|
const rule = {
|
|
2088
2164
|
service: runtime.service,
|
|
2089
2165
|
anchor: collection.anchor?.name,
|
|
2090
|
-
access: collection.access
|
|
2166
|
+
access: collection.access,
|
|
2167
|
+
requires: collection.requires
|
|
2091
2168
|
};
|
|
2092
2169
|
const allowed = await allowedScopes(runtime.access, rule, principal, [scope]);
|
|
2093
2170
|
if (!allowed.has(scope)) {
|
|
@@ -2106,9 +2183,7 @@ async function scopeWhere(run) {
|
|
|
2106
2183
|
return await membersWhere(reader, collection, run.query.scope, collection.maxLimit) ?? "none";
|
|
2107
2184
|
}
|
|
2108
2185
|
function searchOrder(run) {
|
|
2109
|
-
|
|
2110
|
-
const collection = scope === void 0 ? void 0 : run.call.runtime.service.collections.get(scope);
|
|
2111
|
-
return collection?.order ?? ID_ORDER;
|
|
2186
|
+
return scopedCollection(run)?.order ?? ID_ORDER;
|
|
2112
2187
|
}
|
|
2113
2188
|
async function strategyIds(run, ids) {
|
|
2114
2189
|
const found = await checked(() => ids(run.query.q, run.ctx, { limit: run.query.limit }));
|
|
@@ -2272,14 +2347,14 @@ function checkStrategy(strategy) {
|
|
|
2272
2347
|
}
|
|
2273
2348
|
return strategy;
|
|
2274
2349
|
}
|
|
2275
|
-
var
|
|
2350
|
+
var OPTION_KEYS2 = ["access", "strategy", "method"];
|
|
2276
2351
|
function checkOptions(options, kit) {
|
|
2277
2352
|
if (!isRecord8(options)) {
|
|
2278
2353
|
fail2("options must be { access, strategy?, method? }");
|
|
2279
2354
|
}
|
|
2280
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
2355
|
+
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS2.includes(key));
|
|
2281
2356
|
if (unknownKey !== void 0) {
|
|
2282
|
-
fail2(`options has an unknown key "${unknownKey}"; the options are ${
|
|
2357
|
+
fail2(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS2.join(", ")}`);
|
|
2283
2358
|
}
|
|
2284
2359
|
const problem = accessFormProblem(options.access);
|
|
2285
2360
|
if (problem !== void 0) {
|
|
@@ -2344,6 +2419,7 @@ function handlers2(contract, options) {
|
|
|
2344
2419
|
checkScope(contract, name, spec, projection);
|
|
2345
2420
|
const handler = searchHandler({ spec, form, projection, strategy });
|
|
2346
2421
|
checkWhenDefined(handler, (service) => serviceProblem2(service, contract, name, form));
|
|
2422
|
+
dependsOnCaller(handler);
|
|
2347
2423
|
entries[name] = Object.freeze({ access: form, handler, share: "caller" });
|
|
2348
2424
|
}
|
|
2349
2425
|
return Object.freeze(entries);
|
|
@@ -2618,6 +2694,14 @@ function capGrant(own, given, model) {
|
|
|
2618
2694
|
);
|
|
2619
2695
|
}
|
|
2620
2696
|
}
|
|
2697
|
+
function capTarget(own, current, model) {
|
|
2698
|
+
if (current !== null && !meetsLevel(own, current)) {
|
|
2699
|
+
throw new QuickdrawError(
|
|
2700
|
+
"FORBIDDEN",
|
|
2701
|
+
`That user has ${current} on this ${model}, above your own level on it (${own ?? "none"}); you cannot change or take away their access`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2621
2705
|
|
|
2622
2706
|
// src/server/kits/sharing/context.ts
|
|
2623
2707
|
async function resolveTarget(call, context, given) {
|
|
@@ -2692,15 +2776,17 @@ async function checkLastAdmin(call, tx, listed, change) {
|
|
|
2692
2776
|
async function changeList(call, context, change) {
|
|
2693
2777
|
const columns = aclColumnsOf(call.runtime.service);
|
|
2694
2778
|
return await inSerializable(call.db, async (tx) => {
|
|
2695
|
-
|
|
2779
|
+
const own = await checkCaller(call, context.form, change.id);
|
|
2780
|
+
capGrant(own, change.level, call.model);
|
|
2696
2781
|
const table = tableOf(tx, call.model);
|
|
2697
2782
|
const listed = await readList(table, columns, change.id, call.model);
|
|
2698
2783
|
const problem = refusal2(listed, change, call.model);
|
|
2699
2784
|
if (problem !== void 0) {
|
|
2700
2785
|
throw problem;
|
|
2701
2786
|
}
|
|
2702
|
-
await checkLastAdmin(call, tx, listed, change);
|
|
2703
2787
|
const before = levelIn(listed.entries, change.userId);
|
|
2788
|
+
capTarget(own, before, call.model);
|
|
2789
|
+
await checkLastAdmin(call, tx, listed, change);
|
|
2704
2790
|
if (before === change.level && entriesOf(listed.entries, change.userId) === 1) {
|
|
2705
2791
|
return sharesOf(listed.entries);
|
|
2706
2792
|
}
|
|
@@ -2852,15 +2938,16 @@ async function invite(call, context, target, role) {
|
|
|
2852
2938
|
async function end(call, context, target, kind) {
|
|
2853
2939
|
const read = membershipTableOf(call.runtime.service);
|
|
2854
2940
|
return await inSerializable(call.db, async (tx) => {
|
|
2855
|
-
|
|
2856
|
-
await checkCaller(call, context.form, target.entryId);
|
|
2857
|
-
}
|
|
2941
|
+
const own = kind === "remove" ? await checkCaller(call, context.form, target.entryId) : void 0;
|
|
2858
2942
|
const table = tableOf(tx, read.model);
|
|
2859
2943
|
const rows = await rowsOf(table, read, target);
|
|
2860
2944
|
if (rows.length === 0) {
|
|
2861
2945
|
throw kind === "leave" ? new QuickdrawError("FORBIDDEN", `You are not a member of this ${call.model}`) : new QuickdrawError("NOT_FOUND", `That user is not a member of this ${call.model}`);
|
|
2862
2946
|
}
|
|
2863
2947
|
const current = currentRole(read, rows);
|
|
2948
|
+
if (own !== void 0) {
|
|
2949
|
+
capTarget(own, current.level, call.model);
|
|
2950
|
+
}
|
|
2864
2951
|
if (current.level === "Admin" && !await anotherAdmin(call, tx, read, target)) {
|
|
2865
2952
|
throw lastAdmin(call.model);
|
|
2866
2953
|
}
|
|
@@ -2883,6 +2970,7 @@ async function changeRole(call, context, target, role) {
|
|
|
2883
2970
|
throw new QuickdrawError("NOT_FOUND", `That user is not a member of this ${call.model}`);
|
|
2884
2971
|
}
|
|
2885
2972
|
const current = currentRole(read, rows);
|
|
2973
|
+
capTarget(own, current.level, call.model);
|
|
2886
2974
|
if (rows.length === 1 && current.role === role) {
|
|
2887
2975
|
return memberOf(read, target.userId, role);
|
|
2888
2976
|
}
|
|
@@ -3071,6 +3159,9 @@ function handlers3(contract, ...rest) {
|
|
|
3071
3159
|
const form = access[name] ?? DEFAULT_ACCESS[spec.method];
|
|
3072
3160
|
const handler = handlerOf2(spec.method, Object.freeze({ ...hooks, form }));
|
|
3073
3161
|
checkWhenDefined(handler, (service) => serviceProblem3(service, contract, spec.mode));
|
|
3162
|
+
if (spec.method === "listShares" || spec.method === "listMembers") {
|
|
3163
|
+
dependsOnCaller(handler);
|
|
3164
|
+
}
|
|
3074
3165
|
entries[name] = kitEntry(name, form, handler, rowless);
|
|
3075
3166
|
}
|
|
3076
3167
|
return Object.freeze(entries);
|
|
@@ -3123,6 +3214,13 @@ var DEFAULT_FIELDS = Object.freeze({
|
|
|
3123
3214
|
showInTable: false
|
|
3124
3215
|
}
|
|
3125
3216
|
});
|
|
3217
|
+
function grantNamesOf(column) {
|
|
3218
|
+
return column === void 0 || ADMIN_GRANT_FIELDS.includes(column) ? ADMIN_GRANT_FIELDS : [...ADMIN_GRANT_FIELDS, column];
|
|
3219
|
+
}
|
|
3220
|
+
function grantsColumnFor(model, source) {
|
|
3221
|
+
const same = model !== void 0 && source !== void 0;
|
|
3222
|
+
return same && modelKey(source.model) === modelKey(model) ? source.column : void 0;
|
|
3223
|
+
}
|
|
3126
3224
|
function isRecord10(value) {
|
|
3127
3225
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3128
3226
|
}
|
|
@@ -3142,23 +3240,19 @@ function checkDisplayName(displayName, serviceName, fail5) {
|
|
|
3142
3240
|
}
|
|
3143
3241
|
return displayName;
|
|
3144
3242
|
}
|
|
3145
|
-
function checkGrants(grants, names, fail5) {
|
|
3243
|
+
function checkGrants(grants, [names, grantNames], fail5) {
|
|
3146
3244
|
if (grants === void 0 || grants === false) {
|
|
3147
3245
|
return /* @__PURE__ */ new Set();
|
|
3148
3246
|
}
|
|
3149
3247
|
if (grants !== true) {
|
|
3150
3248
|
fail5("grants must be true or false");
|
|
3151
3249
|
}
|
|
3152
|
-
|
|
3153
|
-
if (fields.length === 0) {
|
|
3154
|
-
fail5(
|
|
3155
|
-
`grants: the entity has no ${ADMIN_GRANT_FIELDS.join(" or ")} field for the kit to show and write`
|
|
3156
|
-
);
|
|
3157
|
-
}
|
|
3158
|
-
return new Set(fields);
|
|
3250
|
+
return new Set(grantNames.filter((name) => names.includes(name)));
|
|
3159
3251
|
}
|
|
3160
|
-
function checkHidden(hiddenFields, names, grants, fail5) {
|
|
3161
|
-
const defaults = ADMIN_HIDDEN_FIELDS.filter(
|
|
3252
|
+
function checkHidden(hiddenFields, [names, grantNames], grants, fail5) {
|
|
3253
|
+
const defaults = [.../* @__PURE__ */ new Set([...ADMIN_HIDDEN_FIELDS, ...grantNames])].filter(
|
|
3254
|
+
(name) => !grants.has(name)
|
|
3255
|
+
);
|
|
3162
3256
|
if (hiddenFields === void 0) {
|
|
3163
3257
|
return new Set(defaults);
|
|
3164
3258
|
}
|
|
@@ -3293,8 +3387,9 @@ function frozenConfig(config) {
|
|
|
3293
3387
|
}
|
|
3294
3388
|
function adminFieldsOf(serviceName, spec, options, fail5) {
|
|
3295
3389
|
const names = spec.fields.map((field) => field.name);
|
|
3296
|
-
const
|
|
3297
|
-
const
|
|
3390
|
+
const grantNames = grantNamesOf(options.grantsColumn);
|
|
3391
|
+
const grants = checkGrants(options.grants, [names, grantNames], fail5);
|
|
3392
|
+
const hidden = checkHidden(options.hiddenFields, [names, grantNames], grants, fail5);
|
|
3298
3393
|
const listed = [...spec.filter, ...spec.sort].find((name) => hidden.has(name));
|
|
3299
3394
|
if (listed !== void 0) {
|
|
3300
3395
|
fail5(`"${listed}" is hidden, so adminList may not filter or sort on it`);
|
|
@@ -3322,10 +3417,26 @@ function adminFieldsOf(serviceName, spec, options, fail5) {
|
|
|
3322
3417
|
fields: Object.freeze(fields)
|
|
3323
3418
|
});
|
|
3324
3419
|
const readOnly = new Set(fields.filter((field) => !field.editable).map((field) => field.name));
|
|
3325
|
-
|
|
3420
|
+
const named = /* @__PURE__ */ new Set([
|
|
3421
|
+
...editable ?? [],
|
|
3422
|
+
...Object.entries(overrides).filter(([, override]) => override.editable === true).map(([name]) => name)
|
|
3423
|
+
]);
|
|
3424
|
+
return Object.freeze({ meta, hidden, readOnly, grants, named });
|
|
3425
|
+
}
|
|
3426
|
+
function withReadOnlyFields(meta, readOnly) {
|
|
3427
|
+
if (!meta.fields.some((field) => readOnly.has(field.name) && field.editable)) {
|
|
3428
|
+
return meta;
|
|
3429
|
+
}
|
|
3430
|
+
const fields = meta.fields.map(
|
|
3431
|
+
(field) => readOnly.has(field.name) && field.editable ? frozenConfig({ ...field, editable: false }) : field
|
|
3432
|
+
);
|
|
3433
|
+
return Object.freeze({ ...meta, fields: Object.freeze(fields) });
|
|
3326
3434
|
}
|
|
3327
3435
|
|
|
3328
3436
|
// src/server/kits/admin/runtime.ts
|
|
3437
|
+
function grantsColumnOf(runtime) {
|
|
3438
|
+
return grantsColumnFor(runtime.service.model, runtime.grantsSource);
|
|
3439
|
+
}
|
|
3329
3440
|
function tableOf2(db, model) {
|
|
3330
3441
|
const name = modelKey(model);
|
|
3331
3442
|
const isClient = (typeof db === "object" || typeof db === "function") && db !== null;
|
|
@@ -3339,7 +3450,7 @@ function tableOf2(db, model) {
|
|
|
3339
3450
|
}
|
|
3340
3451
|
return delegate;
|
|
3341
3452
|
}
|
|
3342
|
-
function adminCall(ctx, db,
|
|
3453
|
+
function adminCall(ctx, db, fieldsFor) {
|
|
3343
3454
|
const runtime = kitRuntimeOf(ctx);
|
|
3344
3455
|
const model = runtime?.service.model;
|
|
3345
3456
|
const projection = runtime?.service.projections.get("entity");
|
|
@@ -3349,6 +3460,7 @@ function adminCall(ctx, db, fields) {
|
|
|
3349
3460
|
"The admin kit's handlers run through a dispatcher, in a service with a model and an entity"
|
|
3350
3461
|
);
|
|
3351
3462
|
}
|
|
3463
|
+
const fields = fieldsFor(grantsColumnOf(runtime));
|
|
3352
3464
|
const { principal } = ctx;
|
|
3353
3465
|
const level = principal === null ? null : serviceGrant(principal, runtime.service.name) ?? null;
|
|
3354
3466
|
const tiered = projection.tiers.hidden(level);
|
|
@@ -3360,7 +3472,8 @@ function adminCall(ctx, db, fields) {
|
|
|
3360
3472
|
principal,
|
|
3361
3473
|
projection,
|
|
3362
3474
|
unseen,
|
|
3363
|
-
omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden])
|
|
3475
|
+
omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden]),
|
|
3476
|
+
fields
|
|
3364
3477
|
};
|
|
3365
3478
|
}
|
|
3366
3479
|
function rowOut(call, row) {
|
|
@@ -3384,7 +3497,7 @@ function checkSeen(call, names) {
|
|
|
3384
3497
|
var NO_NULLABLE = /* @__PURE__ */ new Set();
|
|
3385
3498
|
function adminListHandler(context) {
|
|
3386
3499
|
const handler = async ({ input, ctx, db }) => {
|
|
3387
|
-
const call = adminCall(ctx, db, context.
|
|
3500
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3388
3501
|
const query = input;
|
|
3389
3502
|
checkSeen(call, namedFields(query));
|
|
3390
3503
|
const order = listOrder(defaultSorts(context.spec.sort, call.unseen), query.sort);
|
|
@@ -3435,14 +3548,14 @@ function subscribersOf(call, id) {
|
|
|
3435
3548
|
}
|
|
3436
3549
|
function adminSubscribersHandler(context) {
|
|
3437
3550
|
const handler = ({ input, ctx, db }) => {
|
|
3438
|
-
const call = adminCall(ctx, db, context.
|
|
3551
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3439
3552
|
return Promise.resolve(subscribersOf(call, input.id));
|
|
3440
3553
|
};
|
|
3441
3554
|
return handler;
|
|
3442
3555
|
}
|
|
3443
3556
|
function adminReemitHandler(context) {
|
|
3444
3557
|
const handler = async ({ input, ctx, db }) => {
|
|
3445
|
-
const call = adminCall(ctx, db, context.
|
|
3558
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3446
3559
|
const { id } = input;
|
|
3447
3560
|
requireRow(
|
|
3448
3561
|
await call.table.findUnique({ where: { id }, select: { id: true } }),
|
|
@@ -3456,7 +3569,7 @@ function adminReemitHandler(context) {
|
|
|
3456
3569
|
|
|
3457
3570
|
// src/server/kits/admin/writes.ts
|
|
3458
3571
|
async function checkWrite(call, context, data) {
|
|
3459
|
-
const { hidden, readOnly } =
|
|
3572
|
+
const { hidden, readOnly } = call.fields;
|
|
3460
3573
|
const names = Object.keys(data);
|
|
3461
3574
|
const issues = names.filter((name) => hidden.has(name) || readOnly.has(name)).map((name) => ({
|
|
3462
3575
|
path: ["data", name],
|
|
@@ -3472,9 +3585,49 @@ async function checkWrite(call, context, data) {
|
|
|
3472
3585
|
call.runtime,
|
|
3473
3586
|
call.principal,
|
|
3474
3587
|
data,
|
|
3475
|
-
rowLevel(context.form, "Moderate")
|
|
3588
|
+
rowLevel(context.form, "Moderate"),
|
|
3589
|
+
["data"]
|
|
3476
3590
|
);
|
|
3477
3591
|
}
|
|
3592
|
+
function preparedIssues(call, columns) {
|
|
3593
|
+
const never = Object.keys(columns).filter((name) => ADMIN_NEVER_WRITABLE.includes(name)).map((name) => ({
|
|
3594
|
+
path: ["data", name],
|
|
3595
|
+
message: `"${name}" is not writable; the database sets it`
|
|
3596
|
+
}));
|
|
3597
|
+
return [...never, ...columnValueIssues(call.runtime.service, columns, ["data"])];
|
|
3598
|
+
}
|
|
3599
|
+
async function prepareWrite(call, context, write, ctx, db) {
|
|
3600
|
+
const { prepare } = context;
|
|
3601
|
+
const data = write.method === "adminDelete" ? {} : write.data;
|
|
3602
|
+
if (prepare === void 0) {
|
|
3603
|
+
return data;
|
|
3604
|
+
}
|
|
3605
|
+
const returned = await checked(() => prepare(write, ctx, db));
|
|
3606
|
+
if (write.method === "adminDelete" || returned === void 0) {
|
|
3607
|
+
return data;
|
|
3608
|
+
}
|
|
3609
|
+
const { name } = call.runtime.service;
|
|
3610
|
+
if (!isPlainObject(returned)) {
|
|
3611
|
+
throw new QuickdrawError(
|
|
3612
|
+
"INTERNAL",
|
|
3613
|
+
`The admin kit's prepare for ${name} must return column values, or nothing`
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
const columns = writtenData(returned);
|
|
3617
|
+
const issues = preparedIssues(call, columns);
|
|
3618
|
+
if (issues.length > 0) {
|
|
3619
|
+
throw new QuickdrawError(
|
|
3620
|
+
"VALIDATION",
|
|
3621
|
+
`The admin kit's prepare for ${name} returned data the kit does not write`,
|
|
3622
|
+
{ issues }
|
|
3623
|
+
);
|
|
3624
|
+
}
|
|
3625
|
+
const level = rowLevel(context.form, "Moderate");
|
|
3626
|
+
await checkAnchorColumns(call.runtime, call.principal, columns, level, {
|
|
3627
|
+
creating: write.method === "adminCreate"
|
|
3628
|
+
});
|
|
3629
|
+
return { ...data, ...columns };
|
|
3630
|
+
}
|
|
3478
3631
|
function isValueRefusal(error) {
|
|
3479
3632
|
if (!(error instanceof Error)) {
|
|
3480
3633
|
return false;
|
|
@@ -3516,7 +3669,7 @@ function committed(context, args, write) {
|
|
|
3516
3669
|
category: "quickdraw.admin",
|
|
3517
3670
|
method: write.method,
|
|
3518
3671
|
id: write.id,
|
|
3519
|
-
error:
|
|
3672
|
+
error: errorText(error)
|
|
3520
3673
|
});
|
|
3521
3674
|
};
|
|
3522
3675
|
try {
|
|
@@ -3525,43 +3678,71 @@ function committed(context, args, write) {
|
|
|
3525
3678
|
failed(error);
|
|
3526
3679
|
}
|
|
3527
3680
|
}
|
|
3528
|
-
async function
|
|
3529
|
-
const
|
|
3530
|
-
|
|
3681
|
+
async function rowBefore(context, call, table, write) {
|
|
3682
|
+
const readsAsItDeletes = write.method === "adminDelete" && context.prepare === void 0;
|
|
3683
|
+
if (write.method === "adminCreate" || readsAsItDeletes) {
|
|
3684
|
+
return void 0;
|
|
3685
|
+
}
|
|
3686
|
+
const row = await table.findUnique({ where: { id: write.id }, select: call.projection.select });
|
|
3687
|
+
return requireRow(row, `No such ${call.model}`);
|
|
3688
|
+
}
|
|
3689
|
+
function prepareCall(write, before) {
|
|
3690
|
+
if (write.method === "adminCreate") {
|
|
3691
|
+
return Object.freeze({ method: write.method, data: Object.freeze({ ...write.data }) });
|
|
3692
|
+
}
|
|
3693
|
+
if (write.method === "adminUpdate") {
|
|
3694
|
+
const data = Object.freeze({ ...write.data });
|
|
3695
|
+
return Object.freeze({ method: write.method, id: write.id, data, before });
|
|
3696
|
+
}
|
|
3697
|
+
return Object.freeze({ method: write.method, id: write.id, before });
|
|
3698
|
+
}
|
|
3699
|
+
async function written(context, call, args, write, perform) {
|
|
3700
|
+
const { prepare, onWrite, onCommitted } = context;
|
|
3701
|
+
const data = write.method === "adminDelete" ? {} : write.data;
|
|
3702
|
+
const attempt = async (table, values, before) => {
|
|
3531
3703
|
try {
|
|
3532
|
-
return await perform(table, before);
|
|
3704
|
+
return await perform({ table, values, before });
|
|
3533
3705
|
} catch (error) {
|
|
3534
3706
|
throw writeRefusal(error);
|
|
3535
3707
|
}
|
|
3536
3708
|
};
|
|
3537
|
-
if (onWrite === void 0 && onCommitted === void 0) {
|
|
3538
|
-
return (await attempt(call.table, false)).reply;
|
|
3709
|
+
if (prepare === void 0 && onWrite === void 0 && onCommitted === void 0) {
|
|
3710
|
+
return (await attempt(call.table, data, false)).reply;
|
|
3539
3711
|
}
|
|
3540
|
-
|
|
3712
|
+
const ctx = args.ctx;
|
|
3713
|
+
let heard;
|
|
3541
3714
|
const reply = await inKitTransaction(
|
|
3542
3715
|
args.db,
|
|
3543
3716
|
async (tx) => {
|
|
3544
|
-
const
|
|
3717
|
+
const table = tableOf2(tx, call.model);
|
|
3545
3718
|
const whole = (row) => row ? rowWhole(call, row) : null;
|
|
3546
|
-
|
|
3547
|
-
|
|
3719
|
+
const first = await rowBefore(context, call, table, write);
|
|
3720
|
+
const before = first === void 0 ? void 0 : whole(first);
|
|
3721
|
+
const values = await prepareWrite(call, context, prepareCall(write, before), ctx, tx);
|
|
3722
|
+
const done = await attempt(
|
|
3723
|
+
table,
|
|
3724
|
+
values,
|
|
3725
|
+
write.method === "adminDelete" && first === void 0
|
|
3726
|
+
);
|
|
3727
|
+
heard = {
|
|
3728
|
+
method: write.method,
|
|
3548
3729
|
id: done.id,
|
|
3549
|
-
...method === "adminCreate" ? {} : { before: whole(done.before) },
|
|
3730
|
+
...write.method === "adminCreate" ? {} : { before: before ?? whole(done.before) },
|
|
3550
3731
|
after: whole(done.after)
|
|
3551
3732
|
};
|
|
3552
|
-
await onWrite?.(
|
|
3733
|
+
await onWrite?.(heard, ctx, tx);
|
|
3553
3734
|
return done.reply;
|
|
3554
3735
|
},
|
|
3555
|
-
{ owner: "The admin kit's writes with onWrite or onCommitted" }
|
|
3736
|
+
{ owner: "The admin kit's writes with prepare, onWrite or onCommitted" }
|
|
3556
3737
|
);
|
|
3557
|
-
if (
|
|
3558
|
-
committed(context, args,
|
|
3738
|
+
if (heard !== void 0) {
|
|
3739
|
+
committed(context, args, heard);
|
|
3559
3740
|
}
|
|
3560
3741
|
return reply;
|
|
3561
3742
|
}
|
|
3562
3743
|
function adminGetHandler(context) {
|
|
3563
3744
|
const handler = async ({ input, ctx, db }) => {
|
|
3564
|
-
const call = adminCall(ctx, db, context.
|
|
3745
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3565
3746
|
return await readRow(call, input.id);
|
|
3566
3747
|
};
|
|
3567
3748
|
return handler;
|
|
@@ -3571,11 +3752,13 @@ function idOf(row) {
|
|
|
3571
3752
|
}
|
|
3572
3753
|
function adminCreateHandler(context) {
|
|
3573
3754
|
const handler = async (args) => {
|
|
3574
|
-
const call = adminCall(args.ctx, args.db, context.
|
|
3755
|
+
const call = adminCall(args.ctx, args.db, context.fieldsFor);
|
|
3575
3756
|
const { data } = args.input;
|
|
3576
3757
|
await checkWrite(call, context, data);
|
|
3577
|
-
|
|
3578
|
-
|
|
3758
|
+
const { select } = call.projection;
|
|
3759
|
+
const write = { method: "adminCreate", data };
|
|
3760
|
+
return await written(context, call, args, write, async ({ table, values }) => {
|
|
3761
|
+
const after = await table.create({ data: values, select });
|
|
3579
3762
|
return { id: idOf(after), after, reply: rowOut(call, after) };
|
|
3580
3763
|
});
|
|
3581
3764
|
};
|
|
@@ -3583,29 +3766,30 @@ function adminCreateHandler(context) {
|
|
|
3583
3766
|
}
|
|
3584
3767
|
function adminUpdateHandler(context) {
|
|
3585
3768
|
const handler = async (args) => {
|
|
3586
|
-
const call = adminCall(args.ctx, args.db, context.
|
|
3769
|
+
const call = adminCall(args.ctx, args.db, context.fieldsFor);
|
|
3587
3770
|
const { id, data } = args.input;
|
|
3588
3771
|
await checkWrite(call, context, data);
|
|
3589
3772
|
if (Object.keys(data).length === 0) {
|
|
3590
3773
|
return await readRow(call, id);
|
|
3591
3774
|
}
|
|
3592
3775
|
const { select } = call.projection;
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
const after = await table.update({ where: { id }, data, select });
|
|
3596
|
-
return { id,
|
|
3776
|
+
const write = { method: "adminUpdate", id, data };
|
|
3777
|
+
return await written(context, call, args, write, async ({ table, values }) => {
|
|
3778
|
+
const after = await table.update({ where: { id }, data: values, select });
|
|
3779
|
+
return { id, after, reply: rowOut(call, after) };
|
|
3597
3780
|
});
|
|
3598
3781
|
};
|
|
3599
3782
|
return handler;
|
|
3600
3783
|
}
|
|
3601
3784
|
function adminDeleteHandler(context) {
|
|
3602
3785
|
const handler = async (args) => {
|
|
3603
|
-
const call = adminCall(args.ctx, args.db, context.
|
|
3786
|
+
const call = adminCall(args.ctx, args.db, context.fieldsFor);
|
|
3604
3787
|
const { id } = args.input;
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
const
|
|
3608
|
-
|
|
3788
|
+
const write = { method: "adminDelete", id };
|
|
3789
|
+
return await written(context, call, args, write, async ({ table, before }) => {
|
|
3790
|
+
const select = before ? call.projection.select : { id: true };
|
|
3791
|
+
const row = await table.delete({ where: { id }, select });
|
|
3792
|
+
return { id, ...before ? { before: row } : {}, after: null, reply: null };
|
|
3609
3793
|
});
|
|
3610
3794
|
};
|
|
3611
3795
|
return handler;
|
|
@@ -3623,8 +3807,23 @@ var MAKERS2 = Object.freeze({
|
|
|
3623
3807
|
adminReemit: adminReemitHandler
|
|
3624
3808
|
});
|
|
3625
3809
|
function adminMetaHandler(context) {
|
|
3626
|
-
const
|
|
3627
|
-
const handler = (
|
|
3810
|
+
const byService = /* @__PURE__ */ new WeakMap();
|
|
3811
|
+
const handler = (args) => {
|
|
3812
|
+
const runtime = kitRuntimeOf(args.ctx);
|
|
3813
|
+
if (runtime === void 0) {
|
|
3814
|
+
return Promise.resolve(context.fieldsFor(void 0).meta);
|
|
3815
|
+
}
|
|
3816
|
+
const { service } = runtime;
|
|
3817
|
+
const column = grantsColumnFor(service.model, runtime.grantsSource);
|
|
3818
|
+
const answers = byService.get(service) ?? /* @__PURE__ */ new Map();
|
|
3819
|
+
byService.set(service, answers);
|
|
3820
|
+
let answer = answers.get(column ?? "");
|
|
3821
|
+
if (answer === void 0) {
|
|
3822
|
+
answer = withReadOnlyFields(context.fieldsFor(column).meta, relationKeysOf(service));
|
|
3823
|
+
answers.set(column ?? "", answer);
|
|
3824
|
+
}
|
|
3825
|
+
return Promise.resolve(answer);
|
|
3826
|
+
};
|
|
3628
3827
|
return handler;
|
|
3629
3828
|
}
|
|
3630
3829
|
function handlerOf3(context) {
|
|
@@ -3633,7 +3832,7 @@ function handlerOf3(context) {
|
|
|
3633
3832
|
}
|
|
3634
3833
|
|
|
3635
3834
|
// src/server/kits/admin/handlers.ts
|
|
3636
|
-
var
|
|
3835
|
+
var OPTION_KEYS3 = [
|
|
3637
3836
|
"access",
|
|
3638
3837
|
"displayName",
|
|
3639
3838
|
"hiddenFields",
|
|
@@ -3641,6 +3840,7 @@ var OPTION_KEYS4 = [
|
|
|
3641
3840
|
"fieldOverrides",
|
|
3642
3841
|
"rowless",
|
|
3643
3842
|
"grants",
|
|
3843
|
+
"prepare",
|
|
3644
3844
|
"onWrite",
|
|
3645
3845
|
"onCommitted"
|
|
3646
3846
|
];
|
|
@@ -3678,12 +3878,12 @@ function checkOptions3(options) {
|
|
|
3678
3878
|
}
|
|
3679
3879
|
if (!isRecord11(options)) {
|
|
3680
3880
|
fail4(
|
|
3681
|
-
"options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
|
|
3881
|
+
"options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, prepare?, onWrite?, onCommitted? }"
|
|
3682
3882
|
);
|
|
3683
3883
|
}
|
|
3684
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
3884
|
+
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
|
|
3685
3885
|
if (unknownKey !== void 0) {
|
|
3686
|
-
fail4(`options has an unknown key "${unknownKey}"; the options are ${
|
|
3886
|
+
fail4(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS3.join(", ")}`);
|
|
3687
3887
|
}
|
|
3688
3888
|
return options;
|
|
3689
3889
|
}
|
|
@@ -3719,7 +3919,7 @@ function contractEditable(contract, specs) {
|
|
|
3719
3919
|
}
|
|
3720
3920
|
return [...lists.values()][0];
|
|
3721
3921
|
}
|
|
3722
|
-
function fieldsOf(contract, kit, options) {
|
|
3922
|
+
function fieldsOf(contract, kit, options, grantsColumn) {
|
|
3723
3923
|
const specs = kit.map(([, spec2]) => spec2);
|
|
3724
3924
|
const [first] = specs;
|
|
3725
3925
|
const spec = {
|
|
@@ -3736,16 +3936,47 @@ function fieldsOf(contract, kit, options) {
|
|
|
3736
3936
|
hiddenFields: options.hiddenFields,
|
|
3737
3937
|
editable: options.editable,
|
|
3738
3938
|
fieldOverrides: options.fieldOverrides,
|
|
3739
|
-
grants: options.grants
|
|
3939
|
+
grants: options.grants,
|
|
3940
|
+
...grantsColumn === void 0 ? {} : { grantsColumn }
|
|
3740
3941
|
},
|
|
3741
3942
|
fail4
|
|
3742
3943
|
);
|
|
3743
3944
|
}
|
|
3744
|
-
function
|
|
3945
|
+
function fieldsByColumn(contract, kit, options) {
|
|
3946
|
+
const byColumn = /* @__PURE__ */ new Map();
|
|
3947
|
+
return (grantsColumn) => {
|
|
3948
|
+
let fields = byColumn.get(grantsColumn ?? "");
|
|
3949
|
+
if (fields === void 0) {
|
|
3950
|
+
fields = fieldsOf(contract, kit, options, grantsColumn);
|
|
3951
|
+
byColumn.set(grantsColumn ?? "", fields);
|
|
3952
|
+
}
|
|
3953
|
+
return fields;
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
function servedProblemOf(fieldsFor, grants, { service, grantsSource }) {
|
|
3957
|
+
const column = grantsColumnFor(service.model, grantsSource);
|
|
3958
|
+
let fields;
|
|
3959
|
+
try {
|
|
3960
|
+
fields = fieldsFor(column);
|
|
3961
|
+
} catch (error) {
|
|
3962
|
+
return error instanceof Error ? error.message : String(error);
|
|
3963
|
+
}
|
|
3964
|
+
if (grants && fields.grants.size === 0) {
|
|
3965
|
+
const named = column === void 0 ? `auth.serviceAccessSource names no column of model "${service.model ?? ""}"` : `the entity has no field "${column}", the column auth.serviceAccessSource names`;
|
|
3966
|
+
return `grants: true needs a grants field: the entity has no serviceAccess or service_access field, and ${named}`;
|
|
3967
|
+
}
|
|
3968
|
+
return void 0;
|
|
3969
|
+
}
|
|
3970
|
+
function serviceProblem4(service, contract, fields) {
|
|
3745
3971
|
if (service.contract !== contract) {
|
|
3746
3972
|
return `its admin kit handlers were made for another contract; pass ${service.name}'s own contract to admin.handlers`;
|
|
3747
3973
|
}
|
|
3748
|
-
|
|
3974
|
+
if (service.model === void 0) {
|
|
3975
|
+
return "the admin kit reads and writes the service's rows: declare its model";
|
|
3976
|
+
}
|
|
3977
|
+
const relations2 = relationKeysOf(service);
|
|
3978
|
+
const relation = [...fields.named].find((name) => relations2.has(name));
|
|
3979
|
+
return relation === void 0 ? void 0 : `"${relation}" is a relation of ${service.name}'s model (a projection selects it with a nested select): the admin kit writes column values only; leave it out of editable and fieldOverrides`;
|
|
3749
3980
|
}
|
|
3750
3981
|
function handlers4(contract, options) {
|
|
3751
3982
|
const kit = kitMethods4(contract);
|
|
@@ -3753,8 +3984,12 @@ function handlers4(contract, options) {
|
|
|
3753
3984
|
const names = kit.map(([name]) => name);
|
|
3754
3985
|
const access = checkAccess3(checked2.access, names);
|
|
3755
3986
|
const rowless = rowlessMethods(checked2.rowless, names, fail4);
|
|
3756
|
-
const
|
|
3757
|
-
const
|
|
3987
|
+
const fieldsFor = fieldsByColumn(contract, kit, checked2);
|
|
3988
|
+
const fields = fieldsFor(void 0);
|
|
3989
|
+
const { prepare, onWrite, onCommitted } = checked2;
|
|
3990
|
+
if (prepare !== void 0 && typeof prepare !== "function") {
|
|
3991
|
+
fail4("prepare must be a function of (call, ctx, db)");
|
|
3992
|
+
}
|
|
3758
3993
|
if (onWrite !== void 0 && typeof onWrite !== "function") {
|
|
3759
3994
|
fail4("onWrite must be a function of (write, ctx, db)");
|
|
3760
3995
|
}
|
|
@@ -3762,17 +3997,25 @@ function handlers4(contract, options) {
|
|
|
3762
3997
|
fail4("onCommitted must be a function of (write, ctx)");
|
|
3763
3998
|
}
|
|
3764
3999
|
const hooks = {
|
|
4000
|
+
prepare,
|
|
3765
4001
|
onWrite,
|
|
3766
4002
|
onCommitted
|
|
3767
4003
|
};
|
|
3768
4004
|
const entries = {};
|
|
3769
4005
|
for (const [name, spec] of kit) {
|
|
3770
4006
|
const form = access[name] ?? ADMIN_DEFAULT_ACCESS;
|
|
3771
|
-
const handler = handlerOf3({ spec,
|
|
3772
|
-
checkWhenDefined(handler, (service) => serviceProblem4(service, contract));
|
|
4007
|
+
const handler = handlerOf3({ spec, fieldsFor, form, ...hooks });
|
|
4008
|
+
checkWhenDefined(handler, (service) => serviceProblem4(service, contract, fields));
|
|
4009
|
+
checkWhenServed(
|
|
4010
|
+
handler,
|
|
4011
|
+
(served) => servedProblemOf(fieldsFor, checked2.grants === true, served)
|
|
4012
|
+
);
|
|
3773
4013
|
if (checked2.grants === true) {
|
|
3774
4014
|
markGrantsEditor(handler);
|
|
3775
4015
|
}
|
|
4016
|
+
if (spec.method === "adminList" || spec.method === "adminGet") {
|
|
4017
|
+
dependsOnCaller(handler);
|
|
4018
|
+
}
|
|
3776
4019
|
entries[name] = kitEntry(name, form, handler, rowless);
|
|
3777
4020
|
}
|
|
3778
4021
|
return Object.freeze(entries);
|
|
@@ -3816,7 +4059,8 @@ function reportErrors(client, logger, role) {
|
|
|
3816
4059
|
}
|
|
3817
4060
|
async function setupRedisAdapter(io, options = {}) {
|
|
3818
4061
|
const logger = options.logger?.child({ service: "RedisAdapter" }) ?? consoleLogger.child({ service: "RedisAdapter" });
|
|
3819
|
-
const { host = "localhost", port = 6379, password, db = 0
|
|
4062
|
+
const { host = "localhost", port = 6379, password, db = 0 } = options;
|
|
4063
|
+
const key = options.keyPrefix ?? deploymentOf(io);
|
|
3820
4064
|
try {
|
|
3821
4065
|
const peers = await loadRedisPeers();
|
|
3822
4066
|
const createAdapter = peers.createAdapter;
|
|
@@ -3830,10 +4074,9 @@ async function setupRedisAdapter(io, options = {}) {
|
|
|
3830
4074
|
reportErrors(pubClient, logger, "publish");
|
|
3831
4075
|
reportErrors(subClient, logger, "subscribe");
|
|
3832
4076
|
await Promise.all([pubClient.connect(), subClient.connect()]);
|
|
3833
|
-
io.adapter(
|
|
3834
|
-
createAdapter(pubClient, subClient, { key: keyPrefix })
|
|
3835
|
-
);
|
|
4077
|
+
io.adapter(createAdapter(pubClient, subClient, { key }));
|
|
3836
4078
|
watchAdapterClients(io, logger);
|
|
4079
|
+
seedRoomHosts(io);
|
|
3837
4080
|
logger.info(`Redis adapter connected to ${host}:${port}`);
|
|
3838
4081
|
const cleanup = async () => {
|
|
3839
4082
|
try {
|
|
@@ -3977,8 +4220,11 @@ export {
|
|
|
3977
4220
|
DevWarningError,
|
|
3978
4221
|
INDEX_MAX_ROWS,
|
|
3979
4222
|
MAX_APP_ROOMS,
|
|
4223
|
+
MAX_ENTITY_IDS_PER_SOCKET,
|
|
3980
4224
|
MAX_ITEM_IDS,
|
|
4225
|
+
MAX_SCOPES_PER_SOCKET,
|
|
3981
4226
|
MAX_STREAMS_PER_SOCKET,
|
|
4227
|
+
MAX_TOPICS_PER_SOCKET,
|
|
3982
4228
|
MIN_STALL_INTERVAL_MS,
|
|
3983
4229
|
N_PLUS_ONE_STATEMENTS,
|
|
3984
4230
|
ORDINAL_STEP,
|