lazyconvex 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +926 -0
  2. package/dist/components/index.mjs +937 -0
  3. package/dist/error-D4GuI0ot.mjs +71 -0
  4. package/dist/file-field-BqVgy8xY.mjs +205 -0
  5. package/dist/form-BXJK_j10.d.mts +99 -0
  6. package/dist/index.d.mts +433 -0
  7. package/dist/index.mjs +1 -0
  8. package/dist/index2.d.mts +5 -0
  9. package/dist/index3.d.mts +35 -0
  10. package/dist/index4.d.mts +101 -0
  11. package/dist/index5.d.mts +842 -0
  12. package/dist/next/index.mjs +151 -0
  13. package/dist/org-CmJBb8z-.d.mts +56 -0
  14. package/dist/react/index.mjs +158 -0
  15. package/dist/retry.d.mts +12 -0
  16. package/dist/retry.mjs +35 -0
  17. package/dist/schema.d.mts +23 -0
  18. package/dist/schema.mjs +15 -0
  19. package/dist/server/index.mjs +2572 -0
  20. package/dist/types-DWBVRtit.d.mts +322 -0
  21. package/dist/use-online-status-CMr73Jlk.mjs +155 -0
  22. package/dist/use-upload-DtELytQi.mjs +95 -0
  23. package/dist/zod.d.mts +18 -0
  24. package/dist/zod.mjs +87 -0
  25. package/package.json +40 -0
  26. package/src/components/editors-section.tsx +86 -0
  27. package/src/components/fields.tsx +884 -0
  28. package/src/components/file-field.tsx +234 -0
  29. package/src/components/form.tsx +191 -0
  30. package/src/components/index.ts +11 -0
  31. package/src/components/offline-indicator.tsx +15 -0
  32. package/src/components/org-avatar.tsx +13 -0
  33. package/src/components/permission-guard.tsx +36 -0
  34. package/src/components/role-badge.tsx +14 -0
  35. package/src/components/suspense-wrap.tsx +8 -0
  36. package/src/index.ts +40 -0
  37. package/src/next/active-org.ts +33 -0
  38. package/src/next/auth.ts +9 -0
  39. package/src/next/image.ts +134 -0
  40. package/src/next/index.ts +3 -0
  41. package/src/react/form-meta.ts +53 -0
  42. package/src/react/form.ts +201 -0
  43. package/src/react/index.ts +8 -0
  44. package/src/react/org.tsx +96 -0
  45. package/src/react/use-active-org.ts +48 -0
  46. package/src/react/use-bulk-selection.ts +47 -0
  47. package/src/react/use-online-status.ts +21 -0
  48. package/src/react/use-optimistic.ts +54 -0
  49. package/src/react/use-upload.ts +101 -0
  50. package/src/retry.ts +47 -0
  51. package/src/schema.ts +30 -0
  52. package/src/server/cache-crud.ts +175 -0
  53. package/src/server/check-schema.ts +29 -0
  54. package/src/server/child.ts +98 -0
  55. package/src/server/crud.ts +384 -0
  56. package/src/server/db.ts +7 -0
  57. package/src/server/error.ts +39 -0
  58. package/src/server/file.ts +372 -0
  59. package/src/server/helpers.ts +214 -0
  60. package/src/server/index.ts +12 -0
  61. package/src/server/org-crud.ts +307 -0
  62. package/src/server/org-helpers.ts +54 -0
  63. package/src/server/org.ts +572 -0
  64. package/src/server/schema-helpers.ts +107 -0
  65. package/src/server/setup.ts +138 -0
  66. package/src/server/test-crud.ts +211 -0
  67. package/src/server/test.ts +554 -0
  68. package/src/server/types.ts +392 -0
  69. package/src/server/unique.ts +28 -0
  70. package/src/zod.ts +141 -0
