@fitzzero/quickdraw-core 5.1.1 → 5.1.2
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 +533 -180
- package/dist/{admin-DKwkIh5i.d.ts → admin-DWWrT1h6.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-D4bFKsrb.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DXiSJZaf.d.ts} +42 -16
- package/dist/{caller-CXTFswBO.d.ts → caller-BT-Mv4bD.d.ts} +117 -22
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/chunk-5CGO426C.js +15 -0
- package/dist/chunk-5CGO426C.js.map +1 -0
- package/dist/{chunk-RZTP4IEW.js → chunk-6K5BY47X.js} +4594 -3481
- package/dist/chunk-6K5BY47X.js.map +1 -0
- package/dist/{chunk-VSQBHPWH.js → chunk-753VJIUP.js} +13 -15
- package/dist/chunk-753VJIUP.js.map +1 -0
- package/dist/{chunk-ZIVPHYRG.js → chunk-7QCUX4TK.js} +4 -4
- package/dist/chunk-7QCUX4TK.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/{chunk-SHEGLZEP.js → chunk-AONAFOYU.js} +14 -5
- package/dist/{chunk-SHEGLZEP.js.map → chunk-AONAFOYU.js.map} +1 -1
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.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-CDKG4TFA.js → chunk-GKH3LJ7N.js} +1 -1
- package/dist/chunk-GKH3LJ7N.js.map +1 -0
- package/dist/{chunk-OECZPRPW.js → chunk-GSIPUNJL.js} +49 -9
- package/dist/chunk-GSIPUNJL.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-LCC7HNFP.js} +12 -1
- package/dist/chunk-LCC7HNFP.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-MBH6IDSM.js} +480 -12
- package/dist/chunk-MBH6IDSM.js.map +1 -0
- package/dist/{chunk-QCHKWIRS.js → chunk-NMXYENH4.js} +10 -10
- package/dist/{chunk-QCHKWIRS.js.map → chunk-NMXYENH4.js.map} +1 -1
- 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-REX5YPHX.js → chunk-RJ5I2VO3.js} +74 -19
- package/dist/chunk-RJ5I2VO3.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-RL4VKLBX.js} +1 -1
- package/dist/{chunk-QEKVYG5F.js.map → chunk-RL4VKLBX.js.map} +1 -1
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-Z5ILULF2.js} +26 -7
- package/dist/chunk-Z5ILULF2.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +5 -3
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +30 -18
- package/dist/client/index.js +8 -6
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-hWcPUVS9.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-CgijdGpT.d.ts} +8 -4
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Cp-Wk500.d.ts} +36 -14
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-BB_PkqQ7.d.ts} +16 -10
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-DzWyIXjj.d.ts} +18 -4
- package/dist/{http-Djj2Inuu.d.ts → http-DotQ5bj8.d.ts} +68 -17
- package/dist/index.d.ts +7 -7
- package/dist/index.js +37 -13
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-BmoZsL9a.d.ts} +43 -10
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-DW80Exve.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/prisma/index.d.ts +5 -5
- package/dist/prisma/index.js +130 -67
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +273 -268
- 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 +147 -67
- package/dist/server/index.js +520 -381
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +63 -17
- package/dist/server/mcp/index.js +256 -54
- 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-UDLn1049.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +9 -7
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +12 -12
- package/dist/testing/index.js +21 -12
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +8 -6
- package/dist/{types-Cu0vmx7c.d.ts → types-CYliTP65.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.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-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-RZTP4IEW.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VSQBHPWH.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.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 → rateLimit-5RKBFLUR.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,20 @@ 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,
|
|
60
52
|
selectWith,
|
|
61
|
-
serviceGrant,
|
|
62
|
-
strip,
|
|
63
53
|
watchAdapterClients
|
|
64
|
-
} from "../chunk-
|
|
54
|
+
} from "../chunk-6K5BY47X.js";
|
|
65
55
|
import {
|
|
66
56
|
DevWarningError,
|
|
67
57
|
N_PLUS_ONE_STATEMENTS,
|
|
@@ -69,273 +59,93 @@ import {
|
|
|
69
59
|
currentRev,
|
|
70
60
|
formatDevWarning,
|
|
71
61
|
runBeforeAnyDispatcher
|
|
72
|
-
} from "../chunk-
|
|
62
|
+
} from "../chunk-Z5ILULF2.js";
|
|
73
63
|
import {
|
|
74
64
|
crud,
|
|
75
65
|
crudSpecOf,
|
|
76
66
|
sharing,
|
|
77
67
|
sharingSpecOf
|
|
78
|
-
} from "../chunk-
|
|
79
|
-
import "../chunk-
|
|
68
|
+
} from "../chunk-NMXYENH4.js";
|
|
69
|
+
import "../chunk-RL4VKLBX.js";
|
|
80
70
|
import {
|
|
71
|
+
SUBSCRIBER_LEVELS,
|
|
81
72
|
abortError,
|
|
82
73
|
callerGrantsOf,
|
|
74
|
+
checkWhenDefined,
|
|
75
|
+
checkWhenServed,
|
|
76
|
+
checksRowsItself,
|
|
77
|
+
compileProjections,
|
|
83
78
|
createCaller,
|
|
79
|
+
dependsOnCaller,
|
|
80
|
+
handlerProblem,
|
|
81
|
+
isCallerDependent,
|
|
84
82
|
kitRuntimeOf,
|
|
83
|
+
levelsAtLeast,
|
|
84
|
+
markGrantsEditor,
|
|
85
|
+
maxLevel,
|
|
86
|
+
meetsLevel,
|
|
87
|
+
narrowKinds,
|
|
85
88
|
outputSchemaOf,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
projectRow,
|
|
90
|
+
projectedOutput,
|
|
91
|
+
registerRuntime,
|
|
92
|
+
schemaOutputOf,
|
|
93
|
+
serviceGrant,
|
|
94
|
+
strip
|
|
95
|
+
} from "../chunk-MBH6IDSM.js";
|
|
96
|
+
import "../chunk-LCC7HNFP.js";
|
|
89
97
|
import {
|
|
90
98
|
recordSocketSession,
|
|
91
99
|
toCallReply,
|
|
92
100
|
unreadable
|
|
93
|
-
} from "../chunk-
|
|
101
|
+
} from "../chunk-RJ5I2VO3.js";
|
|
94
102
|
import {
|
|
95
103
|
ANY_FIELD,
|
|
96
104
|
modelKey,
|
|
97
105
|
storageOf
|
|
98
|
-
} from "../chunk-
|
|
106
|
+
} from "../chunk-DUBU3BB3.js";
|
|
99
107
|
import {
|
|
100
108
|
ADMIN_FIELD_TYPES,
|
|
101
109
|
ADMIN_NEVER_WRITABLE,
|
|
110
|
+
MAX_CURSOR_LENGTH,
|
|
102
111
|
admin,
|
|
103
112
|
adminSpecOf,
|
|
104
113
|
search,
|
|
105
114
|
searchSpecOf
|
|
106
|
-
} from "../chunk-
|
|
115
|
+
} from "../chunk-GKH3LJ7N.js";
|
|
107
116
|
import {
|
|
108
|
-
|
|
117
|
+
MAX_ENTITY_IDS_PER_SOCKET,
|
|
118
|
+
MAX_SCOPES_PER_SOCKET,
|
|
119
|
+
MAX_TOPICS_PER_SOCKET,
|
|
109
120
|
entityRoom,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
} from "../chunk-
|
|
121
|
+
reservedRoomPrefix,
|
|
122
|
+
socketIdShaped
|
|
123
|
+
} from "../chunk-753VJIUP.js";
|
|
113
124
|
import {
|
|
114
125
|
CHANNEL_DEFAULT_RATE,
|
|
115
|
-
DEFAULT_COLLECTION_LIMIT,
|
|
116
|
-
DEFAULT_COLLECTION_MAX_LIMIT,
|
|
117
126
|
isScopedStream
|
|
118
|
-
} from "../chunk-
|
|
127
|
+
} from "../chunk-5CGO426C.js";
|
|
128
|
+
import "../chunk-4D2DOGAW.js";
|
|
129
|
+
import {
|
|
130
|
+
ACCESS_LEVELS,
|
|
131
|
+
isAccessLevel
|
|
132
|
+
} from "../chunk-AK23XSWD.js";
|
|
119
133
|
import "../chunk-47AEXVDM.js";
|
|
120
134
|
import "../chunk-Z2LZ7IZ7.js";
|
|
121
135
|
import {
|
|
122
|
-
consoleLogger
|
|
123
|
-
|
|
136
|
+
consoleLogger,
|
|
137
|
+
errorText
|
|
138
|
+
} from "../chunk-DKE6QO26.js";
|
|
124
139
|
import {
|
|
125
140
|
QuickdrawError
|
|
126
141
|
} from "../chunk-4YO5H5ON.js";
|
|
127
142
|
|
|
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
|
-
}
|
|
332
|
-
|
|
333
143
|
// src/server/realtime/define.ts
|
|
334
|
-
function
|
|
144
|
+
function isRecord(value) {
|
|
335
145
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
336
146
|
}
|
|
337
147
|
function compileSelector(selector) {
|
|
338
|
-
const read = typeof selector === "function" ? selector : (payload) =>
|
|
148
|
+
const read = typeof selector === "function" ? selector : (payload) => isRecord(payload) ? payload[selector] : void 0;
|
|
339
149
|
return (payload) => {
|
|
340
150
|
try {
|
|
341
151
|
const value = read(payload);
|
|
@@ -352,7 +162,7 @@ function compileRoom(room) {
|
|
|
352
162
|
const computed = compileSelector(room);
|
|
353
163
|
return (payload) => {
|
|
354
164
|
const name = computed(payload);
|
|
355
|
-
return name === void 0 || reservedRoomPrefix(name) !== void 0 ? void 0 : name;
|
|
165
|
+
return name === void 0 || reservedRoomPrefix(name) !== void 0 || socketIdShaped(name) ? void 0 : name;
|
|
356
166
|
};
|
|
357
167
|
}
|
|
358
168
|
function compileRequires(def) {
|
|
@@ -380,8 +190,8 @@ function checkChannelAccess(owner2, access, fail5) {
|
|
|
380
190
|
if (access === void 0 || access === "authenticated") {
|
|
381
191
|
return "authenticated";
|
|
382
192
|
}
|
|
383
|
-
const keys =
|
|
384
|
-
if (!
|
|
193
|
+
const keys = isRecord(access) ? Object.keys(access) : [];
|
|
194
|
+
if (!isRecord(access) || keys.length !== 1 || !isAccessLevel(access.service)) {
|
|
385
195
|
fail5(`${owner2}: access must be "authenticated" or { service: level }`);
|
|
386
196
|
}
|
|
387
197
|
return Object.freeze({ service: access.service });
|
|
@@ -389,7 +199,7 @@ function checkChannelAccess(owner2, access, fail5) {
|
|
|
389
199
|
function checkChannel(service, name, def, entry, kinds, fail5) {
|
|
390
200
|
const owner2 = `channel "${name}"`;
|
|
391
201
|
const implementation = typeof entry === "function" ? { handler: entry } : entry;
|
|
392
|
-
if (!
|
|
202
|
+
if (!isRecord(implementation) || typeof implementation.handler !== "function") {
|
|
393
203
|
fail5(`${owner2} must be a handler function or { access, handler }`);
|
|
394
204
|
}
|
|
395
205
|
const unknownKey = Object.keys(implementation).find(
|
|
@@ -408,6 +218,7 @@ function checkChannel(service, name, def, entry, kinds, fail5) {
|
|
|
408
218
|
burst: def.burst ?? Math.max(1, ratePerSecond * 2),
|
|
409
219
|
requires: compileRequires(def),
|
|
410
220
|
access: checkChannelAccess(owner2, implementation.access, fail5),
|
|
221
|
+
accessDeclared: implementation.access !== void 0,
|
|
411
222
|
kinds,
|
|
412
223
|
handler: implementation.handler
|
|
413
224
|
});
|
|
@@ -417,7 +228,7 @@ function compileChannels(contract, value, kinds, fail5) {
|
|
|
417
228
|
if (value === void 0 && declared.length === 0) {
|
|
418
229
|
return /* @__PURE__ */ new Map();
|
|
419
230
|
}
|
|
420
|
-
if (!
|
|
231
|
+
if (!isRecord(value)) {
|
|
421
232
|
fail5("channels must be an object with one implementation per contract channel");
|
|
422
233
|
}
|
|
423
234
|
const missing2 = declared.filter(([name]) => !Object.hasOwn(value, name)).map(([name]) => name);
|
|
@@ -474,7 +285,7 @@ function checkStreamOptions(name, def, entry, fail5) {
|
|
|
474
285
|
if (entry === void 0) {
|
|
475
286
|
return { computeSeed: void 0, validate: "always" };
|
|
476
287
|
}
|
|
477
|
-
if (!
|
|
288
|
+
if (!isRecord(entry)) {
|
|
478
289
|
fail5(`${owner2} must be an object: { seed?, validate? }`);
|
|
479
290
|
}
|
|
480
291
|
const unknownKey = Object.keys(entry).find((key) => !STREAM_OPTION_KEYS.includes(key));
|
|
@@ -522,7 +333,7 @@ function checkStream(name, def, needs, options, fail5) {
|
|
|
522
333
|
});
|
|
523
334
|
}
|
|
524
335
|
function compileStreams(contract, needs, value, fail5) {
|
|
525
|
-
if (value !== void 0 && !
|
|
336
|
+
if (value !== void 0 && !isRecord(value)) {
|
|
526
337
|
fail5(
|
|
527
338
|
"streams must be an object of options per contract stream: { <stream>: { seed?, validate? } }"
|
|
528
339
|
);
|
|
@@ -541,14 +352,14 @@ function compileStreams(contract, needs, value, fail5) {
|
|
|
541
352
|
}
|
|
542
353
|
|
|
543
354
|
// src/server/serviceData.ts
|
|
544
|
-
function
|
|
355
|
+
function isRecord2(value) {
|
|
545
356
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
546
357
|
}
|
|
547
358
|
function isName(value) {
|
|
548
359
|
return typeof value === "string" && value.length > 0;
|
|
549
360
|
}
|
|
550
|
-
function
|
|
551
|
-
return
|
|
361
|
+
function isContract(value) {
|
|
362
|
+
return isRecord2(value) && isName(value.name) && Object.isFrozen(value);
|
|
552
363
|
}
|
|
553
364
|
function idsIn(value) {
|
|
554
365
|
if (isName(value)) {
|
|
@@ -558,7 +369,7 @@ function idsIn(value) {
|
|
|
558
369
|
}
|
|
559
370
|
function checkLink(index, entry, fail5) {
|
|
560
371
|
const owner2 = `affects[${index}]`;
|
|
561
|
-
if (!
|
|
372
|
+
if (!isRecord2(entry) || !isContract(entry.service)) {
|
|
562
373
|
fail5(`${owner2} must be { service: contract, id }`);
|
|
563
374
|
}
|
|
564
375
|
const { id, columns } = entry;
|
|
@@ -613,7 +424,7 @@ function checkWatchAccess(value, fail5) {
|
|
|
613
424
|
if (value === "public" || value === "authenticated") {
|
|
614
425
|
return value;
|
|
615
426
|
}
|
|
616
|
-
if (!
|
|
427
|
+
if (!isRecord2(value) || Object.keys(value).length !== 1 || !isAccessLevel(value.service)) {
|
|
617
428
|
fail5('watchAccess must be "public", "authenticated" or { service: level }');
|
|
618
429
|
}
|
|
619
430
|
return Object.freeze({ service: value.service });
|
|
@@ -635,6 +446,9 @@ function checkMethodKinds(owner2, entry, data, fail5) {
|
|
|
635
446
|
(message) => fail5(`${owner2}: ${message}`)
|
|
636
447
|
);
|
|
637
448
|
}
|
|
449
|
+
function checkMethodClaims(owner2, entry, fail5) {
|
|
450
|
+
return claimsOption(owner2, entry, fail5);
|
|
451
|
+
}
|
|
638
452
|
function checkServiceData(definition, appKinds, fail5) {
|
|
639
453
|
const { model, access, versionColumn } = definition;
|
|
640
454
|
if (model !== void 0 && !isName(model)) {
|
|
@@ -703,9 +517,10 @@ var METHOD_KEYS = /* @__PURE__ */ new Set([
|
|
|
703
517
|
"timeoutMs",
|
|
704
518
|
"version",
|
|
705
519
|
"rowless",
|
|
706
|
-
"kinds"
|
|
520
|
+
"kinds",
|
|
521
|
+
"claims"
|
|
707
522
|
]);
|
|
708
|
-
function
|
|
523
|
+
function isRecord3(value) {
|
|
709
524
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
710
525
|
}
|
|
711
526
|
function checkKeys(value, allowed, owner2, fail5) {
|
|
@@ -720,7 +535,7 @@ function isDuration(value, max) {
|
|
|
720
535
|
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max;
|
|
721
536
|
}
|
|
722
537
|
function checkContract(contract) {
|
|
723
|
-
const valid =
|
|
538
|
+
const valid = isRecord3(contract) && typeof contract.name === "string" && contract.name.length > 0 && isRecord3(contract.methods) && Object.isFrozen(contract);
|
|
724
539
|
if (!valid) {
|
|
725
540
|
throw new TypeError("defineService: the first argument must be a contract from defineContract");
|
|
726
541
|
}
|
|
@@ -731,11 +546,16 @@ function checkShare(owner2, entry, fail5) {
|
|
|
731
546
|
if (share !== void 0 && share !== "caller" && share !== "all") {
|
|
732
547
|
fail5(`${owner2}: share must be "caller" or "all"`);
|
|
733
548
|
}
|
|
734
|
-
if (share === "all" &&
|
|
549
|
+
if (share === "all" && isRecord3(entry.access) && entry.access.kind === "custom") {
|
|
735
550
|
fail5(
|
|
736
551
|
`${owner2} has custom access, so it cannot share: "all"; its result may depend on who asks`
|
|
737
552
|
);
|
|
738
553
|
}
|
|
554
|
+
if (share === "all" && typeof entry.handler === "function" && isCallerDependent(entry.handler)) {
|
|
555
|
+
fail5(
|
|
556
|
+
`${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`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
739
559
|
if (ttlMs !== void 0 && (share === void 0 || !isDuration(ttlMs, MAX_TIMEOUT_MS))) {
|
|
740
560
|
fail5(`${owner2}: ttlMs needs share and must be a positive number of milliseconds`);
|
|
741
561
|
}
|
|
@@ -756,7 +576,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
756
576
|
if (def === void 0) {
|
|
757
577
|
fail5(`"${name}" is not a method of the contract`);
|
|
758
578
|
}
|
|
759
|
-
if (!
|
|
579
|
+
if (!isRecord3(entry)) {
|
|
760
580
|
fail5(`${owner2} must be { access, handler }`);
|
|
761
581
|
}
|
|
762
582
|
checkKeys(entry, METHOD_KEYS, owner2, fail5);
|
|
@@ -777,6 +597,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
777
597
|
}
|
|
778
598
|
checkQueryOptions(owner2, entry, def.kind, fail5);
|
|
779
599
|
const kinds = checkMethodKinds(owner2, entry, data, fail5);
|
|
600
|
+
const claims = checkMethodClaims(owner2, entry, fail5);
|
|
780
601
|
const projection = projectedOutput(def.output, projections);
|
|
781
602
|
return Object.freeze({
|
|
782
603
|
name,
|
|
@@ -787,6 +608,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
787
608
|
schemaOutput: projection === void 0 ? schemaOutputOf(def.output) : void 0,
|
|
788
609
|
access: entry.access,
|
|
789
610
|
kinds,
|
|
611
|
+
claims,
|
|
790
612
|
handler: entry.handler,
|
|
791
613
|
share: entry.share,
|
|
792
614
|
ttlMs: entry.ttlMs,
|
|
@@ -796,7 +618,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
|
|
|
796
618
|
});
|
|
797
619
|
}
|
|
798
620
|
function checkMethods(contract, projections, data, value, fail5) {
|
|
799
|
-
if (!
|
|
621
|
+
if (!isRecord3(value)) {
|
|
800
622
|
fail5("methods must be an object with one implementation per contract method");
|
|
801
623
|
}
|
|
802
624
|
const missing2 = Object.keys(contract.methods).filter((name) => !Object.hasOwn(value, name));
|
|
@@ -818,7 +640,7 @@ function checkWatches(contract, collections, data, fail5) {
|
|
|
818
640
|
if (def.kind !== "query") {
|
|
819
641
|
fail5(`method "${name}" is a mutation; only a query can watch`);
|
|
820
642
|
}
|
|
821
|
-
if (watch === "service" ||
|
|
643
|
+
if (watch === "service" || isRecord3(watch) && Object.hasOwn(watch, "service")) {
|
|
822
644
|
if (data.watchAccess === void 0) {
|
|
823
645
|
fail5(
|
|
824
646
|
`method "${name}" watches the service's topic, which is closed without watchAccess: declare who may watch it ("public", "authenticated" or { service: level })`
|
|
@@ -829,7 +651,7 @@ function checkWatches(contract, collections, data, fail5) {
|
|
|
829
651
|
}
|
|
830
652
|
continue;
|
|
831
653
|
}
|
|
832
|
-
if (!
|
|
654
|
+
if (!isRecord3(watch) || typeof watch.scope !== "function") {
|
|
833
655
|
fail5(`method "${name}": watch needs a scope function, which finds the scope from the input`);
|
|
834
656
|
}
|
|
835
657
|
if (typeof watch.collection !== "string" || !collections.has(watch.collection)) {
|
|
@@ -857,12 +679,12 @@ function checkRowForms(methods, data, fail5) {
|
|
|
857
679
|
}
|
|
858
680
|
}
|
|
859
681
|
}
|
|
860
|
-
function checkRowless(methods, data, fail5) {
|
|
682
|
+
function checkRowless(contract, methods, data, fail5) {
|
|
861
683
|
if (data.access === void 0) {
|
|
862
684
|
return;
|
|
863
685
|
}
|
|
864
686
|
for (const method of Object.values(methods)) {
|
|
865
|
-
const problem = rowlessProblem(method);
|
|
687
|
+
const problem = rowlessProblem(method, contract.name);
|
|
866
688
|
if (problem !== void 0) {
|
|
867
689
|
fail5(problem);
|
|
868
690
|
}
|
|
@@ -898,7 +720,7 @@ function buildService(runtime, contract, definition) {
|
|
|
898
720
|
const fail5 = (message) => {
|
|
899
721
|
throw new TypeError(`defineService("${checked2.name}"): ${message}`);
|
|
900
722
|
};
|
|
901
|
-
if (!
|
|
723
|
+
if (!isRecord3(definition)) {
|
|
902
724
|
fail5("the definition must be an object");
|
|
903
725
|
}
|
|
904
726
|
checkKeys(definition, DEFINITION_KEYS, "the definition", fail5);
|
|
@@ -916,12 +738,13 @@ function buildService(runtime, contract, definition) {
|
|
|
916
738
|
projections,
|
|
917
739
|
data.model,
|
|
918
740
|
definition.collections,
|
|
919
|
-
fail5
|
|
741
|
+
fail5,
|
|
742
|
+
data.access
|
|
920
743
|
);
|
|
921
744
|
checkWatches(checked2, collections, data, fail5);
|
|
922
745
|
const methods = checkMethods(checked2, projections, data, definition.methods, fail5);
|
|
923
746
|
checkRowForms(methods, data, fail5);
|
|
924
|
-
checkRowless(methods, data, fail5);
|
|
747
|
+
checkRowless(checked2, methods, data, fail5);
|
|
925
748
|
const service = Object.freeze({
|
|
926
749
|
name: checked2.name,
|
|
927
750
|
contract: checked2,
|
|
@@ -1060,7 +883,7 @@ var COLUMNS = /* @__PURE__ */ new WeakMap();
|
|
|
1060
883
|
function jsonAclColumnsOf(policy) {
|
|
1061
884
|
return COLUMNS.get(policy);
|
|
1062
885
|
}
|
|
1063
|
-
function
|
|
886
|
+
function isRecord4(value) {
|
|
1064
887
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1065
888
|
}
|
|
1066
889
|
function aclLevel(acl, userId) {
|
|
@@ -1069,7 +892,7 @@ function aclLevel(acl, userId) {
|
|
|
1069
892
|
}
|
|
1070
893
|
let level = null;
|
|
1071
894
|
for (const entry of acl) {
|
|
1072
|
-
if (
|
|
895
|
+
if (isRecord4(entry) && entry.userId === userId) {
|
|
1073
896
|
level = maxLevel(level, entry.level);
|
|
1074
897
|
}
|
|
1075
898
|
}
|
|
@@ -1115,11 +938,11 @@ function jsonAcl(field, options = {}) {
|
|
|
1115
938
|
}
|
|
1116
939
|
|
|
1117
940
|
// src/server/access/policies/inherit.ts
|
|
1118
|
-
function
|
|
941
|
+
function isContract2(value) {
|
|
1119
942
|
return typeof value === "object" && value !== null && typeof value.name === "string" && Object.isFrozen(value);
|
|
1120
943
|
}
|
|
1121
944
|
function inherit(options) {
|
|
1122
|
-
if (typeof options !== "object" || options === null || !
|
|
945
|
+
if (typeof options !== "object" || options === null || !isContract2(options.from)) {
|
|
1123
946
|
throw new TypeError("inherit({ from, via }): from must be the parent service's contract");
|
|
1124
947
|
}
|
|
1125
948
|
const { from } = options;
|
|
@@ -1202,6 +1025,10 @@ function rowLevel(form, fallback) {
|
|
|
1202
1025
|
}
|
|
1203
1026
|
return form.entry ?? form.scope ?? fallback;
|
|
1204
1027
|
}
|
|
1028
|
+
function bulkFallback(counterpart) {
|
|
1029
|
+
const level = counterpart === void 0 ? "Moderate" : rowLevel(counterpart, "Moderate");
|
|
1030
|
+
return meetsLevel(level, "Moderate") ? level : "Moderate";
|
|
1031
|
+
}
|
|
1205
1032
|
function unchecked(call, form, use) {
|
|
1206
1033
|
return call.runtime.service.access === void 0 || use === "read" && form === "public";
|
|
1207
1034
|
}
|
|
@@ -1228,8 +1055,8 @@ function grantRowLevel(call, form) {
|
|
|
1228
1055
|
function grantReaches(call, form, level) {
|
|
1229
1056
|
return meetsLevel(grantRowLevel(call, form), level);
|
|
1230
1057
|
}
|
|
1231
|
-
async function rowsWhere(call, form, level) {
|
|
1232
|
-
if (unchecked(call, form,
|
|
1058
|
+
async function rowsWhere(call, form, level, use = "read") {
|
|
1059
|
+
if (unchecked(call, form, use) || grantReaches(call, form, level)) {
|
|
1233
1060
|
return void 0;
|
|
1234
1061
|
}
|
|
1235
1062
|
if (call.principal === null) {
|
|
@@ -1291,29 +1118,85 @@ function guardedColumns(service) {
|
|
|
1291
1118
|
add(collection.scope.column, collection.anchor.name);
|
|
1292
1119
|
}
|
|
1293
1120
|
}
|
|
1294
|
-
const
|
|
1295
|
-
const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !
|
|
1121
|
+
const parents = new Set((reads?.parents ?? []).map(({ via }) => via));
|
|
1122
|
+
const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !parents.has(column));
|
|
1296
1123
|
return { locked: new Set(locked), anchors: [...anchors.values()] };
|
|
1297
1124
|
}
|
|
1125
|
+
var MEMBERSHIPS = /* @__PURE__ */ new WeakMap();
|
|
1126
|
+
function membershipColumnsOf(services, model) {
|
|
1127
|
+
let byModel = MEMBERSHIPS.get(services);
|
|
1128
|
+
if (byModel === void 0) {
|
|
1129
|
+
byModel = /* @__PURE__ */ new Map();
|
|
1130
|
+
MEMBERSHIPS.set(services, byModel);
|
|
1131
|
+
}
|
|
1132
|
+
const key = modelKey(model);
|
|
1133
|
+
const kept = byModel.get(key);
|
|
1134
|
+
if (kept !== void 0) {
|
|
1135
|
+
return kept;
|
|
1136
|
+
}
|
|
1137
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1138
|
+
for (const service of services.values()) {
|
|
1139
|
+
for (const read of service.access?.reads.memberships ?? []) {
|
|
1140
|
+
if (modelKey(read.model) !== key) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
for (const column of [read.entry, read.user, read.level]) {
|
|
1144
|
+
if (!columns.has(column)) {
|
|
1145
|
+
columns.set(column, service.name);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
byModel.set(key, columns);
|
|
1151
|
+
return columns;
|
|
1152
|
+
}
|
|
1298
1153
|
function forbidden(message) {
|
|
1299
1154
|
return new QuickdrawError("FORBIDDEN", message);
|
|
1300
1155
|
}
|
|
1301
1156
|
function bypasses(service, principal) {
|
|
1302
1157
|
return principal !== null && service.adminBypass && serviceGrant(principal, service.name) === "Admin";
|
|
1303
1158
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1159
|
+
function namedColumns(data) {
|
|
1160
|
+
return new Set(Object.keys(data).filter((column) => data[column] !== void 0));
|
|
1161
|
+
}
|
|
1162
|
+
function membershipNamed(runtime, columns) {
|
|
1163
|
+
const { service, services } = runtime;
|
|
1164
|
+
if (service.model === void 0) {
|
|
1165
|
+
return void 0;
|
|
1166
|
+
}
|
|
1167
|
+
const memberships = membershipColumnsOf(services, service.model);
|
|
1168
|
+
const column = columns.find((named) => memberships.has(named));
|
|
1169
|
+
return column === void 0 ? void 0 : [column, memberships.get(column) ?? service.name];
|
|
1170
|
+
}
|
|
1171
|
+
function checkLockedColumns(runtime, principal, data, options = {}) {
|
|
1172
|
+
const { service } = runtime;
|
|
1306
1173
|
if (bypasses(service, principal)) {
|
|
1307
1174
|
return;
|
|
1308
1175
|
}
|
|
1309
|
-
const { locked
|
|
1310
|
-
const named =
|
|
1311
|
-
const readOnly =
|
|
1176
|
+
const { locked } = guardedColumns(service);
|
|
1177
|
+
const named = [...namedColumns(data)];
|
|
1178
|
+
const readOnly = named.find((column) => locked.has(column));
|
|
1179
|
+
const creating = options.creating === true;
|
|
1312
1180
|
if (readOnly !== void 0) {
|
|
1313
1181
|
throw forbidden(
|
|
1314
|
-
`"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may change it`
|
|
1182
|
+
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`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const membership = membershipNamed(runtime, named);
|
|
1186
|
+
if (membership !== void 0) {
|
|
1187
|
+
const [column, reader] = membership;
|
|
1188
|
+
throw forbidden(
|
|
1189
|
+
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`
|
|
1315
1190
|
);
|
|
1316
1191
|
}
|
|
1192
|
+
}
|
|
1193
|
+
async function checkAnchorColumns(runtime, principal, data, level, options = {}) {
|
|
1194
|
+
const { service, access } = runtime;
|
|
1195
|
+
if (bypasses(service, principal)) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const { anchors } = guardedColumns(service);
|
|
1199
|
+
const named = namedColumns(data);
|
|
1317
1200
|
for (const { column, anchor } of anchors) {
|
|
1318
1201
|
const target = data[column];
|
|
1319
1202
|
if (!named.has(column) || target === null) {
|
|
@@ -1323,11 +1206,75 @@ async function checkWritableColumns(runtime, principal, data, level) {
|
|
|
1323
1206
|
const resolved = valid ? await access.resolve(anchor, principal, [target], { grants: false }) : void 0;
|
|
1324
1207
|
if (resolved === void 0 || !meetsLevel(resolved.levels.get(target), level)) {
|
|
1325
1208
|
throw forbidden(
|
|
1326
|
-
`Moving a row of ${service.name} by "${column}" needs ${level} on the ${anchor} row it moves into`
|
|
1209
|
+
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
1210
|
);
|
|
1328
1211
|
}
|
|
1329
1212
|
}
|
|
1330
1213
|
}
|
|
1214
|
+
var relations = /* @__PURE__ */ new WeakMap();
|
|
1215
|
+
function relationKeysOf(service) {
|
|
1216
|
+
let keys = relations.get(service);
|
|
1217
|
+
if (keys === void 0) {
|
|
1218
|
+
const found = /* @__PURE__ */ new Set();
|
|
1219
|
+
for (const projection of service.projections.values()) {
|
|
1220
|
+
for (const [key, value] of Object.entries(projection.select)) {
|
|
1221
|
+
if (typeof value === "object" && value !== null) {
|
|
1222
|
+
found.add(key);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
keys = found;
|
|
1227
|
+
relations.set(service, keys);
|
|
1228
|
+
}
|
|
1229
|
+
return keys;
|
|
1230
|
+
}
|
|
1231
|
+
function isPlainObject(value) {
|
|
1232
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1236
|
+
return prototype === Object.prototype || prototype === null;
|
|
1237
|
+
}
|
|
1238
|
+
function columnValueIssues(service, data, path = []) {
|
|
1239
|
+
const relationKeys = relationKeysOf(service);
|
|
1240
|
+
const nested = new Map(
|
|
1241
|
+
findNestedWrites({ ...data }).map(({ field, operation }) => [field, operation])
|
|
1242
|
+
);
|
|
1243
|
+
const issues = [];
|
|
1244
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1245
|
+
if (value === void 0) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const operation = nested.get(key) ?? (isPlainObject(value) && Array.isArray(value.set) ? "set" : void 0);
|
|
1249
|
+
if (relationKeys.has(key)) {
|
|
1250
|
+
issues.push({
|
|
1251
|
+
path: [...path, key],
|
|
1252
|
+
message: `"${key}" is a relation of ${service.name}'s model: kits write column values only; set its foreign key column instead`
|
|
1253
|
+
});
|
|
1254
|
+
} else if (operation !== void 0) {
|
|
1255
|
+
issues.push({
|
|
1256
|
+
path: [...path, key],
|
|
1257
|
+
message: `"${key}" holds a nested write ({ ${operation} }), which is not tracked: kits write column values only`
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return issues;
|
|
1262
|
+
}
|
|
1263
|
+
function checkColumnValues(service, data, path = []) {
|
|
1264
|
+
const issues = columnValueIssues(service, data, path);
|
|
1265
|
+
if (issues.length > 0) {
|
|
1266
|
+
throw new QuickdrawError(
|
|
1267
|
+
"VALIDATION",
|
|
1268
|
+
"The data holds a relation or a nested write, which kits do not write",
|
|
1269
|
+
{ issues }
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
async function checkWritableColumns(runtime, principal, data, level, path = []) {
|
|
1274
|
+
checkColumnValues(runtime.service, data, path);
|
|
1275
|
+
checkLockedColumns(runtime, principal, data);
|
|
1276
|
+
await checkAnchorColumns(runtime, principal, data, level);
|
|
1277
|
+
}
|
|
1331
1278
|
|
|
1332
1279
|
// src/server/kits/transactions.ts
|
|
1333
1280
|
function isWriteConflict(error) {
|
|
@@ -1447,13 +1394,13 @@ function updateHandler(form) {
|
|
|
1447
1394
|
}
|
|
1448
1395
|
|
|
1449
1396
|
// src/server/kits/crud/bulk.ts
|
|
1450
|
-
async function writeAllowed(call, db, form, ids, write) {
|
|
1397
|
+
async function writeAllowed(call, db, [form, level], ids, write) {
|
|
1451
1398
|
const unique = uniqueIds(ids);
|
|
1452
1399
|
if (unique.length === 0) {
|
|
1453
1400
|
return { count: 0 };
|
|
1454
1401
|
}
|
|
1455
1402
|
return await inTransaction(db, async (tx) => {
|
|
1456
|
-
const allowed = await allowedIds(call, form, unique,
|
|
1403
|
+
const allowed = await allowedIds(call, form, unique, level, "write");
|
|
1457
1404
|
if (allowed.length === 0) {
|
|
1458
1405
|
return { count: 0 };
|
|
1459
1406
|
}
|
|
@@ -1461,7 +1408,8 @@ async function writeAllowed(call, db, form, ids, write) {
|
|
|
1461
1408
|
return { count };
|
|
1462
1409
|
});
|
|
1463
1410
|
}
|
|
1464
|
-
function bulkUpdateHandler(form) {
|
|
1411
|
+
function bulkUpdateHandler(form, fallback) {
|
|
1412
|
+
const level = rowLevel(form, fallback);
|
|
1465
1413
|
const handler = async ({ input, ctx, db }) => {
|
|
1466
1414
|
const call = crudCall(ctx, db);
|
|
1467
1415
|
const { ids, data } = input;
|
|
@@ -1469,24 +1417,25 @@ function bulkUpdateHandler(form) {
|
|
|
1469
1417
|
if (Object.keys(patch).length === 0) {
|
|
1470
1418
|
return { count: 0 };
|
|
1471
1419
|
}
|
|
1472
|
-
await checkWritableColumns(call.runtime, call.principal, patch,
|
|
1420
|
+
await checkWritableColumns(call.runtime, call.principal, patch, level, ["data"]);
|
|
1473
1421
|
return await writeAllowed(
|
|
1474
1422
|
call,
|
|
1475
1423
|
db,
|
|
1476
|
-
form,
|
|
1424
|
+
[form, level],
|
|
1477
1425
|
ids,
|
|
1478
1426
|
(table, allowed) => table.updateMany({ where: { id: { in: [...allowed] } }, data: patch })
|
|
1479
1427
|
);
|
|
1480
1428
|
};
|
|
1481
1429
|
return handler;
|
|
1482
1430
|
}
|
|
1483
|
-
function bulkDeleteHandler(form) {
|
|
1431
|
+
function bulkDeleteHandler(form, fallback) {
|
|
1432
|
+
const level = rowLevel(form, fallback);
|
|
1484
1433
|
const handler = async ({ input, ctx, db }) => {
|
|
1485
1434
|
const call = crudCall(ctx, db);
|
|
1486
1435
|
return await writeAllowed(
|
|
1487
1436
|
call,
|
|
1488
1437
|
db,
|
|
1489
|
-
form,
|
|
1438
|
+
[form, level],
|
|
1490
1439
|
input.ids,
|
|
1491
1440
|
(table, allowed) => table.deleteMany({ where: { id: { in: [...allowed] } } })
|
|
1492
1441
|
);
|
|
@@ -1495,16 +1444,40 @@ function bulkDeleteHandler(form) {
|
|
|
1495
1444
|
}
|
|
1496
1445
|
|
|
1497
1446
|
// src/server/kits/crud/create.ts
|
|
1498
|
-
function
|
|
1447
|
+
function isRecord5(value) {
|
|
1448
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1449
|
+
}
|
|
1450
|
+
function isPlainObject2(value) {
|
|
1451
|
+
if (!isRecord5(value)) {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1455
|
+
return prototype === Object.prototype || prototype === null;
|
|
1456
|
+
}
|
|
1457
|
+
function createHandler(form, prepare) {
|
|
1499
1458
|
const handler = async ({ input, ctx, db }) => {
|
|
1500
1459
|
const call = crudCall(ctx, db);
|
|
1460
|
+
const { service } = call.runtime;
|
|
1461
|
+
const sent = isRecord5(input) ? input : {};
|
|
1462
|
+
checkColumnValues(service, sent);
|
|
1463
|
+
checkLockedColumns(call.runtime, call.principal, sent, { creating: true });
|
|
1501
1464
|
const data = prepare === void 0 ? input : await checked(() => prepare(input, ctx, db));
|
|
1502
|
-
if (
|
|
1465
|
+
if (!isPlainObject2(data)) {
|
|
1466
|
+
throw new QuickdrawError(
|
|
1467
|
+
"INTERNAL",
|
|
1468
|
+
`The read/write kit's prepare for ${service.name} must return the new row's data`
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
const issues = columnValueIssues(service, data);
|
|
1472
|
+
if (issues.length > 0) {
|
|
1503
1473
|
throw new QuickdrawError(
|
|
1504
1474
|
"INTERNAL",
|
|
1505
|
-
`The read/write kit's prepare for ${
|
|
1475
|
+
`The read/write kit's prepare for ${service.name} must return column values: ${issues.map((issue) => issue.message).join("; ")}`
|
|
1506
1476
|
);
|
|
1507
1477
|
}
|
|
1478
|
+
await checkAnchorColumns(call.runtime, call.principal, data, rowLevel(form, "Moderate"), {
|
|
1479
|
+
creating: true
|
|
1480
|
+
});
|
|
1508
1481
|
const { select } = projectionOf(call, "entity");
|
|
1509
1482
|
return await call.table.create({ data, select });
|
|
1510
1483
|
};
|
|
@@ -1602,20 +1575,33 @@ function signatureOf(order) {
|
|
|
1602
1575
|
function signed(order) {
|
|
1603
1576
|
return [[SIGNATURE, "asc"], ...order];
|
|
1604
1577
|
}
|
|
1578
|
+
var ROW = "row ";
|
|
1579
|
+
var ROW_CURSOR = [
|
|
1580
|
+
[SIGNATURE, "asc"],
|
|
1581
|
+
["id", "asc"]
|
|
1582
|
+
];
|
|
1605
1583
|
function cursorAfter(order, row) {
|
|
1606
|
-
|
|
1584
|
+
const cursor = encodeCursor(signed(order), { ...row, [SIGNATURE]: signatureOf(order) });
|
|
1585
|
+
return cursor.length <= MAX_CURSOR_LENGTH ? cursor : encodeCursor(ROW_CURSOR, { [SIGNATURE]: `${ROW}${signatureOf(order)}`, id: row.id });
|
|
1607
1586
|
}
|
|
1608
|
-
function
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1587
|
+
function readCursor(order, cursor) {
|
|
1588
|
+
const signature = signatureOf(order);
|
|
1589
|
+
const decoded = (shape) => {
|
|
1590
|
+
try {
|
|
1591
|
+
return decodeCursor(shape, cursor);
|
|
1592
|
+
} catch {
|
|
1593
|
+
return void 0;
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
const values = decoded(signed(order));
|
|
1597
|
+
if (values?.[0] === signature) {
|
|
1598
|
+
return { values: values.slice(1) };
|
|
1614
1599
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1600
|
+
const named = decoded(ROW_CURSOR);
|
|
1601
|
+
if (named?.[0] === `${ROW}${signature}` && typeof named[1] === "string") {
|
|
1602
|
+
return { row: named[1] };
|
|
1617
1603
|
}
|
|
1618
|
-
|
|
1604
|
+
throw unreadable(NOT_A_CURSOR, ["cursor"]);
|
|
1619
1605
|
}
|
|
1620
1606
|
async function nullableColumns(read, values) {
|
|
1621
1607
|
const { storage, model, order } = read;
|
|
@@ -1652,8 +1638,27 @@ function refusal(error, read) {
|
|
|
1652
1638
|
refused.cause = error;
|
|
1653
1639
|
return refused;
|
|
1654
1640
|
}
|
|
1641
|
+
async function startValues(read, cursor) {
|
|
1642
|
+
const held = readCursor(read.order, cursor);
|
|
1643
|
+
if ("values" in held) {
|
|
1644
|
+
return held.values;
|
|
1645
|
+
}
|
|
1646
|
+
let row;
|
|
1647
|
+
try {
|
|
1648
|
+
row = await read.table.findFirst({
|
|
1649
|
+
where: { AND: [read.where, { id: held.row }] },
|
|
1650
|
+
select: Object.fromEntries(read.order.map(([column]) => [column, true]))
|
|
1651
|
+
});
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
throw refusal(error, read);
|
|
1654
|
+
}
|
|
1655
|
+
if (row === null) {
|
|
1656
|
+
throw unreadable(NOT_A_CURSOR, ["cursor"]);
|
|
1657
|
+
}
|
|
1658
|
+
return read.order.map(([column]) => row[column]);
|
|
1659
|
+
}
|
|
1655
1660
|
async function readPage(read) {
|
|
1656
|
-
const values = read.cursor === void 0 ? void 0 :
|
|
1661
|
+
const values = read.cursor === void 0 ? void 0 : await startValues(read, read.cursor);
|
|
1657
1662
|
const nullable = await nullableColumns(read, values);
|
|
1658
1663
|
const where = values === void 0 ? read.where : { AND: [read.where, afterCursor(read.order, values, nullable)] };
|
|
1659
1664
|
let rows;
|
|
@@ -1762,14 +1767,17 @@ async function nextOrdinal(db, model, where = {}, options = {}) {
|
|
|
1762
1767
|
function invalid(message, path) {
|
|
1763
1768
|
return new QuickdrawError("VALIDATION", message, { issues: [{ path: [...path], message }] });
|
|
1764
1769
|
}
|
|
1765
|
-
async function readNamed(table, spec, move, model) {
|
|
1770
|
+
async function readNamed(table, [spec, writable], move, model) {
|
|
1766
1771
|
const select = Object.fromEntries(["id", spec.column, ...spec.within].map((key) => [key, true]));
|
|
1767
|
-
const ids =
|
|
1768
|
-
|
|
1772
|
+
const ids = {
|
|
1773
|
+
id: { in: [move.id, move.beforeId, move.afterId].filter((id) => id !== void 0) }
|
|
1774
|
+
};
|
|
1775
|
+
const where = writable === void 0 ? ids : { AND: [ids, writable] };
|
|
1776
|
+
const rows = writable === "none" ? [] : await table.findMany({ where, select });
|
|
1769
1777
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
1770
1778
|
const moved = byId.get(move.id);
|
|
1771
1779
|
if (moved === void 0) {
|
|
1772
|
-
throw new QuickdrawError("NOT_FOUND", `No such ${model}`);
|
|
1780
|
+
throw writable === void 0 ? new QuickdrawError("NOT_FOUND", `No such ${model}`) : new QuickdrawError("FORBIDDEN", "Insufficient permissions");
|
|
1773
1781
|
}
|
|
1774
1782
|
const neighbor = (id, key) => {
|
|
1775
1783
|
const row = id === void 0 ? void 0 : byId.get(id);
|
|
@@ -1785,9 +1793,10 @@ async function readNamed(table, spec, move, model) {
|
|
|
1785
1793
|
after: neighbor(move.afterId, "afterId")
|
|
1786
1794
|
};
|
|
1787
1795
|
}
|
|
1788
|
-
function listOf(spec, moved) {
|
|
1796
|
+
function listOf(spec, moved, writable) {
|
|
1789
1797
|
const scope = Object.fromEntries(spec.within.map((column) => [column, moved[column] ?? null]));
|
|
1790
|
-
|
|
1798
|
+
const mine = writable === void 0 ? [] : [writable === "none" ? { id: { in: [] } } : writable];
|
|
1799
|
+
return { AND: [scope, { id: { not: moved.id } }, ...mine] };
|
|
1791
1800
|
}
|
|
1792
1801
|
async function adjacent(table, spec, list, row, side) {
|
|
1793
1802
|
const { column } = spec;
|
|
@@ -1805,10 +1814,10 @@ async function adjacent(table, spec, list, row, side) {
|
|
|
1805
1814
|
select: { id: true, [column]: true }
|
|
1806
1815
|
});
|
|
1807
1816
|
}
|
|
1808
|
-
async function bounds(table, spec, named) {
|
|
1817
|
+
async function bounds(table, spec, named, writable) {
|
|
1809
1818
|
const { column } = spec;
|
|
1810
1819
|
const { before, after } = named;
|
|
1811
|
-
const list = listOf(spec, named.moved);
|
|
1820
|
+
const list = listOf(spec, named.moved, writable);
|
|
1812
1821
|
const low = before ?? (after === void 0 ? null : await adjacent(table, spec, list, after, "previous"));
|
|
1813
1822
|
const high = after ?? (before === void 0 ? null : await adjacent(table, spec, list, before, "next"));
|
|
1814
1823
|
const lowValue = low === null ? null : low[column];
|
|
@@ -1822,14 +1831,21 @@ async function bounds(table, spec, named) {
|
|
|
1822
1831
|
}
|
|
1823
1832
|
return [lowValue, highValue];
|
|
1824
1833
|
}
|
|
1825
|
-
async function renumber(table, spec, named, move) {
|
|
1834
|
+
async function renumber(table, [spec, writable], named, move) {
|
|
1826
1835
|
const { column } = spec;
|
|
1827
1836
|
const rows = await table.findMany({
|
|
1828
|
-
where: listOf(spec, named.moved),
|
|
1837
|
+
where: listOf(spec, named.moved, writable),
|
|
1829
1838
|
select: { id: true, [column]: true },
|
|
1830
1839
|
orderBy: [{ [column]: "asc" }, { id: "asc" }]
|
|
1831
1840
|
});
|
|
1832
1841
|
const order = rows.map((row) => row.id);
|
|
1842
|
+
const neighbor = move.beforeId ?? move.afterId;
|
|
1843
|
+
if (neighbor !== void 0 && !order.includes(neighbor)) {
|
|
1844
|
+
throw new QuickdrawError(
|
|
1845
|
+
"INTERNAL",
|
|
1846
|
+
"A reorder's neighbor is missing from the list it renumbers"
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1833
1849
|
const at = move.beforeId === void 0 ? Math.max(order.indexOf(move.afterId), 0) : order.indexOf(move.beforeId) + 1;
|
|
1834
1850
|
order.splice(at, 0, move.id);
|
|
1835
1851
|
const current = new Map(rows.map((row) => [row.id, row[column]]));
|
|
@@ -1848,29 +1864,48 @@ var RENUMBER_BASE_MS = 5e3;
|
|
|
1848
1864
|
var RENUMBER_MS_PER_ROW = 10;
|
|
1849
1865
|
var OWNER = "The read/write kit's reorder";
|
|
1850
1866
|
var CONFLICT = "Another change to this list ran at the same time; try again";
|
|
1851
|
-
async function moveIn(tx, { call, spec, move, select }, mayRenumber) {
|
|
1867
|
+
async function moveIn(tx, { call, spec, move, writable, select }, mayRenumber) {
|
|
1852
1868
|
const table = delegateOf(tx, call.model);
|
|
1853
|
-
const named = await readNamed(table, spec, move, call.model);
|
|
1854
|
-
const range = await bounds(table, spec, named);
|
|
1869
|
+
const named = await readNamed(table, [spec, writable], move, call.model);
|
|
1870
|
+
const range = await bounds(table, spec, named, writable);
|
|
1855
1871
|
const between = range === void 0 ? void 0 : ordinalBetween(range[0] ?? void 0, range[1] ?? void 0);
|
|
1856
1872
|
if (between === void 0 && !mayRenumber) {
|
|
1857
|
-
return { list: listOf(spec, named.moved) };
|
|
1873
|
+
return { list: listOf(spec, named.moved, writable) };
|
|
1858
1874
|
}
|
|
1859
|
-
const value = between ?? await renumber(table, spec, named, move);
|
|
1875
|
+
const value = between ?? await renumber(table, [spec, writable], named, move);
|
|
1860
1876
|
return {
|
|
1861
1877
|
row: await table.update({ where: { id: move.id }, data: { [spec.column]: value }, select })
|
|
1862
1878
|
};
|
|
1863
1879
|
}
|
|
1880
|
+
async function checkNamed(call, [form, level], move) {
|
|
1881
|
+
const named = [move.beforeId, move.afterId].filter((id) => id !== void 0);
|
|
1882
|
+
const ids = uniqueIds([move.id, ...named]);
|
|
1883
|
+
const writable = new Set(await allowedIds(call, form, ids, level, "write"));
|
|
1884
|
+
if (!writable.has(move.id)) {
|
|
1885
|
+
throw new QuickdrawError("FORBIDDEN", "Insufficient permissions");
|
|
1886
|
+
}
|
|
1887
|
+
for (const [id, key] of [
|
|
1888
|
+
[move.beforeId, "beforeId"],
|
|
1889
|
+
[move.afterId, "afterId"]
|
|
1890
|
+
]) {
|
|
1891
|
+
if (id !== void 0 && !writable.has(id)) {
|
|
1892
|
+
throw new QuickdrawError("NOT_FOUND", `No such ${call.model} in this list as ${key}`);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1864
1896
|
function reorderHandler(spec, form) {
|
|
1897
|
+
const level = rowLevel(form, "Moderate");
|
|
1865
1898
|
const handler = async ({ input, ctx, db }) => {
|
|
1866
1899
|
const call = crudCall(ctx, db);
|
|
1900
|
+
const asked = input;
|
|
1901
|
+
await checkNamed(call, [form, level], asked);
|
|
1867
1902
|
const move = {
|
|
1868
1903
|
call,
|
|
1869
1904
|
spec,
|
|
1870
|
-
move:
|
|
1905
|
+
move: asked,
|
|
1906
|
+
writable: await rowsWhere(call, form, level, "write"),
|
|
1871
1907
|
select: projectionOf(call, "entity").select
|
|
1872
1908
|
};
|
|
1873
|
-
await checkRowWrite(call, form, move.move.id);
|
|
1874
1909
|
const how = { owner: OWNER, conflict: CONFLICT };
|
|
1875
1910
|
const first = await inKitTransaction(db, async (tx) => await moveIn(tx, move, false), how);
|
|
1876
1911
|
if ("row" in first) {
|
|
@@ -1901,7 +1936,7 @@ function handlerOf(context) {
|
|
|
1901
1936
|
case "list":
|
|
1902
1937
|
return listHandler({ spec, form, projection: context.projection });
|
|
1903
1938
|
case "create":
|
|
1904
|
-
return createHandler(context.prepare);
|
|
1939
|
+
return createHandler(form, context.prepare);
|
|
1905
1940
|
case "update":
|
|
1906
1941
|
return updateHandler(form);
|
|
1907
1942
|
case "delete":
|
|
@@ -1909,9 +1944,9 @@ function handlerOf(context) {
|
|
|
1909
1944
|
case "reorder":
|
|
1910
1945
|
return reorderHandler(spec, form);
|
|
1911
1946
|
case "bulkUpdate":
|
|
1912
|
-
return bulkUpdateHandler(form);
|
|
1947
|
+
return bulkUpdateHandler(form, context.bulkLevel);
|
|
1913
1948
|
default:
|
|
1914
|
-
return bulkDeleteHandler(form);
|
|
1949
|
+
return bulkDeleteHandler(form, context.bulkLevel);
|
|
1915
1950
|
}
|
|
1916
1951
|
}
|
|
1917
1952
|
|
|
@@ -1989,7 +2024,14 @@ var CHECKS_ROWS = /* @__PURE__ */ new Set([
|
|
|
1989
2024
|
"delete",
|
|
1990
2025
|
"reorder"
|
|
1991
2026
|
]);
|
|
1992
|
-
var
|
|
2027
|
+
var CALLER_DEPENDENT = /* @__PURE__ */ new Set(["list", "getMany"]);
|
|
2028
|
+
var COUNTERPARTS = { bulkDelete: "delete", bulkUpdate: "update" };
|
|
2029
|
+
function bulkLevelOf(kit, access, spec) {
|
|
2030
|
+
const counterpart = spec.method === "bulkDelete" || spec.method === "bulkUpdate" ? COUNTERPARTS[spec.method] : void 0;
|
|
2031
|
+
const found = kit.find(([, other]) => other.method === counterpart);
|
|
2032
|
+
return bulkFallback(found === void 0 ? void 0 : access[found[0]]);
|
|
2033
|
+
}
|
|
2034
|
+
var OPTION_KEYS = ["access", "prepare", "rowless"];
|
|
1993
2035
|
function serviceProblem(service, contract, [name, spec], form) {
|
|
1994
2036
|
if (service.contract !== contract) {
|
|
1995
2037
|
return `its read/write kit handlers were made for another contract; pass ${service.name}'s own contract to crud.handlers`;
|
|
@@ -2004,9 +2046,9 @@ function handlers(contract, options) {
|
|
|
2004
2046
|
if (!isRecord6(options)) {
|
|
2005
2047
|
fail("options must be { access, prepare?, rowless? }");
|
|
2006
2048
|
}
|
|
2007
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
2049
|
+
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS.includes(key));
|
|
2008
2050
|
if (unknownKey !== void 0) {
|
|
2009
|
-
fail(`options has an unknown key "${unknownKey}"; the options are ${
|
|
2051
|
+
fail(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS.join(", ")}`);
|
|
2010
2052
|
}
|
|
2011
2053
|
const names = kit.map(([name]) => name);
|
|
2012
2054
|
const access = checkAccess(options.access, names);
|
|
@@ -2016,11 +2058,15 @@ function handlers(contract, options) {
|
|
|
2016
2058
|
for (const [name, spec] of kit) {
|
|
2017
2059
|
const form = access[name];
|
|
2018
2060
|
const projection = spec.method === "list" ? itemProjection(contract, spec) : "entity";
|
|
2019
|
-
const
|
|
2061
|
+
const bulkLevel = bulkLevelOf(kit, access, spec);
|
|
2062
|
+
const handler = handlerOf({ spec, form, projection, prepare, bulkLevel });
|
|
2020
2063
|
checkWhenDefined(handler, (service) => serviceProblem(service, contract, [name, spec], form));
|
|
2021
2064
|
if (CHECKS_ROWS.has(spec.method)) {
|
|
2022
2065
|
checksRowsItself(handler);
|
|
2023
2066
|
}
|
|
2067
|
+
if (CALLER_DEPENDENT.has(spec.method)) {
|
|
2068
|
+
dependsOnCaller(handler);
|
|
2069
|
+
}
|
|
2024
2070
|
entries[name] = kitEntry(name, form, handler, rowless);
|
|
2025
2071
|
}
|
|
2026
2072
|
return Object.freeze(entries);
|
|
@@ -2106,9 +2152,7 @@ async function scopeWhere(run) {
|
|
|
2106
2152
|
return await membersWhere(reader, collection, run.query.scope, collection.maxLimit) ?? "none";
|
|
2107
2153
|
}
|
|
2108
2154
|
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;
|
|
2155
|
+
return scopedCollection(run)?.order ?? ID_ORDER;
|
|
2112
2156
|
}
|
|
2113
2157
|
async function strategyIds(run, ids) {
|
|
2114
2158
|
const found = await checked(() => ids(run.query.q, run.ctx, { limit: run.query.limit }));
|
|
@@ -2272,14 +2316,14 @@ function checkStrategy(strategy) {
|
|
|
2272
2316
|
}
|
|
2273
2317
|
return strategy;
|
|
2274
2318
|
}
|
|
2275
|
-
var
|
|
2319
|
+
var OPTION_KEYS2 = ["access", "strategy", "method"];
|
|
2276
2320
|
function checkOptions(options, kit) {
|
|
2277
2321
|
if (!isRecord8(options)) {
|
|
2278
2322
|
fail2("options must be { access, strategy?, method? }");
|
|
2279
2323
|
}
|
|
2280
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
2324
|
+
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS2.includes(key));
|
|
2281
2325
|
if (unknownKey !== void 0) {
|
|
2282
|
-
fail2(`options has an unknown key "${unknownKey}"; the options are ${
|
|
2326
|
+
fail2(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS2.join(", ")}`);
|
|
2283
2327
|
}
|
|
2284
2328
|
const problem = accessFormProblem(options.access);
|
|
2285
2329
|
if (problem !== void 0) {
|
|
@@ -2344,6 +2388,7 @@ function handlers2(contract, options) {
|
|
|
2344
2388
|
checkScope(contract, name, spec, projection);
|
|
2345
2389
|
const handler = searchHandler({ spec, form, projection, strategy });
|
|
2346
2390
|
checkWhenDefined(handler, (service) => serviceProblem2(service, contract, name, form));
|
|
2391
|
+
dependsOnCaller(handler);
|
|
2347
2392
|
entries[name] = Object.freeze({ access: form, handler, share: "caller" });
|
|
2348
2393
|
}
|
|
2349
2394
|
return Object.freeze(entries);
|
|
@@ -2618,6 +2663,14 @@ function capGrant(own, given, model) {
|
|
|
2618
2663
|
);
|
|
2619
2664
|
}
|
|
2620
2665
|
}
|
|
2666
|
+
function capTarget(own, current, model) {
|
|
2667
|
+
if (current !== null && !meetsLevel(own, current)) {
|
|
2668
|
+
throw new QuickdrawError(
|
|
2669
|
+
"FORBIDDEN",
|
|
2670
|
+
`That user has ${current} on this ${model}, above your own level on it (${own ?? "none"}); you cannot change or take away their access`
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2621
2674
|
|
|
2622
2675
|
// src/server/kits/sharing/context.ts
|
|
2623
2676
|
async function resolveTarget(call, context, given) {
|
|
@@ -2692,15 +2745,17 @@ async function checkLastAdmin(call, tx, listed, change) {
|
|
|
2692
2745
|
async function changeList(call, context, change) {
|
|
2693
2746
|
const columns = aclColumnsOf(call.runtime.service);
|
|
2694
2747
|
return await inSerializable(call.db, async (tx) => {
|
|
2695
|
-
|
|
2748
|
+
const own = await checkCaller(call, context.form, change.id);
|
|
2749
|
+
capGrant(own, change.level, call.model);
|
|
2696
2750
|
const table = tableOf(tx, call.model);
|
|
2697
2751
|
const listed = await readList(table, columns, change.id, call.model);
|
|
2698
2752
|
const problem = refusal2(listed, change, call.model);
|
|
2699
2753
|
if (problem !== void 0) {
|
|
2700
2754
|
throw problem;
|
|
2701
2755
|
}
|
|
2702
|
-
await checkLastAdmin(call, tx, listed, change);
|
|
2703
2756
|
const before = levelIn(listed.entries, change.userId);
|
|
2757
|
+
capTarget(own, before, call.model);
|
|
2758
|
+
await checkLastAdmin(call, tx, listed, change);
|
|
2704
2759
|
if (before === change.level && entriesOf(listed.entries, change.userId) === 1) {
|
|
2705
2760
|
return sharesOf(listed.entries);
|
|
2706
2761
|
}
|
|
@@ -2852,15 +2907,16 @@ async function invite(call, context, target, role) {
|
|
|
2852
2907
|
async function end(call, context, target, kind) {
|
|
2853
2908
|
const read = membershipTableOf(call.runtime.service);
|
|
2854
2909
|
return await inSerializable(call.db, async (tx) => {
|
|
2855
|
-
|
|
2856
|
-
await checkCaller(call, context.form, target.entryId);
|
|
2857
|
-
}
|
|
2910
|
+
const own = kind === "remove" ? await checkCaller(call, context.form, target.entryId) : void 0;
|
|
2858
2911
|
const table = tableOf(tx, read.model);
|
|
2859
2912
|
const rows = await rowsOf(table, read, target);
|
|
2860
2913
|
if (rows.length === 0) {
|
|
2861
2914
|
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
2915
|
}
|
|
2863
2916
|
const current = currentRole(read, rows);
|
|
2917
|
+
if (own !== void 0) {
|
|
2918
|
+
capTarget(own, current.level, call.model);
|
|
2919
|
+
}
|
|
2864
2920
|
if (current.level === "Admin" && !await anotherAdmin(call, tx, read, target)) {
|
|
2865
2921
|
throw lastAdmin(call.model);
|
|
2866
2922
|
}
|
|
@@ -2883,6 +2939,7 @@ async function changeRole(call, context, target, role) {
|
|
|
2883
2939
|
throw new QuickdrawError("NOT_FOUND", `That user is not a member of this ${call.model}`);
|
|
2884
2940
|
}
|
|
2885
2941
|
const current = currentRole(read, rows);
|
|
2942
|
+
capTarget(own, current.level, call.model);
|
|
2886
2943
|
if (rows.length === 1 && current.role === role) {
|
|
2887
2944
|
return memberOf(read, target.userId, role);
|
|
2888
2945
|
}
|
|
@@ -3071,6 +3128,9 @@ function handlers3(contract, ...rest) {
|
|
|
3071
3128
|
const form = access[name] ?? DEFAULT_ACCESS[spec.method];
|
|
3072
3129
|
const handler = handlerOf2(spec.method, Object.freeze({ ...hooks, form }));
|
|
3073
3130
|
checkWhenDefined(handler, (service) => serviceProblem3(service, contract, spec.mode));
|
|
3131
|
+
if (spec.method === "listShares" || spec.method === "listMembers") {
|
|
3132
|
+
dependsOnCaller(handler);
|
|
3133
|
+
}
|
|
3074
3134
|
entries[name] = kitEntry(name, form, handler, rowless);
|
|
3075
3135
|
}
|
|
3076
3136
|
return Object.freeze(entries);
|
|
@@ -3123,6 +3183,13 @@ var DEFAULT_FIELDS = Object.freeze({
|
|
|
3123
3183
|
showInTable: false
|
|
3124
3184
|
}
|
|
3125
3185
|
});
|
|
3186
|
+
function grantNamesOf(column) {
|
|
3187
|
+
return column === void 0 || ADMIN_GRANT_FIELDS.includes(column) ? ADMIN_GRANT_FIELDS : [...ADMIN_GRANT_FIELDS, column];
|
|
3188
|
+
}
|
|
3189
|
+
function grantsColumnFor(model, source) {
|
|
3190
|
+
const same = model !== void 0 && source !== void 0;
|
|
3191
|
+
return same && modelKey(source.model) === modelKey(model) ? source.column : void 0;
|
|
3192
|
+
}
|
|
3126
3193
|
function isRecord10(value) {
|
|
3127
3194
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3128
3195
|
}
|
|
@@ -3142,23 +3209,19 @@ function checkDisplayName(displayName, serviceName, fail5) {
|
|
|
3142
3209
|
}
|
|
3143
3210
|
return displayName;
|
|
3144
3211
|
}
|
|
3145
|
-
function checkGrants(grants, names, fail5) {
|
|
3212
|
+
function checkGrants(grants, [names, grantNames], fail5) {
|
|
3146
3213
|
if (grants === void 0 || grants === false) {
|
|
3147
3214
|
return /* @__PURE__ */ new Set();
|
|
3148
3215
|
}
|
|
3149
3216
|
if (grants !== true) {
|
|
3150
3217
|
fail5("grants must be true or false");
|
|
3151
3218
|
}
|
|
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);
|
|
3219
|
+
return new Set(grantNames.filter((name) => names.includes(name)));
|
|
3159
3220
|
}
|
|
3160
|
-
function checkHidden(hiddenFields, names, grants, fail5) {
|
|
3161
|
-
const defaults = ADMIN_HIDDEN_FIELDS.filter(
|
|
3221
|
+
function checkHidden(hiddenFields, [names, grantNames], grants, fail5) {
|
|
3222
|
+
const defaults = [.../* @__PURE__ */ new Set([...ADMIN_HIDDEN_FIELDS, ...grantNames])].filter(
|
|
3223
|
+
(name) => !grants.has(name)
|
|
3224
|
+
);
|
|
3162
3225
|
if (hiddenFields === void 0) {
|
|
3163
3226
|
return new Set(defaults);
|
|
3164
3227
|
}
|
|
@@ -3293,8 +3356,9 @@ function frozenConfig(config) {
|
|
|
3293
3356
|
}
|
|
3294
3357
|
function adminFieldsOf(serviceName, spec, options, fail5) {
|
|
3295
3358
|
const names = spec.fields.map((field) => field.name);
|
|
3296
|
-
const
|
|
3297
|
-
const
|
|
3359
|
+
const grantNames = grantNamesOf(options.grantsColumn);
|
|
3360
|
+
const grants = checkGrants(options.grants, [names, grantNames], fail5);
|
|
3361
|
+
const hidden = checkHidden(options.hiddenFields, [names, grantNames], grants, fail5);
|
|
3298
3362
|
const listed = [...spec.filter, ...spec.sort].find((name) => hidden.has(name));
|
|
3299
3363
|
if (listed !== void 0) {
|
|
3300
3364
|
fail5(`"${listed}" is hidden, so adminList may not filter or sort on it`);
|
|
@@ -3322,10 +3386,26 @@ function adminFieldsOf(serviceName, spec, options, fail5) {
|
|
|
3322
3386
|
fields: Object.freeze(fields)
|
|
3323
3387
|
});
|
|
3324
3388
|
const readOnly = new Set(fields.filter((field) => !field.editable).map((field) => field.name));
|
|
3325
|
-
|
|
3389
|
+
const named = /* @__PURE__ */ new Set([
|
|
3390
|
+
...editable ?? [],
|
|
3391
|
+
...Object.entries(overrides).filter(([, override]) => override.editable === true).map(([name]) => name)
|
|
3392
|
+
]);
|
|
3393
|
+
return Object.freeze({ meta, hidden, readOnly, grants, named });
|
|
3394
|
+
}
|
|
3395
|
+
function withReadOnlyFields(meta, readOnly) {
|
|
3396
|
+
if (!meta.fields.some((field) => readOnly.has(field.name) && field.editable)) {
|
|
3397
|
+
return meta;
|
|
3398
|
+
}
|
|
3399
|
+
const fields = meta.fields.map(
|
|
3400
|
+
(field) => readOnly.has(field.name) && field.editable ? frozenConfig({ ...field, editable: false }) : field
|
|
3401
|
+
);
|
|
3402
|
+
return Object.freeze({ ...meta, fields: Object.freeze(fields) });
|
|
3326
3403
|
}
|
|
3327
3404
|
|
|
3328
3405
|
// src/server/kits/admin/runtime.ts
|
|
3406
|
+
function grantsColumnOf(runtime) {
|
|
3407
|
+
return grantsColumnFor(runtime.service.model, runtime.grantsSource);
|
|
3408
|
+
}
|
|
3329
3409
|
function tableOf2(db, model) {
|
|
3330
3410
|
const name = modelKey(model);
|
|
3331
3411
|
const isClient = (typeof db === "object" || typeof db === "function") && db !== null;
|
|
@@ -3339,7 +3419,7 @@ function tableOf2(db, model) {
|
|
|
3339
3419
|
}
|
|
3340
3420
|
return delegate;
|
|
3341
3421
|
}
|
|
3342
|
-
function adminCall(ctx, db,
|
|
3422
|
+
function adminCall(ctx, db, fieldsFor) {
|
|
3343
3423
|
const runtime = kitRuntimeOf(ctx);
|
|
3344
3424
|
const model = runtime?.service.model;
|
|
3345
3425
|
const projection = runtime?.service.projections.get("entity");
|
|
@@ -3349,6 +3429,7 @@ function adminCall(ctx, db, fields) {
|
|
|
3349
3429
|
"The admin kit's handlers run through a dispatcher, in a service with a model and an entity"
|
|
3350
3430
|
);
|
|
3351
3431
|
}
|
|
3432
|
+
const fields = fieldsFor(grantsColumnOf(runtime));
|
|
3352
3433
|
const { principal } = ctx;
|
|
3353
3434
|
const level = principal === null ? null : serviceGrant(principal, runtime.service.name) ?? null;
|
|
3354
3435
|
const tiered = projection.tiers.hidden(level);
|
|
@@ -3360,7 +3441,8 @@ function adminCall(ctx, db, fields) {
|
|
|
3360
3441
|
principal,
|
|
3361
3442
|
projection,
|
|
3362
3443
|
unseen,
|
|
3363
|
-
omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden])
|
|
3444
|
+
omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden]),
|
|
3445
|
+
fields
|
|
3364
3446
|
};
|
|
3365
3447
|
}
|
|
3366
3448
|
function rowOut(call, row) {
|
|
@@ -3384,7 +3466,7 @@ function checkSeen(call, names) {
|
|
|
3384
3466
|
var NO_NULLABLE = /* @__PURE__ */ new Set();
|
|
3385
3467
|
function adminListHandler(context) {
|
|
3386
3468
|
const handler = async ({ input, ctx, db }) => {
|
|
3387
|
-
const call = adminCall(ctx, db, context.
|
|
3469
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3388
3470
|
const query = input;
|
|
3389
3471
|
checkSeen(call, namedFields(query));
|
|
3390
3472
|
const order = listOrder(defaultSorts(context.spec.sort, call.unseen), query.sort);
|
|
@@ -3435,14 +3517,14 @@ function subscribersOf(call, id) {
|
|
|
3435
3517
|
}
|
|
3436
3518
|
function adminSubscribersHandler(context) {
|
|
3437
3519
|
const handler = ({ input, ctx, db }) => {
|
|
3438
|
-
const call = adminCall(ctx, db, context.
|
|
3520
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3439
3521
|
return Promise.resolve(subscribersOf(call, input.id));
|
|
3440
3522
|
};
|
|
3441
3523
|
return handler;
|
|
3442
3524
|
}
|
|
3443
3525
|
function adminReemitHandler(context) {
|
|
3444
3526
|
const handler = async ({ input, ctx, db }) => {
|
|
3445
|
-
const call = adminCall(ctx, db, context.
|
|
3527
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3446
3528
|
const { id } = input;
|
|
3447
3529
|
requireRow(
|
|
3448
3530
|
await call.table.findUnique({ where: { id }, select: { id: true } }),
|
|
@@ -3456,7 +3538,7 @@ function adminReemitHandler(context) {
|
|
|
3456
3538
|
|
|
3457
3539
|
// src/server/kits/admin/writes.ts
|
|
3458
3540
|
async function checkWrite(call, context, data) {
|
|
3459
|
-
const { hidden, readOnly } =
|
|
3541
|
+
const { hidden, readOnly } = call.fields;
|
|
3460
3542
|
const names = Object.keys(data);
|
|
3461
3543
|
const issues = names.filter((name) => hidden.has(name) || readOnly.has(name)).map((name) => ({
|
|
3462
3544
|
path: ["data", name],
|
|
@@ -3472,7 +3554,8 @@ async function checkWrite(call, context, data) {
|
|
|
3472
3554
|
call.runtime,
|
|
3473
3555
|
call.principal,
|
|
3474
3556
|
data,
|
|
3475
|
-
rowLevel(context.form, "Moderate")
|
|
3557
|
+
rowLevel(context.form, "Moderate"),
|
|
3558
|
+
["data"]
|
|
3476
3559
|
);
|
|
3477
3560
|
}
|
|
3478
3561
|
function isValueRefusal(error) {
|
|
@@ -3516,7 +3599,7 @@ function committed(context, args, write) {
|
|
|
3516
3599
|
category: "quickdraw.admin",
|
|
3517
3600
|
method: write.method,
|
|
3518
3601
|
id: write.id,
|
|
3519
|
-
error:
|
|
3602
|
+
error: errorText(error)
|
|
3520
3603
|
});
|
|
3521
3604
|
};
|
|
3522
3605
|
try {
|
|
@@ -3561,7 +3644,7 @@ async function written(context, call, args, method, perform) {
|
|
|
3561
3644
|
}
|
|
3562
3645
|
function adminGetHandler(context) {
|
|
3563
3646
|
const handler = async ({ input, ctx, db }) => {
|
|
3564
|
-
const call = adminCall(ctx, db, context.
|
|
3647
|
+
const call = adminCall(ctx, db, context.fieldsFor);
|
|
3565
3648
|
return await readRow(call, input.id);
|
|
3566
3649
|
};
|
|
3567
3650
|
return handler;
|
|
@@ -3571,7 +3654,7 @@ function idOf(row) {
|
|
|
3571
3654
|
}
|
|
3572
3655
|
function adminCreateHandler(context) {
|
|
3573
3656
|
const handler = async (args) => {
|
|
3574
|
-
const call = adminCall(args.ctx, args.db, context.
|
|
3657
|
+
const call = adminCall(args.ctx, args.db, context.fieldsFor);
|
|
3575
3658
|
const { data } = args.input;
|
|
3576
3659
|
await checkWrite(call, context, data);
|
|
3577
3660
|
return await written(context, call, args, "adminCreate", async (table) => {
|
|
@@ -3583,7 +3666,7 @@ function adminCreateHandler(context) {
|
|
|
3583
3666
|
}
|
|
3584
3667
|
function adminUpdateHandler(context) {
|
|
3585
3668
|
const handler = async (args) => {
|
|
3586
|
-
const call = adminCall(args.ctx, args.db, context.
|
|
3669
|
+
const call = adminCall(args.ctx, args.db, context.fieldsFor);
|
|
3587
3670
|
const { id, data } = args.input;
|
|
3588
3671
|
await checkWrite(call, context, data);
|
|
3589
3672
|
if (Object.keys(data).length === 0) {
|
|
@@ -3600,7 +3683,7 @@ function adminUpdateHandler(context) {
|
|
|
3600
3683
|
}
|
|
3601
3684
|
function adminDeleteHandler(context) {
|
|
3602
3685
|
const handler = async (args) => {
|
|
3603
|
-
const call = adminCall(args.ctx, args.db, context.
|
|
3686
|
+
const call = adminCall(args.ctx, args.db, context.fieldsFor);
|
|
3604
3687
|
const { id } = args.input;
|
|
3605
3688
|
return await written(context, call, args, "adminDelete", async (table, withBefore) => {
|
|
3606
3689
|
const select = withBefore ? call.projection.select : { id: true };
|
|
@@ -3623,8 +3706,23 @@ var MAKERS2 = Object.freeze({
|
|
|
3623
3706
|
adminReemit: adminReemitHandler
|
|
3624
3707
|
});
|
|
3625
3708
|
function adminMetaHandler(context) {
|
|
3626
|
-
const
|
|
3627
|
-
const handler = (
|
|
3709
|
+
const byService = /* @__PURE__ */ new WeakMap();
|
|
3710
|
+
const handler = (args) => {
|
|
3711
|
+
const runtime = kitRuntimeOf(args.ctx);
|
|
3712
|
+
if (runtime === void 0) {
|
|
3713
|
+
return Promise.resolve(context.fieldsFor(void 0).meta);
|
|
3714
|
+
}
|
|
3715
|
+
const { service } = runtime;
|
|
3716
|
+
const column = grantsColumnFor(service.model, runtime.grantsSource);
|
|
3717
|
+
const answers = byService.get(service) ?? /* @__PURE__ */ new Map();
|
|
3718
|
+
byService.set(service, answers);
|
|
3719
|
+
let answer = answers.get(column ?? "");
|
|
3720
|
+
if (answer === void 0) {
|
|
3721
|
+
answer = withReadOnlyFields(context.fieldsFor(column).meta, relationKeysOf(service));
|
|
3722
|
+
answers.set(column ?? "", answer);
|
|
3723
|
+
}
|
|
3724
|
+
return Promise.resolve(answer);
|
|
3725
|
+
};
|
|
3628
3726
|
return handler;
|
|
3629
3727
|
}
|
|
3630
3728
|
function handlerOf3(context) {
|
|
@@ -3633,7 +3731,7 @@ function handlerOf3(context) {
|
|
|
3633
3731
|
}
|
|
3634
3732
|
|
|
3635
3733
|
// src/server/kits/admin/handlers.ts
|
|
3636
|
-
var
|
|
3734
|
+
var OPTION_KEYS3 = [
|
|
3637
3735
|
"access",
|
|
3638
3736
|
"displayName",
|
|
3639
3737
|
"hiddenFields",
|
|
@@ -3681,9 +3779,9 @@ function checkOptions3(options) {
|
|
|
3681
3779
|
"options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
|
|
3682
3780
|
);
|
|
3683
3781
|
}
|
|
3684
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
3782
|
+
const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
|
|
3685
3783
|
if (unknownKey !== void 0) {
|
|
3686
|
-
fail4(`options has an unknown key "${unknownKey}"; the options are ${
|
|
3784
|
+
fail4(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS3.join(", ")}`);
|
|
3687
3785
|
}
|
|
3688
3786
|
return options;
|
|
3689
3787
|
}
|
|
@@ -3719,7 +3817,7 @@ function contractEditable(contract, specs) {
|
|
|
3719
3817
|
}
|
|
3720
3818
|
return [...lists.values()][0];
|
|
3721
3819
|
}
|
|
3722
|
-
function fieldsOf(contract, kit, options) {
|
|
3820
|
+
function fieldsOf(contract, kit, options, grantsColumn) {
|
|
3723
3821
|
const specs = kit.map(([, spec2]) => spec2);
|
|
3724
3822
|
const [first] = specs;
|
|
3725
3823
|
const spec = {
|
|
@@ -3736,16 +3834,47 @@ function fieldsOf(contract, kit, options) {
|
|
|
3736
3834
|
hiddenFields: options.hiddenFields,
|
|
3737
3835
|
editable: options.editable,
|
|
3738
3836
|
fieldOverrides: options.fieldOverrides,
|
|
3739
|
-
grants: options.grants
|
|
3837
|
+
grants: options.grants,
|
|
3838
|
+
...grantsColumn === void 0 ? {} : { grantsColumn }
|
|
3740
3839
|
},
|
|
3741
3840
|
fail4
|
|
3742
3841
|
);
|
|
3743
3842
|
}
|
|
3744
|
-
function
|
|
3843
|
+
function fieldsByColumn(contract, kit, options) {
|
|
3844
|
+
const byColumn = /* @__PURE__ */ new Map();
|
|
3845
|
+
return (grantsColumn) => {
|
|
3846
|
+
let fields = byColumn.get(grantsColumn ?? "");
|
|
3847
|
+
if (fields === void 0) {
|
|
3848
|
+
fields = fieldsOf(contract, kit, options, grantsColumn);
|
|
3849
|
+
byColumn.set(grantsColumn ?? "", fields);
|
|
3850
|
+
}
|
|
3851
|
+
return fields;
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function servedProblemOf(fieldsFor, grants, { service, grantsSource }) {
|
|
3855
|
+
const column = grantsColumnFor(service.model, grantsSource);
|
|
3856
|
+
let fields;
|
|
3857
|
+
try {
|
|
3858
|
+
fields = fieldsFor(column);
|
|
3859
|
+
} catch (error) {
|
|
3860
|
+
return error instanceof Error ? error.message : String(error);
|
|
3861
|
+
}
|
|
3862
|
+
if (grants && fields.grants.size === 0) {
|
|
3863
|
+
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`;
|
|
3864
|
+
return `grants: true needs a grants field: the entity has no serviceAccess or service_access field, and ${named}`;
|
|
3865
|
+
}
|
|
3866
|
+
return void 0;
|
|
3867
|
+
}
|
|
3868
|
+
function serviceProblem4(service, contract, fields) {
|
|
3745
3869
|
if (service.contract !== contract) {
|
|
3746
3870
|
return `its admin kit handlers were made for another contract; pass ${service.name}'s own contract to admin.handlers`;
|
|
3747
3871
|
}
|
|
3748
|
-
|
|
3872
|
+
if (service.model === void 0) {
|
|
3873
|
+
return "the admin kit reads and writes the service's rows: declare its model";
|
|
3874
|
+
}
|
|
3875
|
+
const relations2 = relationKeysOf(service);
|
|
3876
|
+
const relation = [...fields.named].find((name) => relations2.has(name));
|
|
3877
|
+
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
3878
|
}
|
|
3750
3879
|
function handlers4(contract, options) {
|
|
3751
3880
|
const kit = kitMethods4(contract);
|
|
@@ -3753,7 +3882,8 @@ function handlers4(contract, options) {
|
|
|
3753
3882
|
const names = kit.map(([name]) => name);
|
|
3754
3883
|
const access = checkAccess3(checked2.access, names);
|
|
3755
3884
|
const rowless = rowlessMethods(checked2.rowless, names, fail4);
|
|
3756
|
-
const
|
|
3885
|
+
const fieldsFor = fieldsByColumn(contract, kit, checked2);
|
|
3886
|
+
const fields = fieldsFor(void 0);
|
|
3757
3887
|
const { onWrite, onCommitted } = checked2;
|
|
3758
3888
|
if (onWrite !== void 0 && typeof onWrite !== "function") {
|
|
3759
3889
|
fail4("onWrite must be a function of (write, ctx, db)");
|
|
@@ -3768,11 +3898,18 @@ function handlers4(contract, options) {
|
|
|
3768
3898
|
const entries = {};
|
|
3769
3899
|
for (const [name, spec] of kit) {
|
|
3770
3900
|
const form = access[name] ?? ADMIN_DEFAULT_ACCESS;
|
|
3771
|
-
const handler = handlerOf3({ spec,
|
|
3772
|
-
checkWhenDefined(handler, (service) => serviceProblem4(service, contract));
|
|
3901
|
+
const handler = handlerOf3({ spec, fieldsFor, form, ...hooks });
|
|
3902
|
+
checkWhenDefined(handler, (service) => serviceProblem4(service, contract, fields));
|
|
3903
|
+
checkWhenServed(
|
|
3904
|
+
handler,
|
|
3905
|
+
(served) => servedProblemOf(fieldsFor, checked2.grants === true, served)
|
|
3906
|
+
);
|
|
3773
3907
|
if (checked2.grants === true) {
|
|
3774
3908
|
markGrantsEditor(handler);
|
|
3775
3909
|
}
|
|
3910
|
+
if (spec.method === "adminList" || spec.method === "adminGet") {
|
|
3911
|
+
dependsOnCaller(handler);
|
|
3912
|
+
}
|
|
3776
3913
|
entries[name] = kitEntry(name, form, handler, rowless);
|
|
3777
3914
|
}
|
|
3778
3915
|
return Object.freeze(entries);
|
|
@@ -3816,7 +3953,8 @@ function reportErrors(client, logger, role) {
|
|
|
3816
3953
|
}
|
|
3817
3954
|
async function setupRedisAdapter(io, options = {}) {
|
|
3818
3955
|
const logger = options.logger?.child({ service: "RedisAdapter" }) ?? consoleLogger.child({ service: "RedisAdapter" });
|
|
3819
|
-
const { host = "localhost", port = 6379, password, db = 0
|
|
3956
|
+
const { host = "localhost", port = 6379, password, db = 0 } = options;
|
|
3957
|
+
const key = options.keyPrefix ?? deploymentOf(io);
|
|
3820
3958
|
try {
|
|
3821
3959
|
const peers = await loadRedisPeers();
|
|
3822
3960
|
const createAdapter = peers.createAdapter;
|
|
@@ -3830,9 +3968,7 @@ async function setupRedisAdapter(io, options = {}) {
|
|
|
3830
3968
|
reportErrors(pubClient, logger, "publish");
|
|
3831
3969
|
reportErrors(subClient, logger, "subscribe");
|
|
3832
3970
|
await Promise.all([pubClient.connect(), subClient.connect()]);
|
|
3833
|
-
io.adapter(
|
|
3834
|
-
createAdapter(pubClient, subClient, { key: keyPrefix })
|
|
3835
|
-
);
|
|
3971
|
+
io.adapter(createAdapter(pubClient, subClient, { key }));
|
|
3836
3972
|
watchAdapterClients(io, logger);
|
|
3837
3973
|
logger.info(`Redis adapter connected to ${host}:${port}`);
|
|
3838
3974
|
const cleanup = async () => {
|
|
@@ -3977,8 +4113,11 @@ export {
|
|
|
3977
4113
|
DevWarningError,
|
|
3978
4114
|
INDEX_MAX_ROWS,
|
|
3979
4115
|
MAX_APP_ROOMS,
|
|
4116
|
+
MAX_ENTITY_IDS_PER_SOCKET,
|
|
3980
4117
|
MAX_ITEM_IDS,
|
|
4118
|
+
MAX_SCOPES_PER_SOCKET,
|
|
3981
4119
|
MAX_STREAMS_PER_SOCKET,
|
|
4120
|
+
MAX_TOPICS_PER_SOCKET,
|
|
3982
4121
|
MIN_STALL_INTERVAL_MS,
|
|
3983
4122
|
N_PLUS_ONE_STATEMENTS,
|
|
3984
4123
|
ORDINAL_STEP,
|