@@ -0,0 +1,2572 @@
1
+ import { cvFileKindOf, elementOf, isArrayType, isStringType, unwrapZod } from "../zod.mjs";
2
+ import { n as getErrorMessage, r as isRecord, t as getErrorCode } from "../error-D4GuI0ot.mjs";
3
+ import { ConvexError, v } from "convex/values";
4
+ import { anyApi, defineTable } from "convex/server";
5
+ import { array, boolean, nullable, number, object, string, z } from "zod/v4";
6
+ import { customCtx } from "convex-helpers/server/customFunctions";
7
+ import { zCustomMutation, zCustomQuery, zid, zodOutputToConvexFields } from "convex-helpers/server/zod4";
8
+
9
+ //#region src/server/check-schema.ts
10
+ const unsupportedTypes = new Set(["pipe", "transform"]), scan = (schema, path, out) => {
11
+ const b = unwrapZod(schema);
12
+ if (b.type && unsupportedTypes.has(b.type)) out.push({
13
+ path,
14
+ zodType: b.type
15
+ });
16
+ if (isArrayType(b.type)) return scan(elementOf(b.schema), `${path}[]`, out);
17
+ if (b.type === "object" && b.schema && isRecord(b.schema.shape)) for (const [k, v] of Object.entries(b.schema.shape)) scan(v, path ? `${path}.${k}` : k, out);
18
+ }, checkSchema = (schemas) => {
19
+ const res = [];
20
+ for (const [table, schema] of Object.entries(schemas)) scan(schema, table, res);
21
+ if (res.length) {
22
+ for (const f of res) process.stderr.write(`${f.path}: unsupported zod type "${f.zodType}"\n`);
23
+ process.exitCode = 1;
24
+ }
25
+ };
26
+
27
+ //#endregion
28
+ //#region src/server/test.ts
29
+ const TEST_EMAIL = "test@playwright.local", BATCH_SIZE = 50, isTestMode = () => process.env.CONVEX_TEST_MODE === "true", generateToken$1 = () => {
30
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
31
+ let token = "";
32
+ for (let i = 0; i < 32; i += 1) token += chars.charAt(Math.floor(Math.random() * 62));
33
+ return token;
34
+ }, getOrgMembership$1 = async (db, orgId, userId) => {
35
+ const orgDoc = await db.get(orgId);
36
+ if (!orgDoc) return null;
37
+ const isOwner = orgDoc.userId === userId, member = await db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique();
38
+ if (!(isOwner || member)) return null;
39
+ return {
40
+ isAdmin: isOwner || member?.isAdmin === true,
41
+ isOwner,
42
+ member,
43
+ orgDoc
44
+ };
45
+ }, makeTestAuth = (config) => {
46
+ const { mutation: rawMut, query: rawQry } = config, mutation = rawMut, query = rawQry, getAuthUserIdOrTest = async (ctx) => {
47
+ if (!isTestMode()) return config.getAuthUserId(ctx);
48
+ const c = ctx, identity = await c.auth.getUserIdentity();
49
+ if (identity === null) return (await c.db.query("users").filter(((q) => q.eq(q.field("email"), TEST_EMAIL))).first())?._id;
50
+ return identity.subject.split("|")[0] ?? null;
51
+ }, ensureTestUser = mutation({
52
+ args: {},
53
+ handler: async (ctx) => {
54
+ if (!isTestMode()) return null;
55
+ const u = await ctx.db.query("users").filter(((q) => q.eq(q.field("email"), TEST_EMAIL))).first();
56
+ if (u) return u._id;
57
+ return ctx.db.insert("users", {
58
+ email: TEST_EMAIL,
59
+ emailVerificationTime: Date.now(),
60
+ name: "Test User"
61
+ });
62
+ }
63
+ }), getTestUser = query({
64
+ args: {},
65
+ handler: async (ctx) => {
66
+ if (!isTestMode()) return null;
67
+ return (await ctx.db.query("users").filter(((q) => q.eq(q.field("email"), TEST_EMAIL))).first())?._id ?? null;
68
+ }
69
+ }), createTestUser = mutation({
70
+ args: {
71
+ email: v.string(),
72
+ name: v.string()
73
+ },
74
+ handler: async (ctx, { email, name }) => {
75
+ if (!isTestMode()) return null;
76
+ const existing = await ctx.db.query("users").filter(((q) => q.eq(q.field("email"), email))).first();
77
+ if (existing) return existing._id;
78
+ return ctx.db.insert("users", {
79
+ email,
80
+ emailVerificationTime: Date.now(),
81
+ name
82
+ });
83
+ }
84
+ }), getTestUserByEmail = query({
85
+ args: { email: v.string() },
86
+ handler: async (ctx, { email }) => {
87
+ if (!isTestMode()) return null;
88
+ return (await ctx.db.query("users").filter(((q) => q.eq(q.field("email"), email))).first())?._id ?? null;
89
+ }
90
+ }), addTestOrgMember = mutation({
91
+ args: {
92
+ isAdmin: v.boolean(),
93
+ orgId: v.id("org"),
94
+ userId: v.id("users")
95
+ },
96
+ handler: async (ctx, { isAdmin, orgId, userId }) => {
97
+ if (!isTestMode()) return null;
98
+ const existing = await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique();
99
+ if (existing) {
100
+ await ctx.db.patch(existing._id, {
101
+ isAdmin,
102
+ updatedAt: Date.now()
103
+ });
104
+ return existing._id;
105
+ }
106
+ return ctx.db.insert("orgMember", {
107
+ isAdmin,
108
+ orgId,
109
+ updatedAt: Date.now(),
110
+ userId
111
+ });
112
+ }
113
+ }), removeTestOrgMember = mutation({
114
+ args: {
115
+ orgId: v.id("org"),
116
+ userId: v.id("users")
117
+ },
118
+ handler: async (ctx, { orgId, userId }) => {
119
+ if (!isTestMode()) return false;
120
+ const member = await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique();
121
+ if (member) {
122
+ await ctx.db.delete(member._id);
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ }), cleanupTestUsers = mutation({
128
+ args: { emailPrefix: v.string() },
129
+ handler: async (ctx, { emailPrefix }) => {
130
+ if (!isTestMode()) return { count: 0 };
131
+ const users = await ctx.db.query("users").collect();
132
+ let count = 0;
133
+ for (const u of users) if (u.email?.startsWith(emailPrefix) && u.email !== TEST_EMAIL) {
134
+ await ctx.db.delete(u._id);
135
+ count += 1;
136
+ }
137
+ return { count };
138
+ }
139
+ }), cleanupOrgTestData = mutation({
140
+ args: {
141
+ slugPrefix: v.string(),
142
+ tables: v.optional(v.array(v.string()))
143
+ },
144
+ handler: async (ctx, { slugPrefix, tables }) => {
145
+ if (!isTestMode()) return {
146
+ count: 0,
147
+ done: true
148
+ };
149
+ const allOrgs = await ctx.db.query("org").collect(), orgIds = [];
150
+ for (const o of allOrgs) if (o.slug.startsWith(slugPrefix)) orgIds.push(o._id);
151
+ if (orgIds.length === 0) return {
152
+ count: 0,
153
+ done: true
154
+ };
155
+ let count = 0;
156
+ for (const orgId of orgIds) {
157
+ if (tables) for (const table of tables) {
158
+ const docs = await ctx.db.query(table).take(BATCH_SIZE * 2);
159
+ for (const d of docs) if (d.orgId === orgId) {
160
+ await ctx.db.delete(d._id);
161
+ count += 1;
162
+ }
163
+ }
164
+ const requests = await ctx.db.query("orgJoinRequest").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
165
+ for (const r of requests) {
166
+ await ctx.db.delete(r._id);
167
+ count += 1;
168
+ }
169
+ const invites = await ctx.db.query("orgInvite").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
170
+ for (const i of invites) {
171
+ await ctx.db.delete(i._id);
172
+ count += 1;
173
+ }
174
+ const members = await ctx.db.query("orgMember").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
175
+ for (const m of members) {
176
+ await ctx.db.delete(m._id);
177
+ count += 1;
178
+ }
179
+ await ctx.db.delete(orgId);
180
+ count += 1;
181
+ }
182
+ return {
183
+ count,
184
+ done: true
185
+ };
186
+ }
187
+ }), inviteAsUser = mutation({
188
+ args: {
189
+ email: v.string(),
190
+ isAdmin: v.boolean(),
191
+ orgId: v.id("org"),
192
+ userId: v.id("users")
193
+ },
194
+ handler: async (ctx, { email, isAdmin, orgId, userId }) => {
195
+ if (!isTestMode()) return null;
196
+ const membership = await getOrgMembership$1(ctx.db, orgId, userId);
197
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
198
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
199
+ const token = generateToken$1();
200
+ return {
201
+ inviteId: await ctx.db.insert("orgInvite", {
202
+ email,
203
+ expiresAt: Date.now() + 10080 * 60 * 1e3,
204
+ isAdmin,
205
+ orgId,
206
+ token
207
+ }),
208
+ token
209
+ };
210
+ }
211
+ }), acceptInviteAsUser = mutation({
212
+ args: {
213
+ token: v.string(),
214
+ userId: v.id("users")
215
+ },
216
+ handler: async (ctx, { token, userId }) => {
217
+ if (!isTestMode()) return null;
218
+ const inviteDoc = await ctx.db.query("orgInvite").withIndex("by_token", ((q) => q.eq("token", token))).unique();
219
+ if (!inviteDoc) return { code: "INVALID_INVITE" };
220
+ if (inviteDoc.expiresAt < Date.now()) return { code: "INVITE_EXPIRED" };
221
+ const orgDoc = await ctx.db.get(inviteDoc.orgId);
222
+ if (!orgDoc) return { code: "NOT_FOUND" };
223
+ if (orgDoc.userId === userId) return { code: "ALREADY_ORG_MEMBER" };
224
+ if (await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", inviteDoc.orgId).eq("userId", userId))).unique()) return { code: "ALREADY_ORG_MEMBER" };
225
+ const pendingRequest = await ctx.db.query("orgJoinRequest").withIndex("by_org_status", ((q) => q.eq("orgId", inviteDoc.orgId).eq("status", "pending"))).filter(((q) => q.eq(q.field("userId"), userId))).unique();
226
+ if (pendingRequest) await ctx.db.patch(pendingRequest._id, { status: "approved" });
227
+ await ctx.db.insert("orgMember", {
228
+ isAdmin: inviteDoc.isAdmin,
229
+ orgId: inviteDoc.orgId,
230
+ updatedAt: Date.now(),
231
+ userId
232
+ });
233
+ await ctx.db.delete(inviteDoc._id);
234
+ return { orgId: inviteDoc.orgId };
235
+ }
236
+ }), setAdminAsUser = mutation({
237
+ args: {
238
+ isAdmin: v.boolean(),
239
+ memberId: v.id("orgMember"),
240
+ userId: v.id("users")
241
+ },
242
+ handler: async (ctx, { isAdmin, memberId, userId }) => {
243
+ if (!isTestMode()) return null;
244
+ const memberDoc = await ctx.db.get(memberId);
245
+ if (!memberDoc) return { code: "NOT_FOUND" };
246
+ const orgDoc = await ctx.db.get(memberDoc.orgId);
247
+ if (!orgDoc) return { code: "NOT_FOUND" };
248
+ if (orgDoc.userId !== userId) return { code: "INSUFFICIENT_ORG_ROLE" };
249
+ if (memberDoc.userId === orgDoc.userId) return { code: "CANNOT_MODIFY_OWNER" };
250
+ await ctx.db.patch(memberId, {
251
+ isAdmin,
252
+ updatedAt: Date.now()
253
+ });
254
+ return { success: true };
255
+ }
256
+ }), removeMemberAsUser = mutation({
257
+ args: {
258
+ memberId: v.id("orgMember"),
259
+ userId: v.id("users")
260
+ },
261
+ handler: async (ctx, { memberId, userId }) => {
262
+ if (!isTestMode()) return null;
263
+ const memberDoc = await ctx.db.get(memberId);
264
+ if (!memberDoc) return { code: "NOT_FOUND" };
265
+ const membership = await getOrgMembership$1(ctx.db, memberDoc.orgId, userId);
266
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
267
+ if (memberDoc.userId === membership.orgDoc.userId) return { code: "CANNOT_MODIFY_OWNER" };
268
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
269
+ if (!membership.isOwner && memberDoc.isAdmin) return { code: "CANNOT_MODIFY_ADMIN" };
270
+ await ctx.db.delete(memberId);
271
+ return { success: true };
272
+ }
273
+ }), transferOwnershipAsUser = mutation({
274
+ args: {
275
+ newOwnerId: v.id("users"),
276
+ orgId: v.id("org"),
277
+ userId: v.id("users")
278
+ },
279
+ handler: async (ctx, { newOwnerId, orgId, userId }) => {
280
+ if (!isTestMode()) return null;
281
+ const orgDoc = await ctx.db.get(orgId);
282
+ if (!orgDoc) return { code: "NOT_FOUND" };
283
+ if (orgDoc.userId !== userId) return { code: "INSUFFICIENT_ORG_ROLE" };
284
+ const targetMember = await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", newOwnerId))).unique();
285
+ if (!targetMember) return { code: "NOT_ORG_MEMBER" };
286
+ if (!targetMember.isAdmin) return { code: "TARGET_MUST_BE_ADMIN" };
287
+ await ctx.db.patch(orgId, {
288
+ updatedAt: Date.now(),
289
+ userId: newOwnerId
290
+ });
291
+ await ctx.db.delete(targetMember._id);
292
+ await ctx.db.insert("orgMember", {
293
+ isAdmin: true,
294
+ orgId,
295
+ updatedAt: Date.now(),
296
+ userId
297
+ });
298
+ return { success: true };
299
+ }
300
+ }), updateOrgAsUser = mutation({
301
+ args: {
302
+ data: v.object({
303
+ name: v.optional(v.string()),
304
+ slug: v.optional(v.string())
305
+ }),
306
+ orgId: v.id("org"),
307
+ userId: v.id("users")
308
+ },
309
+ handler: async (ctx, { data, orgId, userId }) => {
310
+ if (!isTestMode()) return null;
311
+ const membership = await getOrgMembership$1(ctx.db, orgId, userId);
312
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
313
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
314
+ if (data.slug !== void 0) {
315
+ const existing = await ctx.db.query("org").withIndex("by_slug", ((q) => q.eq("slug", data.slug))).unique();
316
+ if (existing && existing._id !== orgId) return { code: "ORG_SLUG_TAKEN" };
317
+ }
318
+ await ctx.db.patch(orgId, {
319
+ ...data,
320
+ updatedAt: Date.now()
321
+ });
322
+ return { success: true };
323
+ }
324
+ }), deleteOrgAsUser = mutation({
325
+ args: {
326
+ cascadeTables: v.optional(v.array(v.string())),
327
+ orgId: v.id("org"),
328
+ userId: v.id("users")
329
+ },
330
+ handler: async (ctx, { cascadeTables, orgId, userId }) => {
331
+ if (!isTestMode()) return null;
332
+ const orgDoc = await ctx.db.get(orgId);
333
+ if (!orgDoc) return { code: "NOT_FOUND" };
334
+ if (orgDoc.userId !== userId) return { code: "INSUFFICIENT_ORG_ROLE" };
335
+ if (cascadeTables) for (const table of cascadeTables) {
336
+ const docs = await ctx.db.query(table).filter(((q) => q.eq(q.field("orgId"), orgId))).collect();
337
+ for (const d of docs) await ctx.db.delete(d._id);
338
+ }
339
+ const requests = await ctx.db.query("orgJoinRequest").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
340
+ for (const r of requests) await ctx.db.delete(r._id);
341
+ const invites = await ctx.db.query("orgInvite").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
342
+ for (const i of invites) await ctx.db.delete(i._id);
343
+ const orgMembers = await ctx.db.query("orgMember").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
344
+ for (const m of orgMembers) await ctx.db.delete(m._id);
345
+ await ctx.db.delete(orgId);
346
+ return { success: true };
347
+ }
348
+ }), leaveOrgAsUser = mutation({
349
+ args: {
350
+ orgId: v.id("org"),
351
+ userId: v.id("users")
352
+ },
353
+ handler: async (ctx, { orgId, userId }) => {
354
+ if (!isTestMode()) return null;
355
+ const orgDoc = await ctx.db.get(orgId);
356
+ if (!orgDoc) return { code: "NOT_FOUND" };
357
+ if (orgDoc.userId === userId) return { code: "MUST_TRANSFER_OWNERSHIP" };
358
+ const member = await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique();
359
+ if (!member) return { code: "NOT_ORG_MEMBER" };
360
+ await ctx.db.delete(member._id);
361
+ return { success: true };
362
+ }
363
+ }), requestJoinAsUser = mutation({
364
+ args: {
365
+ message: v.optional(v.string()),
366
+ orgId: v.id("org"),
367
+ userId: v.id("users")
368
+ },
369
+ handler: async (ctx, { message, orgId, userId }) => {
370
+ if (!isTestMode()) return null;
371
+ const orgDoc = await ctx.db.get(orgId);
372
+ if (!orgDoc) return { code: "NOT_FOUND" };
373
+ if (orgDoc.userId === userId) return { code: "ALREADY_ORG_MEMBER" };
374
+ if (await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique()) return { code: "ALREADY_ORG_MEMBER" };
375
+ if (await ctx.db.query("orgJoinRequest").withIndex("by_org_status", ((q) => q.eq("orgId", orgId).eq("status", "pending"))).filter(((q) => q.eq(q.field("userId"), userId))).unique()) return { code: "JOIN_REQUEST_EXISTS" };
376
+ return { requestId: await ctx.db.insert("orgJoinRequest", {
377
+ message,
378
+ orgId,
379
+ status: "pending",
380
+ userId
381
+ }) };
382
+ }
383
+ }), approveJoinRequestAsUser = mutation({
384
+ args: {
385
+ isAdmin: v.boolean(),
386
+ requestId: v.id("orgJoinRequest"),
387
+ userId: v.id("users")
388
+ },
389
+ handler: async (ctx, { isAdmin, requestId, userId }) => {
390
+ if (!isTestMode()) return null;
391
+ const request = await ctx.db.get(requestId);
392
+ if (request?.status !== "pending") return { code: "NOT_FOUND" };
393
+ const membership = await getOrgMembership$1(ctx.db, request.orgId, userId);
394
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
395
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
396
+ if (await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", request.orgId).eq("userId", request.userId))).unique()) return { code: "ALREADY_ORG_MEMBER" };
397
+ await ctx.db.patch(requestId, { status: "approved" });
398
+ await ctx.db.insert("orgMember", {
399
+ isAdmin,
400
+ orgId: request.orgId,
401
+ updatedAt: Date.now(),
402
+ userId: request.userId
403
+ });
404
+ return { success: true };
405
+ }
406
+ }), rejectJoinRequestAsUser = mutation({
407
+ args: {
408
+ requestId: v.id("orgJoinRequest"),
409
+ userId: v.id("users")
410
+ },
411
+ handler: async (ctx, { requestId, userId }) => {
412
+ if (!isTestMode()) return null;
413
+ const request = await ctx.db.get(requestId);
414
+ if (request?.status !== "pending") return { code: "NOT_FOUND" };
415
+ const membership = await getOrgMembership$1(ctx.db, request.orgId, userId);
416
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
417
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
418
+ await ctx.db.patch(requestId, { status: "rejected" });
419
+ return { success: true };
420
+ }
421
+ }), cancelJoinRequestAsUser = mutation({
422
+ args: {
423
+ requestId: v.id("orgJoinRequest"),
424
+ userId: v.id("users")
425
+ },
426
+ handler: async (ctx, { requestId, userId }) => {
427
+ if (!isTestMode()) return null;
428
+ const requestDoc = await ctx.db.get(requestId);
429
+ if (!requestDoc) return { code: "NOT_FOUND" };
430
+ if (requestDoc.userId !== userId) return { code: "FORBIDDEN" };
431
+ if (requestDoc.status !== "pending") return { code: "NOT_FOUND" };
432
+ await ctx.db.delete(requestId);
433
+ return { success: true };
434
+ }
435
+ }), pendingInvitesAsUser = query({
436
+ args: {
437
+ orgId: v.id("org"),
438
+ userId: v.id("users")
439
+ },
440
+ handler: async (ctx, { orgId, userId }) => {
441
+ if (!isTestMode()) return null;
442
+ const membership = await getOrgMembership$1(ctx.db, orgId, userId);
443
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
444
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
445
+ return ctx.db.query("orgInvite").withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
446
+ }
447
+ }), pendingJoinRequestsAsUser = query({
448
+ args: {
449
+ orgId: v.id("org"),
450
+ userId: v.id("users")
451
+ },
452
+ handler: async (ctx, { orgId, userId }) => {
453
+ if (!isTestMode()) return null;
454
+ const membership = await getOrgMembership$1(ctx.db, orgId, userId);
455
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
456
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
457
+ return ctx.db.query("orgJoinRequest").withIndex("by_org_status", ((q) => q.eq("orgId", orgId).eq("status", "pending"))).collect();
458
+ }
459
+ });
460
+ return {
461
+ acceptInviteAsUser,
462
+ addTestOrgMember,
463
+ approveJoinRequestAsUser,
464
+ cancelJoinRequestAsUser,
465
+ cleanupOrgTestData,
466
+ cleanupTestUsers,
467
+ createExpiredInvite: mutation({
468
+ args: {
469
+ email: v.string(),
470
+ isAdmin: v.boolean(),
471
+ orgId: v.id("org")
472
+ },
473
+ handler: async (ctx, { email, isAdmin, orgId }) => {
474
+ if (!isTestMode()) return null;
475
+ const token = generateToken$1();
476
+ return {
477
+ inviteId: await ctx.db.insert("orgInvite", {
478
+ email,
479
+ expiresAt: Date.now() - 1e3,
480
+ isAdmin,
481
+ orgId,
482
+ token
483
+ }),
484
+ token
485
+ };
486
+ }
487
+ }),
488
+ createTestUser,
489
+ deleteOrgAsUser,
490
+ ensureTestUser,
491
+ getAuthUserIdOrTest,
492
+ getJoinRequest: query({
493
+ args: { requestId: v.id("orgJoinRequest") },
494
+ handler: async (ctx, { requestId }) => {
495
+ if (!isTestMode()) return null;
496
+ return ctx.db.get(requestId);
497
+ }
498
+ }),
499
+ getTestUser,
500
+ getTestUserByEmail,
501
+ inviteAsUser,
502
+ isTestMode,
503
+ leaveOrgAsUser,
504
+ pendingInvitesAsUser,
505
+ pendingJoinRequestsAsUser,
506
+ rejectJoinRequestAsUser,
507
+ removeMemberAsUser,
508
+ removeTestOrgMember,
509
+ requestJoinAsUser,
510
+ setAdminAsUser,
511
+ TEST_EMAIL,
512
+ transferOwnershipAsUser,
513
+ updateOrgAsUser
514
+ };
515
+ };
516
+
517
+ //#endregion
518
+ //#region src/server/file.ts
519
+ const DEFAULT_ALLOWED_TYPES = new Set([
520
+ "application/json",
521
+ "application/msword",
522
+ "application/pdf",
523
+ "application/vnd.ms-excel",
524
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
525
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
526
+ "image/gif",
527
+ "image/jpeg",
528
+ "image/png",
529
+ "image/svg+xml",
530
+ "image/webp",
531
+ "text/csv",
532
+ "text/plain"
533
+ ]), DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024, CHUNK_SIZE = 5 * 1024 * 1024, RATE_LIMIT_WINDOW = 60 * 1e3, MAX_UPLOADS_PER_WINDOW = 10, cvErr = (code, message) => new ConvexError(message ? {
534
+ code,
535
+ message
536
+ } : { code }), makeFileUpload = (config) => {
537
+ const { action, allowedTypes = DEFAULT_ALLOWED_TYPES, getAuthUserId, internalMutation, internalQuery, maxFileSize = DEFAULT_MAX_FILE_SIZE, mutation, namespace, query } = config, tPath = anyApi[namespace], authUserId = async (ctx) => getAuthUserId(ctx), validateFileType = async (storage, id, contentType) => {
538
+ if (!allowedTypes.has(contentType ?? "")) {
539
+ await storage.delete(id);
540
+ throw cvErr("INVALID_FILE_TYPE", `File type ${contentType} not allowed`);
541
+ }
542
+ }, validateFileSize = async (storage, id, size) => {
543
+ if (size > maxFileSize) {
544
+ await storage.delete(id);
545
+ throw cvErr("FILE_TOO_LARGE", `File size ${size} exceeds ${maxFileSize} bytes`);
546
+ }
547
+ }, checkRateLimit = async (db, userId) => {
548
+ const now = Date.now(), cutoff = now - RATE_LIMIT_WINDOW;
549
+ if ((await db.query("uploadRateLimit").withIndex("by_user", ((q) => q.eq("userId", userId))).filter((q) => q.gte(q.field("timestamp"), cutoff)).collect()).length >= MAX_UPLOADS_PER_WINDOW) throw cvErr("RATE_LIMITED");
550
+ await db.insert("uploadRateLimit", {
551
+ timestamp: now,
552
+ userId
553
+ });
554
+ const old = await db.query("uploadRateLimit").withIndex("by_user", ((q) => q.eq("userId", userId))).filter((q) => q.lt(q.field("timestamp"), cutoff)).collect();
555
+ await Promise.all(old.map(async (r) => db.delete(r._id)));
556
+ }, upload = mutation({ handler: async (c) => {
557
+ const userId = await authUserId(c);
558
+ if (!userId) throw cvErr("NOT_AUTHENTICATED");
559
+ if (!isTestMode()) await checkRateLimit(c.db, userId);
560
+ return c.storage.generateUploadUrl();
561
+ } }), validate = mutation({
562
+ args: { id: v.id("_storage") },
563
+ handler: async (c, { id }) => {
564
+ if (!await authUserId(c)) throw cvErr("NOT_AUTHENTICATED");
565
+ const meta = await c.db.system.get(id);
566
+ if (!meta) throw cvErr("FILE_NOT_FOUND");
567
+ await validateFileType(c.storage, id, meta.contentType);
568
+ await validateFileSize(c.storage, id, meta.size);
569
+ return {
570
+ contentType: meta.contentType,
571
+ size: meta.size,
572
+ valid: true
573
+ };
574
+ }
575
+ }), info = query({
576
+ args: { id: v.id("_storage") },
577
+ handler: async (c, { id }) => {
578
+ if (!await authUserId(c)) throw cvErr("NOT_AUTHENTICATED");
579
+ const [meta, url] = await Promise.all([c.db.system.get(id), c.storage.getUrl(id)]);
580
+ return meta ? {
581
+ ...meta,
582
+ url
583
+ } : null;
584
+ }
585
+ }), startChunkedUpload = mutation({
586
+ args: {
587
+ contentType: v.string(),
588
+ fileName: v.string(),
589
+ totalChunks: v.number(),
590
+ totalSize: v.number()
591
+ },
592
+ handler: async (c, { contentType, fileName, totalChunks, totalSize }) => {
593
+ const userId = await authUserId(c);
594
+ if (!userId) throw cvErr("NOT_AUTHENTICATED");
595
+ if (!isTestMode()) await checkRateLimit(c.db, userId);
596
+ if (!allowedTypes.has(contentType)) throw cvErr("INVALID_FILE_TYPE", `File type ${contentType} not allowed`);
597
+ if (totalSize > maxFileSize) throw cvErr("FILE_TOO_LARGE", `File size ${totalSize} exceeds ${maxFileSize} bytes`);
598
+ const uploadId = `${userId}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
599
+ await c.db.insert("uploadSession", {
600
+ completedChunks: 0,
601
+ contentType,
602
+ fileName,
603
+ status: "pending",
604
+ totalChunks,
605
+ totalSize,
606
+ uploadId,
607
+ userId
608
+ });
609
+ return { uploadId };
610
+ }
611
+ }), uploadChunk = mutation({
612
+ args: {
613
+ chunkIndex: v.number(),
614
+ uploadId: v.string()
615
+ },
616
+ handler: async (c, { chunkIndex, uploadId }) => {
617
+ const userId = await authUserId(c);
618
+ if (!userId) throw cvErr("NOT_AUTHENTICATED");
619
+ const session = await c.db.query("uploadSession").withIndex("by_upload_id", ((q) => q.eq("uploadId", uploadId))).unique();
620
+ if (!session) throw cvErr("SESSION_NOT_FOUND");
621
+ if (session.userId !== userId) throw cvErr("UNAUTHORIZED");
622
+ if (session.status !== "pending") throw cvErr("INVALID_SESSION_STATE");
623
+ if (await c.db.query("uploadChunk").withIndex("by_upload", ((q) => q.eq("uploadId", uploadId))).filter((q) => q.eq(q.field("chunkIndex"), chunkIndex)).unique()) throw cvErr("CHUNK_ALREADY_UPLOADED");
624
+ return c.storage.generateUploadUrl();
625
+ }
626
+ }), confirmChunk = mutation({
627
+ args: {
628
+ chunkIndex: v.number(),
629
+ storageId: v.id("_storage"),
630
+ uploadId: v.string()
631
+ },
632
+ handler: async (c, { chunkIndex, storageId, uploadId }) => {
633
+ const userId = await authUserId(c);
634
+ if (!userId) throw cvErr("NOT_AUTHENTICATED");
635
+ const session = await c.db.query("uploadSession").withIndex("by_upload_id", ((q) => q.eq("uploadId", uploadId))).unique();
636
+ if (!session) throw cvErr("SESSION_NOT_FOUND");
637
+ if (session.userId !== userId) throw cvErr("UNAUTHORIZED");
638
+ await c.db.insert("uploadChunk", {
639
+ chunkIndex,
640
+ storageId,
641
+ totalChunks: session.totalChunks,
642
+ uploadId,
643
+ userId
644
+ });
645
+ const chunks = await c.db.query("uploadChunk").withIndex("by_upload", ((q) => q.eq("uploadId", uploadId))).collect();
646
+ await c.db.patch(session._id, { completedChunks: chunks.length });
647
+ return {
648
+ allUploaded: chunks.length === session.totalChunks,
649
+ completedChunks: chunks.length,
650
+ totalChunks: session.totalChunks
651
+ };
652
+ }
653
+ }), getSessionForAssembly = internalQuery({
654
+ args: { uploadId: v.string() },
655
+ handler: async (c, { uploadId }) => {
656
+ const session = await c.db.query("uploadSession").withIndex("by_upload_id", ((q) => q.eq("uploadId", uploadId))).unique();
657
+ if (!session) return null;
658
+ const chunks = await c.db.query("uploadChunk").withIndex("by_upload", ((q) => q.eq("uploadId", uploadId))).collect();
659
+ if (chunks.length !== session.totalChunks) throw cvErr("INCOMPLETE_UPLOAD");
660
+ return {
661
+ ...session,
662
+ chunks
663
+ };
664
+ }
665
+ }), finalizeAssembly = internalMutation({
666
+ args: {
667
+ chunkStorageIds: v.array(v.id("_storage")),
668
+ finalStorageId: v.id("_storage"),
669
+ uploadId: v.string()
670
+ },
671
+ handler: async (c, { chunkStorageIds, finalStorageId, uploadId }) => {
672
+ const session = await c.db.query("uploadSession").withIndex("by_upload_id", ((q) => q.eq("uploadId", uploadId))).unique();
673
+ if (!session) throw cvErr("SESSION_NOT_FOUND");
674
+ await c.db.patch(session._id, {
675
+ finalStorageId,
676
+ status: "completed"
677
+ });
678
+ const chunks = await c.db.query("uploadChunk").withIndex("by_upload", ((q) => q.eq("uploadId", uploadId))).collect();
679
+ await Promise.all([...chunkStorageIds.map(async (id) => c.storage.delete(id)), ...chunks.map(async (chunk) => c.db.delete(chunk._id))]);
680
+ }
681
+ });
682
+ return {
683
+ assembleChunks: action({
684
+ args: { uploadId: v.string() },
685
+ handler: async (c, { uploadId }) => {
686
+ const session = await c.runQuery(tPath.getSessionForAssembly, { uploadId });
687
+ if (!session) throw cvErr("SESSION_NOT_FOUND");
688
+ if (session.status !== "pending") throw cvErr("INVALID_SESSION_STATE");
689
+ const sortedChunks = session.chunks.toSorted((a, b) => a.chunkIndex - b.chunkIndex), chunkBlobs = await Promise.all(sortedChunks.map(async (chunk) => {
690
+ const blob = await c.storage.get(chunk.storageId);
691
+ if (!blob) throw cvErr("CHUNK_NOT_FOUND");
692
+ return blob;
693
+ })), combinedBlob = new Blob(chunkBlobs, { type: session.contentType }), finalStorageId = await c.storage.store(combinedBlob);
694
+ await c.runMutation(tPath.finalizeAssembly, {
695
+ chunkStorageIds: sortedChunks.map((ch) => ch.storageId),
696
+ finalStorageId,
697
+ uploadId
698
+ });
699
+ return {
700
+ contentType: session.contentType,
701
+ size: session.totalSize,
702
+ storageId: finalStorageId
703
+ };
704
+ }
705
+ }),
706
+ cancelChunkedUpload: mutation({
707
+ args: { uploadId: v.string() },
708
+ handler: async (c, { uploadId }) => {
709
+ const userId = await authUserId(c);
710
+ if (!userId) throw cvErr("NOT_AUTHENTICATED");
711
+ const session = await c.db.query("uploadSession").withIndex("by_upload_id", ((q) => q.eq("uploadId", uploadId))).unique();
712
+ if (!session) throw cvErr("SESSION_NOT_FOUND");
713
+ if (session.userId !== userId) throw cvErr("UNAUTHORIZED");
714
+ const chunks = await c.db.query("uploadChunk").withIndex("by_upload", ((q) => q.eq("uploadId", uploadId))).collect();
715
+ await Promise.all(chunks.map(async (chunk) => c.storage.delete(chunk.storageId)));
716
+ await Promise.all(chunks.map(async (chunk) => c.db.delete(chunk._id)));
717
+ await c.db.patch(session._id, { status: "failed" });
718
+ return { cancelled: true };
719
+ }
720
+ }),
721
+ CHUNK_SIZE,
722
+ confirmChunk,
723
+ finalizeAssembly,
724
+ getSessionForAssembly,
725
+ getUploadProgress: query({
726
+ args: { uploadId: v.string() },
727
+ handler: async (c, { uploadId }) => {
728
+ const userId = await authUserId(c);
729
+ if (!userId) throw cvErr("NOT_AUTHENTICATED");
730
+ const session = await c.db.query("uploadSession").withIndex("by_upload_id", ((q) => q.eq("uploadId", uploadId))).unique();
731
+ if (!session) return null;
732
+ if (session.userId !== userId) throw cvErr("UNAUTHORIZED");
733
+ return {
734
+ completedChunks: session.completedChunks,
735
+ finalStorageId: session.finalStorageId,
736
+ progress: Math.round(session.completedChunks / session.totalChunks * 100),
737
+ status: session.status,
738
+ totalChunks: session.totalChunks
739
+ };
740
+ }
741
+ }),
742
+ info,
743
+ startChunkedUpload,
744
+ upload,
745
+ uploadChunk,
746
+ validate
747
+ };
748
+ };
749
+
750
+ //#endregion
751
+ //#region src/server/helpers.ts
752
+ const log = (level, msg, data) => {
753
+ console[level](JSON.stringify({
754
+ level,
755
+ msg,
756
+ ts: Date.now(),
757
+ ...data
758
+ }));
759
+ }, isRecord$1 = (v) => Boolean(v) && typeof v === "object", isComparisonOp = (val) => typeof val === "object" && val !== null && ("$gt" in val || "$gte" in val || "$lt" in val || "$lte" in val || "$between" in val), pgOpts = object({
760
+ cursor: nullable(string()),
761
+ endCursor: nullable(string()).optional(),
762
+ id: number().optional(),
763
+ maximumBytesRead: number().optional(),
764
+ maximumRowsRead: number().optional(),
765
+ numItems: number()
766
+ }), cascadeFor = (parent, children) => {
767
+ const result = [];
768
+ for (const [table, c] of Object.entries(children)) if (c.parent === parent) result.push({
769
+ foreignKey: c.foreignKey,
770
+ table
771
+ });
772
+ return result;
773
+ }, detectFiles = (s) => Object.keys(s).filter((k) => cvFileKindOf(s[k])), err = (code, debug) => {
774
+ throw new ConvexError(debug ? {
775
+ code,
776
+ debug
777
+ } : { code });
778
+ }, noFetcher = () => err("NO_FETCHER"), time = () => ({ updatedAt: Date.now() }), getUser = async ({ ctx, db, getAuthUserId }) => {
779
+ const uid = await getAuthUserId(ctx);
780
+ if (!uid) return err("NOT_AUTHENTICATED");
781
+ return await db.get(uid) ?? err("USER_NOT_FOUND");
782
+ }, ownGet = (db, userId) => async (id) => {
783
+ const d = await db.get(id);
784
+ return d && d.userId === userId ? d : err("NOT_FOUND");
785
+ }, readCtx = ({ db, storage, viewerId }) => ({
786
+ db,
787
+ storage,
788
+ viewerId,
789
+ withAuthor: async (docs) => {
790
+ const ids = [...new Set(docs.map((d) => d.userId))], users = await Promise.all(ids.map(async (id) => db.get(id))), map = new Map(ids.map((id, i) => [id, users[i]]));
791
+ return docs.map((d) => ({
792
+ ...d,
793
+ author: map.get(d.userId) ?? null,
794
+ own: viewerId ? viewerId === d.userId : null
795
+ }));
796
+ }
797
+ }), toId = (x) => typeof x === "string" ? x : null, cleanFiles = async (opts) => {
798
+ const { doc, fileFields, next, storage } = opts;
799
+ if (!fileFields.length) return;
800
+ const del = /* @__PURE__ */ new Set();
801
+ for (const f of fileFields) {
802
+ const prev = doc[f];
803
+ if (prev === null) continue;
804
+ const pArr = Array.isArray(prev) ? prev : [prev];
805
+ if (!next) {
806
+ for (const p of pArr) {
807
+ const id = toId(p);
808
+ if (id) del.add(id);
809
+ }
810
+ continue;
811
+ }
812
+ if (!Object.hasOwn(next, f)) continue;
813
+ const nv = next[f], keep = new Set(Array.isArray(nv) ? nv : nv ? [nv] : []);
814
+ for (const p of pArr) if (!keep.has(p)) {
815
+ const id = toId(p);
816
+ if (id) del.add(id);
817
+ }
818
+ }
819
+ if (del.size) await Promise.all([...del].map(async (id) => storage.delete(id)));
820
+ }, addUrls = async ({ doc, fileFields, storage }) => {
821
+ if (!fileFields.length) return doc;
822
+ const o = { ...doc }, getUrl = async (x) => {
823
+ const id = toId(x);
824
+ return id ? storage.getUrl(id) : null;
825
+ };
826
+ for (const f of fileFields) {
827
+ const fv = doc[f];
828
+ if (fv !== null) o[Array.isArray(fv) ? `${f}Urls` : `${f}Url`] = Array.isArray(fv) ? await Promise.all(fv.map(getUrl)) : await getUrl(fv);
829
+ }
830
+ return o;
831
+ }, matchField = (docVal, filterVal) => {
832
+ if (isComparisonOp(filterVal)) {
833
+ const dv = docVal;
834
+ if (filterVal.$gt !== void 0 && !(dv > filterVal.$gt)) return false;
835
+ if (filterVal.$gte !== void 0 && !(dv >= filterVal.$gte)) return false;
836
+ if (filterVal.$lt !== void 0 && !(dv < filterVal.$lt)) return false;
837
+ if (filterVal.$lte !== void 0 && !(dv <= filterVal.$lte)) return false;
838
+ if (filterVal.$between !== void 0) {
839
+ const [min, max] = filterVal.$between;
840
+ if (!(dv >= min && dv <= max)) return false;
841
+ }
842
+ return true;
843
+ }
844
+ return Object.is(docVal, filterVal);
845
+ }, groupList = (w) => w ? [{
846
+ ...w,
847
+ or: void 0
848
+ }, ...w.or ?? []].filter((g) => g.own ?? Object.keys(g).some((k) => k !== "own" && g[k] !== void 0)) : [], matchW = (doc, w, vid) => {
849
+ const gs = groupList(w);
850
+ if (!gs.length) return true;
851
+ for (const g of gs) if (Object.entries(g).every(([k, vl]) => k === "own" || vl === void 0 || matchField(doc[k], vl)) && (!g.own || vid === doc.userId)) return true;
852
+ return false;
853
+ }, pickFields = (data, keys) => {
854
+ const result = {};
855
+ for (const k of keys) if (k in data) result[k] = data[k];
856
+ return result;
857
+ }, errValidation = (code, zodError) => {
858
+ const { fieldErrors } = zodError.flatten(), fields = Object.keys(fieldErrors);
859
+ throw new ConvexError({
860
+ code,
861
+ fields,
862
+ message: fields.length ? `Invalid: ${fields.join(", ")}` : "Validation failed"
863
+ });
864
+ };
865
+
866
+ //#endregion
867
+ //#region src/server/org-helpers.ts
868
+ const ROLE_LEVEL = {
869
+ admin: 2,
870
+ member: 1,
871
+ owner: 3
872
+ }, getOrgRole = ({ member, org, userId }) => {
873
+ if (org.userId === userId) return "owner";
874
+ if (!member) return null;
875
+ return member.isAdmin ? "admin" : "member";
876
+ }, getOrgMember = async ({ db, orgId, userId }) => db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId))).filter((f) => f.eq(f.field("userId"), userId)).unique(), requireOrgMember = async ({ db, orgId, userId }) => {
877
+ const org = await db.get(orgId);
878
+ if (!org) return err("NOT_FOUND");
879
+ const member = await getOrgMember({
880
+ db,
881
+ orgId,
882
+ userId
883
+ }), role = getOrgRole({
884
+ member,
885
+ org,
886
+ userId
887
+ });
888
+ if (!role) return err("NOT_ORG_MEMBER");
889
+ return {
890
+ member,
891
+ org,
892
+ role
893
+ };
894
+ };
895
+ const requireOrgRole = async ({ db, minRole, orgId, userId }) => {
896
+ const result = await requireOrgMember({
897
+ db,
898
+ orgId,
899
+ userId
900
+ });
901
+ if (ROLE_LEVEL[result.role] < ROLE_LEVEL[minRole]) return err("INSUFFICIENT_ORG_ROLE");
902
+ return result;
903
+ }, canEdit = ({ acl, doc, role, userId }) => {
904
+ if (role === "owner" || role === "admin") return true;
905
+ if (doc.userId === userId) return true;
906
+ if (acl && doc.editors?.includes(userId)) return true;
907
+ return false;
908
+ };
909
+
910
+ //#endregion
911
+ //#region src/server/org.ts
912
+ const generateToken = () => {
913
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
914
+ let token = "";
915
+ for (let i = 0; i < 32; i += 1) token += chars.charAt(Math.floor(Math.random() * 62));
916
+ return token;
917
+ }, SEVEN_DAYS_MS = 10080 * 60 * 1e3, makeOrg = ({ cascadeTables, getAuthUserId, mutation, query, schema: orgSchema }) => {
918
+ const mb = zCustomMutation(mutation, customCtx(async (c) => ({ user: await getUser({
919
+ ctx: c,
920
+ db: c.db,
921
+ getAuthUserId
922
+ }) }))), qb = zCustomQuery(query, customCtx(async (c) => ({ user: await getUser({
923
+ ctx: c,
924
+ db: c.db,
925
+ getAuthUserId
926
+ }) }))), pqb = zCustomQuery(query, customCtx(() => ({}))), m = mb, q = qb, pq = pqb, create = m({
927
+ args: { data: orgSchema },
928
+ handler: async (c, { data }) => {
929
+ if (await c.db.query("org").withIndex("by_slug", ((o) => o.eq("slug", data.slug))).unique()) return err("ORG_SLUG_TAKEN");
930
+ return { orgId: await c.db.insert("org", {
931
+ avatarId: data.avatarId ?? void 0,
932
+ name: data.name,
933
+ slug: data.slug,
934
+ userId: c.user._id,
935
+ ...time()
936
+ }) };
937
+ }
938
+ }), update = m({
939
+ args: {
940
+ data: orgSchema.partial(),
941
+ orgId: zid("org")
942
+ },
943
+ handler: async (c, { data, orgId }) => {
944
+ await requireOrgRole({
945
+ db: c.db,
946
+ minRole: "admin",
947
+ orgId,
948
+ userId: c.user._id
949
+ });
950
+ const newSlug = data.slug;
951
+ if (newSlug !== void 0) {
952
+ const existing = await c.db.query("org").withIndex("by_slug", ((o) => o.eq("slug", newSlug))).unique();
953
+ if (existing && existing._id !== orgId) return err("ORG_SLUG_TAKEN");
954
+ }
955
+ const patchData = {};
956
+ if (data.name !== void 0) patchData.name = data.name;
957
+ if (newSlug !== void 0) patchData.slug = newSlug;
958
+ if (data.avatarId !== void 0 && data.avatarId !== null) patchData.avatarId = data.avatarId;
959
+ await c.db.patch(orgId, {
960
+ ...patchData,
961
+ ...time()
962
+ });
963
+ }
964
+ }), get = q({
965
+ args: { orgId: zid("org") },
966
+ handler: async (c, { orgId }) => {
967
+ await requireOrgMember({
968
+ db: c.db,
969
+ orgId,
970
+ userId: c.user._id
971
+ });
972
+ return c.db.get(orgId);
973
+ }
974
+ }), getBySlug = pq({
975
+ args: { slug: z.string() },
976
+ handler: async (c, { slug }) => c.db.query("org").withIndex("by_slug", ((o) => o.eq("slug", slug))).unique()
977
+ }), getPublic = pq({
978
+ args: { slug: z.string() },
979
+ handler: async (c, { slug }) => {
980
+ const orgDoc = await c.db.query("org").withIndex("by_slug", ((o) => o.eq("slug", slug))).unique();
981
+ if (!orgDoc) return null;
982
+ return {
983
+ _id: orgDoc._id,
984
+ avatarId: orgDoc.avatarId,
985
+ name: orgDoc.name,
986
+ slug: orgDoc.slug
987
+ };
988
+ }
989
+ }), myOrgs = q({
990
+ args: {},
991
+ handler: async (c) => {
992
+ const uid = c.user._id, db = c.db, ownedOrgs = await db.query("org").withIndex("by_user", ((o) => o.eq("userId", uid))).collect(), memberships = await db.query("orgMember").withIndex("by_user", ((o) => o.eq("userId", uid))).collect(), memberOrgIds = memberships.map((x) => x.orgId), memberOrgResults = await Promise.all(memberOrgIds.map(async (id) => db.get(id))), memberOrgs = [];
993
+ for (const orgDoc of memberOrgResults) if (orgDoc) memberOrgs.push(orgDoc);
994
+ const ownedIds = new Set(ownedOrgs.map((o) => o._id)), result = [];
995
+ for (const o of ownedOrgs) result.push({
996
+ org: o,
997
+ role: "owner"
998
+ });
999
+ for (const o of memberOrgs) if (!ownedIds.has(o._id)) {
1000
+ const role = memberships.find((x) => x.orgId === o._id)?.isAdmin ? "admin" : "member";
1001
+ result.push({
1002
+ org: o,
1003
+ role
1004
+ });
1005
+ }
1006
+ return result;
1007
+ }
1008
+ }), remove = m({
1009
+ args: { orgId: zid("org") },
1010
+ handler: async (c, { orgId }) => {
1011
+ const db = c.db, orgDoc = await db.get(orgId);
1012
+ if (!orgDoc) return err("NOT_FOUND");
1013
+ if (orgDoc.userId !== c.user._id) return err("FORBIDDEN");
1014
+ if (cascadeTables) for (const table of cascadeTables) {
1015
+ const docs = await db.query(table).filter((o) => o.eq(o.field("orgId"), orgId)).collect();
1016
+ await Promise.all(docs.map(async (d) => db.delete(d._id)));
1017
+ }
1018
+ const joinRequests = await db.query("orgJoinRequest").withIndex("by_org", ((o) => o.eq("orgId", orgId))).collect();
1019
+ await Promise.all(joinRequests.map(async (r) => db.delete(r._id)));
1020
+ const invites = await db.query("orgInvite").withIndex("by_org", ((o) => o.eq("orgId", orgId))).collect();
1021
+ await Promise.all(invites.map(async (i) => db.delete(i._id)));
1022
+ const orgMembers = await db.query("orgMember").withIndex("by_org", ((o) => o.eq("orgId", orgId))).collect();
1023
+ await Promise.all(orgMembers.map(async (x) => db.delete(x._id)));
1024
+ await db.delete(orgId);
1025
+ }
1026
+ }), isSlugAvailable = pq({
1027
+ args: { slug: z.string() },
1028
+ handler: async (c, { slug }) => {
1029
+ return { available: !await c.db.query("org").withIndex("by_slug", ((o) => o.eq("slug", slug))).unique() };
1030
+ }
1031
+ }), membership = q({
1032
+ args: { orgId: zid("org") },
1033
+ handler: async (c, { orgId }) => {
1034
+ const db = c.db, orgDoc = await db.get(orgId);
1035
+ if (!orgDoc) return err("NOT_FOUND");
1036
+ const userId = c.user._id, member = await getOrgMember({
1037
+ db,
1038
+ orgId,
1039
+ userId
1040
+ }), role = getOrgRole({
1041
+ member,
1042
+ org: orgDoc,
1043
+ userId
1044
+ });
1045
+ if (!role) return null;
1046
+ return {
1047
+ memberId: member?._id ?? null,
1048
+ role
1049
+ };
1050
+ }
1051
+ }), members = q({
1052
+ args: { orgId: zid("org") },
1053
+ handler: async (c, { orgId }) => {
1054
+ const db = c.db, userId = c.user._id;
1055
+ await requireOrgMember({
1056
+ db,
1057
+ orgId,
1058
+ userId
1059
+ });
1060
+ const orgDoc = await db.get(orgId);
1061
+ if (!orgDoc) return err("NOT_FOUND");
1062
+ const result = [], ownerUser = await db.get(orgDoc.userId);
1063
+ result.push({
1064
+ role: "owner",
1065
+ user: ownerUser,
1066
+ userId: orgDoc.userId
1067
+ });
1068
+ const memberDocs = await db.query("orgMember").withIndex("by_org", ((o) => o.eq("orgId", orgId))).collect(), userDocs = await Promise.all(memberDocs.map(async (x) => db.get(x.userId)));
1069
+ for (let i = 0; i < memberDocs.length; i += 1) {
1070
+ const memberDoc = memberDocs[i], userDoc = userDocs[i];
1071
+ if (memberDoc) result.push({
1072
+ memberId: memberDoc._id,
1073
+ role: memberDoc.isAdmin ? "admin" : "member",
1074
+ user: userDoc ?? null,
1075
+ userId: memberDoc.userId
1076
+ });
1077
+ }
1078
+ return result;
1079
+ }
1080
+ }), setAdmin = m({
1081
+ args: {
1082
+ isAdmin: z.boolean(),
1083
+ memberId: zid("orgMember")
1084
+ },
1085
+ handler: async (c, { isAdmin, memberId }) => {
1086
+ const db = c.db, memberDoc = await db.get(memberId);
1087
+ if (!memberDoc) return err("NOT_FOUND");
1088
+ const orgDoc = await db.get(memberDoc.orgId);
1089
+ if (!orgDoc) return err("NOT_FOUND");
1090
+ if (orgDoc.userId !== c.user._id) return err("FORBIDDEN");
1091
+ if (memberDoc.userId === orgDoc.userId) return err("CANNOT_MODIFY_OWNER");
1092
+ await db.patch(memberId, {
1093
+ isAdmin,
1094
+ ...time()
1095
+ });
1096
+ }
1097
+ }), removeMember = m({
1098
+ args: { memberId: zid("orgMember") },
1099
+ handler: async (c, { memberId }) => {
1100
+ const db = c.db, memberDoc = await db.get(memberId);
1101
+ if (!memberDoc) return err("NOT_FOUND");
1102
+ const orgDoc = await db.get(memberDoc.orgId);
1103
+ if (!orgDoc) return err("NOT_FOUND");
1104
+ if (memberDoc.userId === orgDoc.userId) return err("CANNOT_MODIFY_OWNER");
1105
+ const { role } = await requireOrgRole({
1106
+ db,
1107
+ minRole: "admin",
1108
+ orgId: memberDoc.orgId,
1109
+ userId: c.user._id
1110
+ });
1111
+ if (role === "admin" && memberDoc.isAdmin) return err("CANNOT_MODIFY_ADMIN");
1112
+ await db.delete(memberId);
1113
+ }
1114
+ }), leave = m({
1115
+ args: { orgId: zid("org") },
1116
+ handler: async (c, { orgId }) => {
1117
+ const db = c.db, orgDoc = await db.get(orgId);
1118
+ if (!orgDoc) return err("NOT_FOUND");
1119
+ const userId = c.user._id;
1120
+ if (orgDoc.userId === userId) return err("MUST_TRANSFER_OWNERSHIP");
1121
+ const member = await getOrgMember({
1122
+ db,
1123
+ orgId,
1124
+ userId
1125
+ });
1126
+ if (!member) return err("NOT_ORG_MEMBER");
1127
+ await db.delete(member._id);
1128
+ }
1129
+ }), transferOwnership = m({
1130
+ args: {
1131
+ newOwnerId: zid("users"),
1132
+ orgId: zid("org")
1133
+ },
1134
+ handler: async (c, { newOwnerId, orgId }) => {
1135
+ const db = c.db, orgDoc = await db.get(orgId);
1136
+ if (!orgDoc) return err("NOT_FOUND");
1137
+ if (orgDoc.userId !== c.user._id) return err("FORBIDDEN");
1138
+ const targetMember = await getOrgMember({
1139
+ db,
1140
+ orgId,
1141
+ userId: newOwnerId
1142
+ });
1143
+ if (!targetMember) return err("NOT_ORG_MEMBER");
1144
+ if (!targetMember.isAdmin) return err("TARGET_MUST_BE_ADMIN");
1145
+ await db.patch(orgId, {
1146
+ userId: newOwnerId,
1147
+ ...time()
1148
+ });
1149
+ await db.delete(targetMember._id);
1150
+ await db.insert("orgMember", {
1151
+ isAdmin: true,
1152
+ orgId,
1153
+ userId: c.user._id,
1154
+ ...time()
1155
+ });
1156
+ }
1157
+ }), invite = m({
1158
+ args: {
1159
+ email: z.email(),
1160
+ isAdmin: z.boolean(),
1161
+ orgId: zid("org")
1162
+ },
1163
+ handler: async (c, { email, isAdmin, orgId }) => {
1164
+ await requireOrgRole({
1165
+ db: c.db,
1166
+ minRole: "admin",
1167
+ orgId,
1168
+ userId: c.user._id
1169
+ });
1170
+ const token = generateToken(), expiresAt = Date.now() + SEVEN_DAYS_MS;
1171
+ return {
1172
+ inviteId: await c.db.insert("orgInvite", {
1173
+ email,
1174
+ expiresAt,
1175
+ isAdmin,
1176
+ orgId,
1177
+ token
1178
+ }),
1179
+ token
1180
+ };
1181
+ }
1182
+ }), acceptInvite = m({
1183
+ args: { token: z.string() },
1184
+ handler: async (c, { token }) => {
1185
+ const db = c.db, userId = c.user._id, inviteDoc = await db.query("orgInvite").withIndex("by_token", ((o) => o.eq("token", token))).unique();
1186
+ if (!inviteDoc) return err("INVALID_INVITE");
1187
+ if (inviteDoc.expiresAt < Date.now()) return err("INVITE_EXPIRED");
1188
+ const existingMember = await getOrgMember({
1189
+ db,
1190
+ orgId: inviteDoc.orgId,
1191
+ userId
1192
+ }), orgDoc = await db.get(inviteDoc.orgId);
1193
+ if (!orgDoc) return err("NOT_FOUND");
1194
+ if (existingMember || orgDoc.userId === userId) return err("ALREADY_ORG_MEMBER");
1195
+ const pendingRequest = await db.query("orgJoinRequest").withIndex("by_org_status", ((o) => o.eq("orgId", inviteDoc.orgId).eq("status", "pending"))).filter((o) => o.eq(o.field("userId"), userId)).unique();
1196
+ if (pendingRequest) await db.patch(pendingRequest._id, {
1197
+ status: "approved",
1198
+ ...time()
1199
+ });
1200
+ await db.insert("orgMember", {
1201
+ isAdmin: inviteDoc.isAdmin,
1202
+ orgId: inviteDoc.orgId,
1203
+ userId,
1204
+ ...time()
1205
+ });
1206
+ await db.delete(inviteDoc._id);
1207
+ return { orgId: inviteDoc.orgId };
1208
+ }
1209
+ }), revokeInvite = m({
1210
+ args: { inviteId: zid("orgInvite") },
1211
+ handler: async (c, { inviteId }) => {
1212
+ const db = c.db, inviteDoc = await db.get(inviteId);
1213
+ if (!inviteDoc) return err("NOT_FOUND");
1214
+ await requireOrgRole({
1215
+ db,
1216
+ minRole: "admin",
1217
+ orgId: inviteDoc.orgId,
1218
+ userId: c.user._id
1219
+ });
1220
+ await db.delete(inviteId);
1221
+ }
1222
+ }), pendingInvites = q({
1223
+ args: { orgId: zid("org") },
1224
+ handler: async (c, { orgId }) => {
1225
+ await requireOrgRole({
1226
+ db: c.db,
1227
+ minRole: "admin",
1228
+ orgId,
1229
+ userId: c.user._id
1230
+ });
1231
+ return c.db.query("orgInvite").withIndex("by_org", ((o) => o.eq("orgId", orgId))).collect();
1232
+ }
1233
+ }), requestJoin = m({
1234
+ args: {
1235
+ message: z.string().optional(),
1236
+ orgId: zid("org")
1237
+ },
1238
+ handler: async (c, { message, orgId }) => {
1239
+ const db = c.db, userId = c.user._id, orgDoc = await db.get(orgId);
1240
+ if (!orgDoc) return err("NOT_FOUND");
1241
+ if (await getOrgMember({
1242
+ db,
1243
+ orgId,
1244
+ userId
1245
+ }) || orgDoc.userId === userId) return err("ALREADY_ORG_MEMBER");
1246
+ if (await db.query("orgJoinRequest").withIndex("by_org_status", ((o) => o.eq("orgId", orgId).eq("status", "pending"))).filter((o) => o.eq(o.field("userId"), userId)).unique()) return err("JOIN_REQUEST_EXISTS");
1247
+ return { requestId: await db.insert("orgJoinRequest", {
1248
+ message,
1249
+ orgId,
1250
+ status: "pending",
1251
+ userId
1252
+ }) };
1253
+ }
1254
+ }), approveJoinRequest = m({
1255
+ args: {
1256
+ isAdmin: z.boolean().optional(),
1257
+ requestId: zid("orgJoinRequest")
1258
+ },
1259
+ handler: async (c, { isAdmin, requestId }) => {
1260
+ const db = c.db, requestDoc = await db.get(requestId);
1261
+ if (!requestDoc) return err("NOT_FOUND");
1262
+ await requireOrgRole({
1263
+ db,
1264
+ minRole: "admin",
1265
+ orgId: requestDoc.orgId,
1266
+ userId: c.user._id
1267
+ });
1268
+ await db.insert("orgMember", {
1269
+ isAdmin: isAdmin ?? false,
1270
+ orgId: requestDoc.orgId,
1271
+ userId: requestDoc.userId,
1272
+ ...time()
1273
+ });
1274
+ await db.patch(requestId, { status: "approved" });
1275
+ }
1276
+ }), rejectJoinRequest = m({
1277
+ args: { requestId: zid("orgJoinRequest") },
1278
+ handler: async (c, { requestId }) => {
1279
+ const db = c.db, requestDoc = await db.get(requestId);
1280
+ if (!requestDoc) return err("NOT_FOUND");
1281
+ await requireOrgRole({
1282
+ db,
1283
+ minRole: "admin",
1284
+ orgId: requestDoc.orgId,
1285
+ userId: c.user._id
1286
+ });
1287
+ await db.patch(requestId, { status: "rejected" });
1288
+ }
1289
+ }), cancelJoinRequest = m({
1290
+ args: { requestId: zid("orgJoinRequest") },
1291
+ handler: async (c, { requestId }) => {
1292
+ const db = c.db, requestDoc = await db.get(requestId);
1293
+ if (!requestDoc) return err("NOT_FOUND");
1294
+ if (requestDoc.userId !== c.user._id) return err("FORBIDDEN");
1295
+ if (requestDoc.status !== "pending") return err("NOT_FOUND");
1296
+ await db.delete(requestId);
1297
+ }
1298
+ }), pendingJoinRequests = q({
1299
+ args: { orgId: zid("org") },
1300
+ handler: async (c, { orgId }) => {
1301
+ const db = c.db;
1302
+ await requireOrgRole({
1303
+ db,
1304
+ minRole: "admin",
1305
+ orgId,
1306
+ userId: c.user._id
1307
+ });
1308
+ const requests = await db.query("orgJoinRequest").withIndex("by_org_status", ((o) => o.eq("orgId", orgId).eq("status", "pending"))).collect(), users = await Promise.all(requests.map(async (r) => db.get(r.userId))), result = [];
1309
+ for (let i = 0; i < requests.length; i += 1) {
1310
+ const req = requests[i], usr = users[i];
1311
+ if (req) result.push({
1312
+ request: req,
1313
+ user: usr ?? null
1314
+ });
1315
+ }
1316
+ return result;
1317
+ }
1318
+ });
1319
+ return {
1320
+ acceptInvite,
1321
+ approveJoinRequest,
1322
+ cancelJoinRequest,
1323
+ create,
1324
+ get,
1325
+ getBySlug,
1326
+ getPublic,
1327
+ invite,
1328
+ isSlugAvailable,
1329
+ leave,
1330
+ members,
1331
+ membership,
1332
+ myJoinRequest: q({
1333
+ args: { orgId: zid("org") },
1334
+ handler: async (c, { orgId }) => c.db.query("orgJoinRequest").withIndex("by_org_status", ((o) => o.eq("orgId", orgId).eq("status", "pending"))).filter((o) => o.eq(o.field("userId"), c.user._id)).unique()
1335
+ }),
1336
+ myOrgs,
1337
+ pendingInvites,
1338
+ pendingJoinRequests,
1339
+ rejectJoinRequest,
1340
+ remove,
1341
+ removeMember,
1342
+ requestJoin,
1343
+ revokeInvite,
1344
+ setAdmin,
1345
+ transferOwnership,
1346
+ update
1347
+ };
1348
+ };
1349
+
1350
+ //#endregion
1351
+ //#region src/server/db.ts
1352
+ const dbInsert = async (db, table, data) => db.insert(table, data), dbPatch = async (db, id, data) => db.patch(id, data), dbDelete = async (db, id) => db.delete(id);
1353
+
1354
+ //#endregion
1355
+ //#region src/server/org-crud.ts
1356
+ const getEditors = (doc) => doc.editors ?? [], requireOrgDoc = (doc, orgId, debug) => {
1357
+ if (doc?.orgId !== orgId) return err("NOT_FOUND", debug);
1358
+ return doc;
1359
+ }, resolveAclDoc = async (db, doc, opt) => {
1360
+ if (opt?.aclFrom) {
1361
+ const parentId = doc[opt.aclFrom.field], parent = parentId ? await db.get(parentId) : null;
1362
+ return {
1363
+ editors: parent ? getEditors(parent) : [],
1364
+ userId: doc.userId
1365
+ };
1366
+ }
1367
+ return doc;
1368
+ }, makeOrgCrud = ({ builders, options: opt, schema, table }) => {
1369
+ const { m, q } = builders, partial = schema.partial(), bulkIdsSchema = array(zid(table)).max(100), fileFs = detectFiles(schema.shape), idArgs = { id: zid(table) }, orgIdArg = { orgId: zid("org") }, useAcl = Boolean(opt?.acl) || Boolean(opt?.aclFrom), enrich = async (c, docs) => Promise.all((await c.withAuthor(docs)).map(async (d) => addUrls({
1370
+ doc: d,
1371
+ fileFields: fileFs,
1372
+ storage: c.storage
1373
+ }))), cascadeDelete = async (db, id) => {
1374
+ if (!opt?.cascade) return;
1375
+ const { foreignKey, table: tbl } = opt.cascade, kids = await db.query(tbl).filter((f) => f.eq(f.field(foreignKey), id)).collect();
1376
+ for (const kid of kids) await dbDelete(db, kid._id);
1377
+ }, create = m({
1378
+ args: {
1379
+ ...orgIdArg,
1380
+ ...schema.shape
1381
+ },
1382
+ handler: (async (c, a) => {
1383
+ const { orgId, ...data } = a;
1384
+ await requireOrgMember({
1385
+ db: c.db,
1386
+ orgId,
1387
+ userId: c.user._id
1388
+ });
1389
+ return dbInsert(c.db, table, {
1390
+ ...data,
1391
+ orgId,
1392
+ userId: c.user._id,
1393
+ ...time()
1394
+ });
1395
+ })
1396
+ }), list = q({
1397
+ args: {
1398
+ ...orgIdArg,
1399
+ paginationOpts: pgOpts
1400
+ },
1401
+ handler: (async (c, { orgId, paginationOpts }) => {
1402
+ await requireOrgMember({
1403
+ db: c.db,
1404
+ orgId,
1405
+ userId: c.user._id
1406
+ });
1407
+ const { page, ...rest } = await c.db.query(table).withIndex("by_org", ((o) => o.eq("orgId", orgId))).order("desc").paginate(paginationOpts);
1408
+ return {
1409
+ ...rest,
1410
+ page: await enrich(c, page)
1411
+ };
1412
+ })
1413
+ }), all = q({
1414
+ args: { ...orgIdArg },
1415
+ handler: (async (c, { orgId }) => {
1416
+ await requireOrgMember({
1417
+ db: c.db,
1418
+ orgId,
1419
+ userId: c.user._id
1420
+ });
1421
+ return enrich(c, await c.db.query(table).withIndex("by_org", ((o) => o.eq("orgId", orgId))).order("desc").collect());
1422
+ })
1423
+ }), count = q({
1424
+ args: { ...orgIdArg },
1425
+ handler: (async (c, { orgId }) => {
1426
+ await requireOrgMember({
1427
+ db: c.db,
1428
+ orgId,
1429
+ userId: c.user._id
1430
+ });
1431
+ const docs = await c.db.query(table).withIndex("by_org", ((o) => o.eq("orgId", orgId))).collect();
1432
+ if (docs.length > 1e3) log("warn", "crud:large_count", {
1433
+ count: docs.length,
1434
+ table,
1435
+ tip: "Use indexed queries for large tables"
1436
+ });
1437
+ return docs.length;
1438
+ })
1439
+ }), read = q({
1440
+ args: {
1441
+ ...orgIdArg,
1442
+ ...idArgs
1443
+ },
1444
+ handler: (async (c, { id, orgId }) => {
1445
+ await requireOrgMember({
1446
+ db: c.db,
1447
+ orgId,
1448
+ userId: c.user._id
1449
+ });
1450
+ return (await enrich(c, [requireOrgDoc(await c.db.get(id), orgId)]))[0];
1451
+ })
1452
+ }), update = m({
1453
+ args: {
1454
+ ...orgIdArg,
1455
+ ...idArgs,
1456
+ ...partial.shape,
1457
+ expectedUpdatedAt: number().optional()
1458
+ },
1459
+ handler: (async (c, a) => {
1460
+ const { expectedUpdatedAt, id, orgId, ...patch } = a, { role } = await requireOrgMember({
1461
+ db: c.db,
1462
+ orgId,
1463
+ userId: c.user._id
1464
+ }), doc = requireOrgDoc(await c.db.get(id), orgId);
1465
+ if (!canEdit({
1466
+ acl: useAcl,
1467
+ doc: await resolveAclDoc(c.db, doc, opt),
1468
+ role,
1469
+ userId: c.user._id
1470
+ })) return err("FORBIDDEN", `${table}:update`);
1471
+ if (expectedUpdatedAt !== void 0 && doc.updatedAt !== expectedUpdatedAt) return err("CONFLICT", `${table}:update`);
1472
+ await dbPatch(c.db, id, {
1473
+ ...patch,
1474
+ ...time()
1475
+ });
1476
+ return c.db.get(id);
1477
+ })
1478
+ }), rm = m({
1479
+ args: {
1480
+ ...orgIdArg,
1481
+ ...idArgs
1482
+ },
1483
+ handler: (async (c, { id, orgId }) => {
1484
+ const { role } = await requireOrgMember({
1485
+ db: c.db,
1486
+ orgId,
1487
+ userId: c.user._id
1488
+ }), doc = requireOrgDoc(await c.db.get(id), orgId);
1489
+ if (!canEdit({
1490
+ acl: useAcl,
1491
+ doc: await resolveAclDoc(c.db, doc, opt),
1492
+ role,
1493
+ userId: c.user._id
1494
+ })) return err("FORBIDDEN", `${table}:rm`);
1495
+ await cascadeDelete(c.db, id);
1496
+ await dbDelete(c.db, id);
1497
+ await cleanFiles({
1498
+ doc,
1499
+ fileFields: fileFs,
1500
+ storage: c.storage
1501
+ });
1502
+ return doc;
1503
+ })
1504
+ }), bulkUpdate = m({
1505
+ args: {
1506
+ ...orgIdArg,
1507
+ data: partial,
1508
+ ids: bulkIdsSchema
1509
+ },
1510
+ handler: (async (c, a) => {
1511
+ const { data, ids, orgId } = a;
1512
+ if (ids.length > 100) return err("LIMIT_EXCEEDED", `${table}:bulkUpdate`);
1513
+ await requireOrgRole({
1514
+ db: c.db,
1515
+ minRole: "admin",
1516
+ orgId,
1517
+ userId: c.user._id
1518
+ });
1519
+ const results = [];
1520
+ for (const id of ids) {
1521
+ if ((await c.db.get(id))?.orgId !== orgId) continue;
1522
+ await dbPatch(c.db, id, {
1523
+ ...data,
1524
+ ...time()
1525
+ });
1526
+ const updated = await c.db.get(id);
1527
+ if (updated) results.push(updated);
1528
+ }
1529
+ return results;
1530
+ })
1531
+ }), base = {
1532
+ all,
1533
+ bulkRm: m({
1534
+ args: {
1535
+ ...orgIdArg,
1536
+ ids: bulkIdsSchema
1537
+ },
1538
+ handler: (async (c, a) => {
1539
+ const { ids, orgId } = a;
1540
+ if (ids.length > 100) return err("LIMIT_EXCEEDED", `${table}:bulkRm`);
1541
+ await requireOrgRole({
1542
+ db: c.db,
1543
+ minRole: "admin",
1544
+ orgId,
1545
+ userId: c.user._id
1546
+ });
1547
+ let deleted = 0;
1548
+ for (const id of ids) {
1549
+ const doc = await c.db.get(id);
1550
+ if (doc?.orgId !== orgId) continue;
1551
+ await cascadeDelete(c.db, id);
1552
+ await dbDelete(c.db, id);
1553
+ await cleanFiles({
1554
+ doc,
1555
+ fileFields: fileFs,
1556
+ storage: c.storage
1557
+ });
1558
+ deleted += 1;
1559
+ }
1560
+ return deleted;
1561
+ })
1562
+ }),
1563
+ bulkUpdate,
1564
+ count,
1565
+ create,
1566
+ list,
1567
+ read,
1568
+ rm,
1569
+ update
1570
+ }, itemIdKey = `${table}Id`, itemIdArg = { [itemIdKey]: zid(table) }, aclArgs = (a) => {
1571
+ const args = a;
1572
+ return {
1573
+ editorId: args.editorId,
1574
+ editorIds: args.editorIds,
1575
+ itemId: args[itemIdKey],
1576
+ orgId: args.orgId
1577
+ };
1578
+ }, addEditor = m({
1579
+ args: {
1580
+ editorId: zid("users"),
1581
+ ...orgIdArg,
1582
+ ...itemIdArg
1583
+ },
1584
+ handler: (async (c, a) => {
1585
+ const { editorId, itemId, orgId } = aclArgs(a);
1586
+ await requireOrgRole({
1587
+ db: c.db,
1588
+ minRole: "admin",
1589
+ orgId,
1590
+ userId: c.user._id
1591
+ });
1592
+ const doc = requireOrgDoc(await c.db.get(itemId), orgId), editorIsOwner = (await c.db.get(orgId))?.userId === editorId, editorMember = await getOrgMember({
1593
+ db: c.db,
1594
+ orgId,
1595
+ userId: editorId
1596
+ });
1597
+ if (!(editorIsOwner || editorMember)) return err("NOT_ORG_MEMBER");
1598
+ const eds = getEditors(doc);
1599
+ if (eds.some((eid) => eid === editorId)) return doc;
1600
+ if (eds.length >= 100) return err("LIMIT_EXCEEDED");
1601
+ await dbPatch(c.db, itemId, {
1602
+ editors: [...eds, editorId],
1603
+ ...time()
1604
+ });
1605
+ return c.db.get(itemId);
1606
+ })
1607
+ }), removeEditor = m({
1608
+ args: {
1609
+ editorId: zid("users"),
1610
+ ...orgIdArg,
1611
+ ...itemIdArg
1612
+ },
1613
+ handler: (async (c, a) => {
1614
+ const { editorId, itemId, orgId } = aclArgs(a);
1615
+ await requireOrgRole({
1616
+ db: c.db,
1617
+ minRole: "admin",
1618
+ orgId,
1619
+ userId: c.user._id
1620
+ });
1621
+ const filtered = getEditors(requireOrgDoc(await c.db.get(itemId), orgId)).filter((eid) => eid !== editorId);
1622
+ await dbPatch(c.db, itemId, {
1623
+ editors: filtered,
1624
+ ...time()
1625
+ });
1626
+ return c.db.get(itemId);
1627
+ })
1628
+ }), editors = q({
1629
+ args: {
1630
+ ...orgIdArg,
1631
+ ...itemIdArg
1632
+ },
1633
+ handler: (async (c, a) => {
1634
+ const { itemId, orgId } = aclArgs(a);
1635
+ await requireOrgMember({
1636
+ db: c.db,
1637
+ orgId,
1638
+ userId: c.user._id
1639
+ });
1640
+ const editorIds = getEditors(requireOrgDoc(await c.db.get(itemId), orgId)), users = await Promise.all(editorIds.map(async (eid) => c.db.get(eid))), result = [];
1641
+ for (let i = 0; i < editorIds.length; i += 1) {
1642
+ const u = users[i], eid = editorIds[i];
1643
+ if (u && eid) result.push({
1644
+ email: u.email ?? "",
1645
+ name: u.name ?? "",
1646
+ userId: eid
1647
+ });
1648
+ }
1649
+ return result;
1650
+ })
1651
+ }), setEditors = m({
1652
+ args: {
1653
+ editorIds: array(zid("users")).max(100),
1654
+ ...orgIdArg,
1655
+ ...itemIdArg
1656
+ },
1657
+ handler: (async (c, a) => {
1658
+ const { editorIds, itemId, orgId } = aclArgs(a);
1659
+ await requireOrgRole({
1660
+ db: c.db,
1661
+ minRole: "admin",
1662
+ orgId,
1663
+ userId: c.user._id
1664
+ });
1665
+ requireOrgDoc(await c.db.get(itemId), orgId);
1666
+ if (editorIds) for (const editorId of editorIds) {
1667
+ const isOwner = (await c.db.get(orgId))?.userId === editorId, member = await getOrgMember({
1668
+ db: c.db,
1669
+ orgId,
1670
+ userId: editorId
1671
+ });
1672
+ if (!(isOwner || member)) return err("NOT_ORG_MEMBER");
1673
+ }
1674
+ await dbPatch(c.db, itemId, {
1675
+ editors: editorIds ?? [],
1676
+ ...time()
1677
+ });
1678
+ return c.db.get(itemId);
1679
+ })
1680
+ });
1681
+ return {
1682
+ ...base,
1683
+ addEditor,
1684
+ editors,
1685
+ removeEditor,
1686
+ setEditors
1687
+ };
1688
+ }, orgCascade = (config) => config;
1689
+
1690
+ //#endregion
1691
+ //#region src/server/schema-helpers.ts
1692
+ const baseTable = (s) => defineTable({
1693
+ ...zodOutputToConvexFields(s.shape),
1694
+ updatedAt: v.optional(v.number())
1695
+ }), ownedTable = (s) => defineTable({
1696
+ ...zodOutputToConvexFields(s.shape),
1697
+ updatedAt: v.number(),
1698
+ userId: v.id("users")
1699
+ }).index("by_user", ["userId"]), orgTable = (s) => defineTable({
1700
+ ...zodOutputToConvexFields(s.shape),
1701
+ orgId: v.id("org"),
1702
+ updatedAt: v.number(),
1703
+ userId: v.id("users")
1704
+ }).index("by_org", ["orgId"]).index("by_org_user", ["orgId", "userId"]), orgChildTable = (s, parent) => defineTable({
1705
+ ...zodOutputToConvexFields(s.shape),
1706
+ orgId: v.id("org"),
1707
+ updatedAt: v.number(),
1708
+ userId: v.id("users")
1709
+ }).index("by_org", ["orgId"]).index("by_parent", [parent.foreignKey]), childTable = (s, indexField, indexName) => defineTable({
1710
+ ...zodOutputToConvexFields(s.shape),
1711
+ updatedAt: v.number()
1712
+ }).index(indexName ?? `by_${indexField}`, [indexField]), orgTables = () => ({
1713
+ org: defineTable({
1714
+ avatarId: v.optional(v.id("_storage")),
1715
+ name: v.string(),
1716
+ slug: v.string(),
1717
+ updatedAt: v.number(),
1718
+ userId: v.id("users")
1719
+ }).index("by_slug", ["slug"]).index("by_user", ["userId"]),
1720
+ orgInvite: defineTable({
1721
+ email: v.string(),
1722
+ expiresAt: v.number(),
1723
+ isAdmin: v.boolean(),
1724
+ orgId: v.id("org"),
1725
+ token: v.string()
1726
+ }).index("by_org", ["orgId"]).index("by_token", ["token"]),
1727
+ orgJoinRequest: defineTable({
1728
+ message: v.optional(v.string()),
1729
+ orgId: v.id("org"),
1730
+ status: v.union(v.literal("pending"), v.literal("approved"), v.literal("rejected")),
1731
+ userId: v.id("users")
1732
+ }).index("by_org", ["orgId"]).index("by_org_status", ["orgId", "status"]).index("by_user", ["userId"]),
1733
+ orgMember: defineTable({
1734
+ isAdmin: v.boolean(),
1735
+ orgId: v.id("org"),
1736
+ updatedAt: v.number(),
1737
+ userId: v.id("users")
1738
+ }).index("by_org", ["orgId"]).index("by_org_user", ["orgId", "userId"]).index("by_user", ["userId"])
1739
+ }), uploadTables = () => ({
1740
+ uploadChunk: defineTable({
1741
+ chunkIndex: v.number(),
1742
+ storageId: v.id("_storage"),
1743
+ totalChunks: v.number(),
1744
+ uploadId: v.string(),
1745
+ userId: v.id("users")
1746
+ }).index("by_upload", ["uploadId"]).index("by_user", ["userId"]),
1747
+ uploadRateLimit: defineTable({
1748
+ timestamp: v.number(),
1749
+ userId: v.id("users")
1750
+ }).index("by_user", ["userId"]),
1751
+ uploadSession: defineTable({
1752
+ completedChunks: v.number(),
1753
+ contentType: v.string(),
1754
+ fileName: v.string(),
1755
+ finalStorageId: v.optional(v.id("_storage")),
1756
+ status: v.union(v.literal("pending"), v.literal("assembling"), v.literal("completed"), v.literal("failed")),
1757
+ totalChunks: v.number(),
1758
+ totalSize: v.number(),
1759
+ uploadId: v.string(),
1760
+ userId: v.id("users")
1761
+ }).index("by_upload_id", ["uploadId"]).index("by_user", ["userId"])
1762
+ });
1763
+
1764
+ //#endregion
1765
+ //#region src/server/cache-crud.ts
1766
+ const makeCacheCrud = ({ builders: b, fetcher, key, schema, table, ttl = 10080 * 60 * 1e3 }) => {
1767
+ const keys = Object.keys(schema.shape), pick = (d) => pickFields(d, keys), valid = (d) => (d.updatedAt ?? d._creationTime) + ttl > Date.now(), partial = schema.partial(), idx = `by_${key}`, kArgs = zodOutputToConvexFields({ [key]: schema.shape[key] }), idArgs = { id: zid(table) }, expArgs = { includeExpired: boolean().optional() }, listArgs = {
1768
+ includeExpired: boolean().optional(),
1769
+ paginationOpts: pgOpts
1770
+ }, retFields = zodOutputToConvexFields(schema.extend({ cacheHit: boolean() }).shape), kVal = kArgs[key] ?? err("INVALID_WHERE"), byK = (x) => ((i) => i.eq(key, x)), getInt = b.internalQuery({
1771
+ args: kArgs,
1772
+ handler: (async (c, a) => c.db.query(table).withIndex(idx, byK(a[key])).first())
1773
+ }), get = b.query({
1774
+ args: kArgs,
1775
+ handler: (async (c, a) => {
1776
+ const d = await c.db.query(table).withIndex(idx, byK(a[key])).first();
1777
+ return d && valid(d) ? {
1778
+ ...d,
1779
+ cacheHit: true
1780
+ } : null;
1781
+ })
1782
+ }), read = b.cq({
1783
+ args: idArgs,
1784
+ handler: (async (c, { id }) => c.db.get(id))
1785
+ }), all = b.cq({
1786
+ args: expArgs,
1787
+ handler: (async (c, { includeExpired: ie }) => {
1788
+ const d = await c.db.query(table).order("desc").collect();
1789
+ return ie ? d : d.filter(valid);
1790
+ })
1791
+ }), list = b.cq({
1792
+ args: listArgs,
1793
+ handler: (async (c, { includeExpired: ie, paginationOpts: op }) => {
1794
+ const qr = c.db.query(table).order("desc");
1795
+ if (ie) return qr.paginate(op);
1796
+ const { page, ...rest } = await qr.paginate({
1797
+ ...op,
1798
+ numItems: op.numItems * 2
1799
+ });
1800
+ return {
1801
+ ...rest,
1802
+ page: page.filter(valid).slice(0, op.numItems)
1803
+ };
1804
+ })
1805
+ }), upsert = async (c, data) => {
1806
+ const ex = await c.db.query(table).withIndex(idx, byK(data[key])).first(), wt = {
1807
+ ...data,
1808
+ ...time()
1809
+ };
1810
+ if (ex) {
1811
+ await dbPatch(c.db, ex._id, wt);
1812
+ return ex._id;
1813
+ }
1814
+ return dbInsert(c.db, table, wt);
1815
+ }, set = b.internalMutation({
1816
+ args: { data: v.object(zodOutputToConvexFields(schema.shape)) },
1817
+ handler: (async (c, { data }) => {
1818
+ await upsert(c, pick(data));
1819
+ })
1820
+ }), create = b.cm({
1821
+ args: schema.shape,
1822
+ handler: (async (c, d) => upsert(c, d))
1823
+ }), update = b.cm({
1824
+ args: {
1825
+ ...idArgs,
1826
+ ...partial.shape
1827
+ },
1828
+ handler: (async (c, a) => {
1829
+ const { id, ...d } = a, ex = await c.db.get(id), t = time();
1830
+ if (!ex) return err("NOT_FOUND");
1831
+ await dbPatch(c.db, id, {
1832
+ ...d,
1833
+ ...t
1834
+ });
1835
+ return {
1836
+ ...ex,
1837
+ ...d,
1838
+ ...t
1839
+ };
1840
+ })
1841
+ }), rm = b.cm({
1842
+ args: idArgs,
1843
+ handler: (async (c, { id }) => {
1844
+ const d = await c.db.get(id);
1845
+ if (d) await c.db.delete(id);
1846
+ return d;
1847
+ })
1848
+ }), invalidate = b.mutation({
1849
+ args: kArgs,
1850
+ handler: (async (c, a) => {
1851
+ const d = await c.db.query(table).withIndex(idx, byK(a[key])).first();
1852
+ if (d) await dbDelete(c.db, d._id);
1853
+ return d;
1854
+ })
1855
+ }), purge = b.cm({
1856
+ args: {},
1857
+ handler: (async (c) => {
1858
+ const cut = Date.now() - ttl, exp = await c.db.query(table).filter((qr) => qr.lt(qr.field("_creationTime"), cut)).collect();
1859
+ for (const d of exp) await dbDelete(c.db, d._id);
1860
+ return exp.length;
1861
+ })
1862
+ }), tPath = anyApi[table], tKArgs = { [key]: kVal }, doFetch = async (c, kv) => {
1863
+ const d = pick(await fetcher?.(c, kv));
1864
+ await c.runMutation(tPath.set, { data: d });
1865
+ return {
1866
+ ...d,
1867
+ cacheHit: false
1868
+ };
1869
+ };
1870
+ return {
1871
+ all,
1872
+ create,
1873
+ get,
1874
+ getInternal: getInt,
1875
+ invalidate,
1876
+ list,
1877
+ load: fetcher ? b.action({
1878
+ args: tKArgs,
1879
+ handler: (async (c, a) => {
1880
+ const kv = a[key], d = await c.runQuery(tPath.getInternal, { [key]: kv });
1881
+ return d && valid(d) ? {
1882
+ ...pick(d),
1883
+ cacheHit: true
1884
+ } : doFetch(c, kv);
1885
+ }),
1886
+ returns: v.object(retFields)
1887
+ }) : b.action(noFetcher),
1888
+ purge,
1889
+ read,
1890
+ refresh: fetcher ? b.action({
1891
+ args: tKArgs,
1892
+ handler: (async (c, a) => {
1893
+ const kv = a[key];
1894
+ await c.runMutation(tPath.invalidate, { [key]: kv });
1895
+ return doFetch(c, kv);
1896
+ }),
1897
+ returns: v.object(retFields)
1898
+ }) : b.action(noFetcher),
1899
+ rm,
1900
+ set,
1901
+ update
1902
+ };
1903
+ };
1904
+
1905
+ //#endregion
1906
+ //#region src/server/child.ts
1907
+ const makeChildCrud = ({ builders, meta, table }) => {
1908
+ const { m, q } = builders, { foreignKey, index, parent, schema } = meta, getFK = (doc) => doc[foreignKey], schemaKeys = Object.keys(schema.shape), partial = schema.partial(), idArgs = { id: zid(table) }, verifyParentOwnership = async (ctx, parentId) => {
1909
+ const p = await ctx.db.get(parentId);
1910
+ return p && p.userId === ctx.user._id ? p : null;
1911
+ }, create = m({
1912
+ args: {
1913
+ ...schema.shape,
1914
+ [foreignKey]: zid(parent)
1915
+ },
1916
+ handler: (async (ctx, a) => {
1917
+ const args = a, parentId = args[foreignKey], data = schema.parse(pickFields(args, schemaKeys));
1918
+ if (!await verifyParentOwnership(ctx, parentId)) return err("NOT_FOUND", `${table}:create`);
1919
+ return dbInsert(ctx.db, table, {
1920
+ ...data,
1921
+ [foreignKey]: parentId,
1922
+ ...time()
1923
+ });
1924
+ })
1925
+ }), update = m({
1926
+ args: {
1927
+ ...idArgs,
1928
+ ...partial.shape
1929
+ },
1930
+ handler: (async (ctx, a) => {
1931
+ const { id, ...rest } = a, data = partial.parse(pickFields(rest, schemaKeys)), doc = await ctx.db.get(id);
1932
+ if (!doc) return err("NOT_FOUND", `${table}:update`);
1933
+ if (!await verifyParentOwnership(ctx, getFK(doc))) return err("NOT_FOUND", `${table}:update`);
1934
+ await dbPatch(ctx.db, id, {
1935
+ ...data,
1936
+ ...time()
1937
+ });
1938
+ return ctx.db.get(id);
1939
+ })
1940
+ }), rm = m({
1941
+ args: idArgs,
1942
+ handler: (async (ctx, { id }) => {
1943
+ const doc = await ctx.db.get(id);
1944
+ if (!doc) return err("NOT_FOUND", `${table}:rm`);
1945
+ if (!await verifyParentOwnership(ctx, getFK(doc))) return err("NOT_FOUND", `${table}:rm`);
1946
+ await dbDelete(ctx.db, id);
1947
+ return doc;
1948
+ })
1949
+ }), list = q({
1950
+ args: { [foreignKey]: zid(parent) },
1951
+ handler: (async (ctx, a) => {
1952
+ const parentId = a[foreignKey];
1953
+ if (!await verifyParentOwnership(ctx, parentId)) return err("NOT_AUTHORIZED", `${table}:list`);
1954
+ return ctx.db.query(table).withIndex(index, ((i) => i.eq(foreignKey, parentId))).order("asc").collect();
1955
+ })
1956
+ });
1957
+ return {
1958
+ create,
1959
+ get: q({
1960
+ args: idArgs,
1961
+ handler: (async (ctx, { id }) => {
1962
+ const doc = await ctx.db.get(id);
1963
+ if (!doc) return null;
1964
+ if (!await verifyParentOwnership(ctx, getFK(doc))) return err("NOT_AUTHORIZED", `${table}:get`);
1965
+ return doc;
1966
+ })
1967
+ }),
1968
+ list,
1969
+ rm,
1970
+ update
1971
+ };
1972
+ };
1973
+
1974
+ //#endregion
1975
+ //#region src/server/crud.ts
1976
+ const makeCrud = ({ builders, options: opt, schema, table }) => {
1977
+ const { m, pq, q } = builders, stringFields = [];
1978
+ for (const k in schema.shape) if (Object.hasOwn(schema.shape, k) && isStringType(unwrapZod(schema.shape[k]).type)) stringFields.push(k);
1979
+ const partial = schema.partial(), bulkIdsSchema = array(zid(table)).max(100), fileFs = detectFiles(schema.shape), wgSchema = partial.extend({ own: boolean().optional() }), wSchema = wgSchema.extend({ or: array(wgSchema).optional() }), wArgs = { where: wSchema.optional() }, ownArg = { own: boolean().optional() }, idArgs = { id: zid(table) }, parseW = (i, fb) => {
1980
+ if (i === void 0) return fb;
1981
+ const r = wSchema.safeParse(i);
1982
+ return r.success ? r.data : errValidation("INVALID_WHERE", r.error);
1983
+ }, defaults = {
1984
+ auth: parseW(opt?.auth?.where),
1985
+ pub: parseW(opt?.pub?.where)
1986
+ }, enrich = async (c, docs) => Promise.all((await c.withAuthor(docs)).map(async (d) => addUrls({
1987
+ doc: d,
1988
+ fileFields: fileFs,
1989
+ storage: c.storage
1990
+ }))), buildExpr = (fb, w, vid) => {
1991
+ let e = null;
1992
+ const and = (x) => {
1993
+ e = e ? fb.and(e, x) : x;
1994
+ };
1995
+ for (const k in w) {
1996
+ if (k === "own") continue;
1997
+ const fv = w[k];
1998
+ if (fv === void 0) continue;
1999
+ const field = fb.field(k);
2000
+ if (isComparisonOp(fv)) {
2001
+ if (fv.$gt !== void 0) and(fb.gt(field, fv.$gt));
2002
+ if (fv.$gte !== void 0) and(fb.gte(field, fv.$gte));
2003
+ if (fv.$lt !== void 0) and(fb.lt(field, fv.$lt));
2004
+ if (fv.$lte !== void 0) and(fb.lte(field, fv.$lte));
2005
+ if (fv.$between !== void 0) {
2006
+ and(fb.gte(field, fv.$between[0]));
2007
+ and(fb.lte(field, fv.$between[1]));
2008
+ }
2009
+ } else and(fb.eq(field, fv));
2010
+ }
2011
+ if (w.own) and(vid ? fb.eq(fb.field("userId"), vid) : fb.eq(true, false));
2012
+ return e;
2013
+ }, canUseOwnIndex = (w) => {
2014
+ if (!w || w.or?.length) return false;
2015
+ const gs = groupList(w);
2016
+ return gs.length === 1 && gs[0]?.own === true;
2017
+ }, startQ = (c, w) => canUseOwnIndex(w) && c.viewerId ? c.db.query(table).withIndex("by_user", ((o) => o.eq("userId", c.viewerId))) : c.db.query(table), applyW = (qr, w, vid) => {
2018
+ let qry = qr;
2019
+ if (opt?.softDelete) qry = qry.filter((fb) => fb.eq(fb.field("deletedAt"), null));
2020
+ const gs = groupList(w);
2021
+ if (!gs.length) return qry;
2022
+ return qry.filter((f) => {
2023
+ let e = null;
2024
+ for (const g of gs) {
2025
+ const ge = buildExpr(f, g, vid);
2026
+ if (ge) e = e ? f.or(e, ge) : ge;
2027
+ }
2028
+ return e ?? true;
2029
+ });
2030
+ }, allH = (fb) => async (c, { where }) => {
2031
+ const w = parseW(where, fb), docs = await applyW(startQ(c, w), w, c.viewerId).order("desc").collect();
2032
+ if (docs.length > 1e3) log("warn", "crud:large_result", {
2033
+ count: docs.length,
2034
+ table,
2035
+ tip: "Use pagination or indexed queries"
2036
+ });
2037
+ return enrich(c, docs);
2038
+ }, listH = (fb) => async (c, { paginationOpts: op, where }) => {
2039
+ const w = parseW(where, fb), { page, ...rest } = await applyW(startQ(c, w), w, c.viewerId).order("desc").paginate(op);
2040
+ return {
2041
+ ...rest,
2042
+ page: await enrich(c, page)
2043
+ };
2044
+ }, readH = (fb) => async (c, { id, own, where }) => {
2045
+ const doc = await c.db.get(id), w = parseW(where, fb);
2046
+ if (!doc) return null;
2047
+ if (!matchW(doc, w, c.viewerId)) return null;
2048
+ if (own) {
2049
+ if (!c.viewerId) return null;
2050
+ if (doc.userId !== c.viewerId) return null;
2051
+ }
2052
+ return (await enrich(c, [doc]))[0] ?? null;
2053
+ }, countH = (fb) => async (c, { where }) => {
2054
+ const w = parseW(where, fb), docs = await applyW(startQ(c, w), w, c.viewerId).collect();
2055
+ if (docs.length > 1e3) log("warn", "crud:large_count", {
2056
+ count: docs.length,
2057
+ table,
2058
+ tip: "Use indexed queries for large tables"
2059
+ });
2060
+ return docs.length;
2061
+ }, searchIndexed = async (c, qry, w) => {
2062
+ const filtered = (await c.db.query(table).withSearchIndex("search_field", ((sb) => sb.search("text", qry))).collect()).filter((d) => matchW(d, w, c.viewerId));
2063
+ if (opt?.softDelete) return enrich(c, filtered.filter((d) => !d.deletedAt));
2064
+ return enrich(c, filtered);
2065
+ }, searchH = (fb) => async (c, { fields: fs, query: qry, where }) => {
2066
+ const w = parseW(where, fb);
2067
+ if (opt?.search === "index") return searchIndexed(c, qry, w);
2068
+ const searchFs = fs?.length ? fs : stringFields, lower = qry.toLowerCase(), docs = await applyW(startQ(c, w), w, c.viewerId).order("desc").collect(), matches = docs.filter((d) => {
2069
+ const rec = d;
2070
+ return searchFs.some((f) => {
2071
+ const val = rec[f];
2072
+ if (typeof val !== "string") return false;
2073
+ return val.toLowerCase().includes(lower);
2074
+ });
2075
+ });
2076
+ if (docs.length > 1e3) log("warn", "crud:search_scan", {
2077
+ count: docs.length,
2078
+ table,
2079
+ tip: "Add search: \"index\" for large tables"
2080
+ });
2081
+ return enrich(c, matches);
2082
+ }, readApi = (wrap, fb) => ({
2083
+ all: wrap({
2084
+ args: wArgs,
2085
+ handler: allH(fb)
2086
+ }),
2087
+ count: wrap({
2088
+ args: wArgs,
2089
+ handler: countH(fb)
2090
+ }),
2091
+ list: wrap({
2092
+ args: {
2093
+ paginationOpts: pgOpts,
2094
+ ...wArgs
2095
+ },
2096
+ handler: listH(fb)
2097
+ }),
2098
+ read: wrap({
2099
+ args: {
2100
+ ...idArgs,
2101
+ ...ownArg,
2102
+ ...wArgs
2103
+ },
2104
+ handler: readH(fb)
2105
+ }),
2106
+ search: wrap({
2107
+ args: {
2108
+ fields: array(string()).optional(),
2109
+ query: string(),
2110
+ ...wArgs
2111
+ },
2112
+ handler: searchH(fb)
2113
+ })
2114
+ }), indexedH = (fb) => async (c, { index, key, value, where }) => {
2115
+ const w = parseW(where, fb);
2116
+ return enrich(c, await applyW(c.db.query(table).withIndex(index, ((i) => i.eq(key, value))), w, c.viewerId).order("desc").collect());
2117
+ }, rmHandler = async (c, { id }) => {
2118
+ if (opt?.cascade !== false) for (const { foreignKey: fk, table: tbl } of cascadeFor(table, builders.children)) for (const r of await c.db.query(tbl).filter((f) => f.eq(f.field(fk), id)).collect()) await dbDelete(c.db, r._id);
2119
+ if (opt?.softDelete) {
2120
+ const doc = await c.db.get(id);
2121
+ if (!doc) {
2122
+ log("warn", "crud:not_found", {
2123
+ id,
2124
+ table
2125
+ });
2126
+ return err("NOT_FOUND", `${table}:rm`);
2127
+ }
2128
+ await dbPatch(c.db, id, { deletedAt: Date.now() });
2129
+ log("info", "crud:delete", {
2130
+ id,
2131
+ soft: true,
2132
+ table
2133
+ });
2134
+ return doc;
2135
+ }
2136
+ const d = await c.delete(id);
2137
+ await cleanFiles({
2138
+ doc: d,
2139
+ fileFields: fileFs,
2140
+ storage: c.storage
2141
+ });
2142
+ log("info", "crud:delete", {
2143
+ id,
2144
+ table
2145
+ });
2146
+ return d;
2147
+ };
2148
+ return {
2149
+ auth: readApi(q, defaults.auth),
2150
+ authIndexed: q({
2151
+ args: {
2152
+ index: string(),
2153
+ key: string(),
2154
+ value: string(),
2155
+ ...wArgs
2156
+ },
2157
+ handler: indexedH(defaults.auth)
2158
+ }),
2159
+ bulkRm: m({
2160
+ args: { ids: bulkIdsSchema },
2161
+ handler: (async (c, { ids }) => {
2162
+ if (ids.length > 100) return err("LIMIT_EXCEEDED", `${table}:bulkRm`);
2163
+ let deleted = 0;
2164
+ for (const id of ids) {
2165
+ await rmHandler(c, { id });
2166
+ deleted += 1;
2167
+ }
2168
+ return deleted;
2169
+ })
2170
+ }),
2171
+ bulkUpdate: m({
2172
+ args: {
2173
+ data: partial,
2174
+ ids: bulkIdsSchema
2175
+ },
2176
+ handler: (async (c, args) => {
2177
+ const { data, ids } = args;
2178
+ if (ids.length > 100) return err("LIMIT_EXCEEDED", `${table}:bulkUpdate`);
2179
+ const results = [];
2180
+ for (const id of ids) {
2181
+ const prev = await c.get(id), ret = await c.patch(id, data);
2182
+ await cleanFiles({
2183
+ doc: prev,
2184
+ fileFields: fileFs,
2185
+ next: data,
2186
+ storage: c.storage
2187
+ });
2188
+ results.push(ret);
2189
+ }
2190
+ return results;
2191
+ })
2192
+ }),
2193
+ create: m({
2194
+ args: schema.shape,
2195
+ handler: (async (c, a) => {
2196
+ const id = await c.create(table, a);
2197
+ log("info", "crud:create", {
2198
+ table,
2199
+ userId: c.user._id
2200
+ });
2201
+ return id;
2202
+ })
2203
+ }),
2204
+ pub: readApi(pq, defaults.pub),
2205
+ pubIndexed: pq({
2206
+ args: {
2207
+ index: string(),
2208
+ key: string(),
2209
+ value: string(),
2210
+ ...wArgs
2211
+ },
2212
+ handler: indexedH(defaults.pub)
2213
+ }),
2214
+ restore: opt?.softDelete ? m({
2215
+ args: idArgs,
2216
+ handler: (async (c, { id }) => {
2217
+ const doc = await c.get(id);
2218
+ await dbPatch(c.db, id, { deletedAt: null });
2219
+ return {
2220
+ ...doc,
2221
+ deletedAt: null
2222
+ };
2223
+ })
2224
+ }) : void 0,
2225
+ rm: m({
2226
+ args: idArgs,
2227
+ handler: (async (c, { id }) => rmHandler(c, { id }))
2228
+ }),
2229
+ update: m({
2230
+ args: {
2231
+ ...idArgs,
2232
+ ...partial.shape,
2233
+ expectedUpdatedAt: number().optional()
2234
+ },
2235
+ handler: (async (c, a) => {
2236
+ const { expectedUpdatedAt, id, ...rest } = a, patch = rest, prev = await c.get(id), ret = await c.patch(id, patch, expectedUpdatedAt);
2237
+ await cleanFiles({
2238
+ doc: prev,
2239
+ fileFields: fileFs,
2240
+ next: patch,
2241
+ storage: c.storage
2242
+ });
2243
+ log("info", "crud:update", {
2244
+ id,
2245
+ table
2246
+ });
2247
+ return ret;
2248
+ })
2249
+ })
2250
+ };
2251
+ };
2252
+
2253
+ //#endregion
2254
+ //#region src/server/unique.ts
2255
+ const makeUnique = ({ field, pq, table }) => pq({
2256
+ args: {
2257
+ exclude: zid(table).optional(),
2258
+ value: string()
2259
+ },
2260
+ handler: (async (c, { exclude, value }) => {
2261
+ const existing = await c.db.query(table).filter((f) => f.eq(f.field(field), value)).first();
2262
+ return !existing || existing._id === exclude;
2263
+ })
2264
+ });
2265
+
2266
+ //#endregion
2267
+ //#region src/server/setup.ts
2268
+ const setup = (config) => {
2269
+ const { getAuthUserId } = config, authId = async (c) => getAuthUserId(c), asDb = (c) => c.db, pq = zCustomQuery(config.query, customCtx(async (c) => {
2270
+ const vid = await authId(c), { withAuthor } = readCtx({
2271
+ db: asDb(c),
2272
+ storage: c.storage,
2273
+ viewerId: vid
2274
+ });
2275
+ return {
2276
+ viewerId: vid,
2277
+ withAuthor
2278
+ };
2279
+ })), q = zCustomQuery(config.query, customCtx(async (c) => {
2280
+ const db = asDb(c), user = await getUser({
2281
+ ctx: c,
2282
+ db,
2283
+ getAuthUserId
2284
+ }), { viewerId, withAuthor } = readCtx({
2285
+ db,
2286
+ storage: c.storage,
2287
+ viewerId: user._id
2288
+ });
2289
+ return {
2290
+ get: ownGet(db, user._id),
2291
+ user,
2292
+ viewerId,
2293
+ withAuthor
2294
+ };
2295
+ })), m = zCustomMutation(config.mutation, customCtx(async (c) => {
2296
+ const db = asDb(c), now = time(), user = await getUser({
2297
+ ctx: c,
2298
+ db,
2299
+ getAuthUserId
2300
+ }), get = ownGet(db, user._id);
2301
+ return {
2302
+ create: async (t, d) => dbInsert(db, t, {
2303
+ ...d,
2304
+ ...now,
2305
+ userId: user._id
2306
+ }),
2307
+ delete: async (id) => {
2308
+ const d = await get(id);
2309
+ await db.delete(id);
2310
+ return d;
2311
+ },
2312
+ get,
2313
+ patch: async (id, data, expectedUpdatedAt) => {
2314
+ const doc = await get(id);
2315
+ if (expectedUpdatedAt !== void 0 && doc.updatedAt !== expectedUpdatedAt) return err("CONFLICT");
2316
+ const up = typeof data === "function" ? await data(doc) : data;
2317
+ await dbPatch(db, id, {
2318
+ ...up,
2319
+ ...now
2320
+ });
2321
+ return {
2322
+ ...doc,
2323
+ ...up,
2324
+ ...now
2325
+ };
2326
+ },
2327
+ user
2328
+ };
2329
+ })), cq = zCustomQuery(config.query, customCtx(() => ({}))), cm = zCustomMutation(config.mutation, customCtx(() => ({}))), children = config.children ?? {}, crud = (table, schema, opt) => makeCrud({
2330
+ builders: {
2331
+ children,
2332
+ cm,
2333
+ cq,
2334
+ m,
2335
+ pq,
2336
+ q
2337
+ },
2338
+ options: opt,
2339
+ schema,
2340
+ table
2341
+ }), childCrud = (table, meta) => makeChildCrud({
2342
+ builders: {
2343
+ m,
2344
+ q
2345
+ },
2346
+ meta,
2347
+ table
2348
+ }), orgCrud = (table, schema, opt) => makeOrgCrud({
2349
+ builders: {
2350
+ m,
2351
+ q
2352
+ },
2353
+ options: opt,
2354
+ schema,
2355
+ table
2356
+ }), cacheCrud = (opts) => makeCacheCrud({
2357
+ ...opts,
2358
+ builders: {
2359
+ action: config.action,
2360
+ cm,
2361
+ cq,
2362
+ internalMutation: config.internalMutation,
2363
+ internalQuery: config.internalQuery,
2364
+ mutation: config.mutation,
2365
+ query: config.query
2366
+ }
2367
+ }), uniqueCheck = (table, field) => makeUnique({
2368
+ field,
2369
+ pq,
2370
+ table
2371
+ });
2372
+ return {
2373
+ cacheCrud,
2374
+ childCrud,
2375
+ cm,
2376
+ cq,
2377
+ crud,
2378
+ m,
2379
+ org: config.orgSchema ? makeOrg({
2380
+ cascadeTables: config.orgCascadeTables,
2381
+ getAuthUserId: config.getAuthUserId,
2382
+ mutation: config.mutation,
2383
+ query: config.query,
2384
+ schema: config.orgSchema
2385
+ }) : void 0,
2386
+ orgCrud,
2387
+ pq,
2388
+ q,
2389
+ uniqueCheck,
2390
+ user: { me: q({ handler: (c) => c.user }) }
2391
+ };
2392
+ };
2393
+
2394
+ //#endregion
2395
+ //#region src/server/test-crud.ts
2396
+ const getOrgMembership = async (db, orgId, userId) => {
2397
+ const orgDoc = await db.get(orgId);
2398
+ if (!orgDoc) return null;
2399
+ const isOwner = orgDoc.userId === userId, member = await db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique();
2400
+ if (!(isOwner || member)) return null;
2401
+ return {
2402
+ isAdmin: isOwner || member?.isAdmin === true,
2403
+ isOwner,
2404
+ member,
2405
+ orgDoc
2406
+ };
2407
+ }, checkAclPermission = (doc, userId, membership) => {
2408
+ const isCreator = doc.userId === userId, isEditor = (doc.editors ?? []).includes(userId);
2409
+ return isCreator || membership.isAdmin || isEditor;
2410
+ }, checkChildAclPermission = async (db, doc, parentField, userId, membership) => {
2411
+ if (doc.userId === userId || membership.isAdmin) return true;
2412
+ const parentId = doc[parentField], parent = parentId ? await db.get(parentId) : null;
2413
+ return (parent ? parent.editors ?? [] : []).some((eid) => eid === userId);
2414
+ }, addEditorToDoc = async (db, itemId, editorId, orgId) => {
2415
+ const doc = await db.get(itemId);
2416
+ if (doc?.orgId !== orgId) return { code: "NOT_FOUND" };
2417
+ const editors = doc.editors ?? [];
2418
+ if (editors.some((id) => id === editorId)) return doc;
2419
+ await db.patch(itemId, {
2420
+ editors: [...editors, editorId],
2421
+ updatedAt: Date.now()
2422
+ });
2423
+ return db.get(itemId);
2424
+ }, removeEditorFromDoc = async (db, itemId, editorId, orgId) => {
2425
+ const doc = await db.get(itemId);
2426
+ if (doc?.orgId !== orgId) return { code: "NOT_FOUND" };
2427
+ const editors = doc.editors ?? [], filtered = [];
2428
+ for (const id of editors) if (id !== editorId) filtered.push(id);
2429
+ await db.patch(itemId, {
2430
+ editors: filtered,
2431
+ updatedAt: Date.now()
2432
+ });
2433
+ return db.get(itemId);
2434
+ }, makeOrgTestCrud = (config) => {
2435
+ const { acl, aclFrom, cascade, mutation: rawMut, query: rawQry, table } = config, mutation = rawMut, query = rawQry, hasAcl = acl || Boolean(aclFrom), createAsUser = mutation({
2436
+ args: {
2437
+ data: v.any(),
2438
+ orgId: v.id("org"),
2439
+ userId: v.id("users")
2440
+ },
2441
+ handler: async (ctx, { data, orgId, userId }) => {
2442
+ if (!isTestMode()) return null;
2443
+ if (!await getOrgMembership(ctx.db, orgId, userId)) return { code: "NOT_ORG_MEMBER" };
2444
+ return ctx.db.insert(table, {
2445
+ ...data,
2446
+ orgId,
2447
+ updatedAt: Date.now(),
2448
+ userId
2449
+ });
2450
+ }
2451
+ }), updateAsUser = mutation({
2452
+ args: {
2453
+ data: v.any(),
2454
+ id: v.string(),
2455
+ orgId: v.id("org"),
2456
+ userId: v.id("users")
2457
+ },
2458
+ handler: async (ctx, { data, id, orgId, userId }) => {
2459
+ if (!isTestMode()) return null;
2460
+ const doc = await ctx.db.get(id);
2461
+ if (doc?.orgId !== orgId) return { code: "NOT_FOUND" };
2462
+ const membership = await getOrgMembership(ctx.db, orgId, userId);
2463
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
2464
+ if (hasAcl) {
2465
+ if (!(aclFrom ? await checkChildAclPermission(ctx.db, doc, aclFrom.field, userId, membership) : checkAclPermission(doc, userId, membership))) return { code: "FORBIDDEN" };
2466
+ }
2467
+ await ctx.db.patch(id, {
2468
+ ...data,
2469
+ updatedAt: Date.now()
2470
+ });
2471
+ return { success: true };
2472
+ }
2473
+ }), rmAsUser = mutation({
2474
+ args: {
2475
+ id: v.string(),
2476
+ orgId: v.id("org"),
2477
+ userId: v.id("users")
2478
+ },
2479
+ handler: async (ctx, { id, orgId, userId }) => {
2480
+ if (!isTestMode()) return null;
2481
+ const doc = await ctx.db.get(id);
2482
+ if (doc?.orgId !== orgId) return { code: "NOT_FOUND" };
2483
+ const membership = await getOrgMembership(ctx.db, orgId, userId);
2484
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
2485
+ if (hasAcl) {
2486
+ if (!(aclFrom ? await checkChildAclPermission(ctx.db, doc, aclFrom.field, userId, membership) : checkAclPermission(doc, userId, membership))) return { code: "FORBIDDEN" };
2487
+ }
2488
+ if (cascade) for (const { foreignKey, table: childTable } of cascade) {
2489
+ const children = await ctx.db.query(childTable).withIndex("by_parent", ((q) => q.eq(foreignKey, id))).collect();
2490
+ for (const c of children) await ctx.db.delete(c._id);
2491
+ }
2492
+ await ctx.db.delete(id);
2493
+ return { success: true };
2494
+ }
2495
+ }), result = {
2496
+ bulkRmAsUser: mutation({
2497
+ args: {
2498
+ ids: v.array(v.string()),
2499
+ orgId: v.id("org"),
2500
+ userId: v.id("users")
2501
+ },
2502
+ handler: async (ctx, { ids, orgId, userId }) => {
2503
+ if (!isTestMode()) return null;
2504
+ const orgDoc = await ctx.db.get(orgId);
2505
+ if (!orgDoc) return { code: "NOT_FOUND" };
2506
+ const isOwner = orgDoc.userId === userId, member = await ctx.db.query("orgMember").withIndex("by_org_user", ((q) => q.eq("orgId", orgId).eq("userId", userId))).unique();
2507
+ if (!(isOwner || member)) return { code: "NOT_ORG_MEMBER" };
2508
+ if (!(isOwner || member?.isAdmin)) return { code: "INSUFFICIENT_ORG_ROLE" };
2509
+ let count = 0;
2510
+ for (const id of ids) if ((await ctx.db.get(id))?.orgId === orgId) {
2511
+ if (cascade) for (const { foreignKey, table: childTable } of cascade) {
2512
+ const children = await ctx.db.query(childTable).withIndex("by_parent", ((q) => q.eq(foreignKey, id))).collect();
2513
+ for (const c of children) await ctx.db.delete(c._id);
2514
+ }
2515
+ await ctx.db.delete(id);
2516
+ count += 1;
2517
+ }
2518
+ return { count };
2519
+ }
2520
+ }),
2521
+ createAsUser,
2522
+ listAsUser: query({
2523
+ args: {
2524
+ orgId: v.id("org"),
2525
+ userId: v.id("users")
2526
+ },
2527
+ handler: async (ctx, { orgId, userId }) => {
2528
+ if (!isTestMode()) return null;
2529
+ if (!await getOrgMembership(ctx.db, orgId, userId)) return { code: "NOT_ORG_MEMBER" };
2530
+ return ctx.db.query(table).withIndex("by_org", ((q) => q.eq("orgId", orgId))).collect();
2531
+ }
2532
+ }),
2533
+ rmAsUser,
2534
+ updateAsUser
2535
+ };
2536
+ if (hasAcl) {
2537
+ result.addEditorAsUser = mutation({
2538
+ args: {
2539
+ editorId: v.id("users"),
2540
+ itemId: v.string(),
2541
+ orgId: v.id("org"),
2542
+ userId: v.id("users")
2543
+ },
2544
+ handler: async (ctx, { editorId, itemId, orgId, userId }) => {
2545
+ if (!isTestMode()) return null;
2546
+ const membership = await getOrgMembership(ctx.db, orgId, userId);
2547
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
2548
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
2549
+ return addEditorToDoc(ctx.db, itemId, editorId, orgId);
2550
+ }
2551
+ });
2552
+ result.removeEditorAsUser = mutation({
2553
+ args: {
2554
+ editorId: v.id("users"),
2555
+ itemId: v.string(),
2556
+ orgId: v.id("org"),
2557
+ userId: v.id("users")
2558
+ },
2559
+ handler: async (ctx, { editorId, itemId, orgId, userId }) => {
2560
+ if (!isTestMode()) return null;
2561
+ const membership = await getOrgMembership(ctx.db, orgId, userId);
2562
+ if (!membership) return { code: "NOT_ORG_MEMBER" };
2563
+ if (!membership.isAdmin) return { code: "INSUFFICIENT_ORG_ROLE" };
2564
+ return removeEditorFromDoc(ctx.db, itemId, editorId, orgId);
2565
+ }
2566
+ });
2567
+ }
2568
+ return result;
2569
+ };
2570
+
2571
+ //#endregion
2572
+ export { baseTable, canEdit, checkSchema, err, getErrorCode, getErrorMessage, getOrgMember, getOrgMembership, getOrgRole, isRecord, makeFileUpload, makeOrg, makeOrgTestCrud, makeTestAuth, orgCascade, orgChildTable, orgTable, orgTables, ownedTable, requireOrgMember, requireOrgRole, setup, time, uploadTables };