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,572 @@
1
+ /* eslint-disable no-await-in-loop, max-statements, @typescript-eslint/no-unnecessary-condition */
2
+ /** biome-ignore-all lint/performance/noAwaitInLoops: sequential deletes */
3
+ import type { GenericDataModel, GenericMutationCtx, GenericQueryCtx, MutationBuilder, QueryBuilder } from 'convex/server'
4
+ import type { GenericId } from 'convex/values'
5
+ import type { ZodObject, ZodRawShape } from 'zod/v4'
6
+
7
+ import { customCtx } from 'convex-helpers/server/customFunctions'
8
+ import { zCustomMutation, zCustomQuery, zid } from 'convex-helpers/server/zod4'
9
+ import { z } from 'zod/v4'
10
+
11
+ import type { DbLike, FilterLike, IndexLike, Mb, OrgRole, Qb, Rec } from './types'
12
+
13
+ import { err, getUser, time } from './helpers'
14
+ import { getOrgMember, getOrgRole, requireOrgMember, requireOrgRole } from './org-helpers'
15
+
16
+ interface InviteDocLike {
17
+ [k: string]: unknown
18
+ _creationTime: number
19
+ _id: GenericId<'orgInvite'>
20
+ email: string
21
+ expiresAt: number
22
+ isAdmin: boolean
23
+ orgId: GenericId<'org'>
24
+ token: string
25
+ }
26
+
27
+ interface JoinRequestItem {
28
+ request: {
29
+ [k: string]: unknown
30
+ _creationTime: number
31
+ _id: GenericId<'orgJoinRequest'>
32
+ message?: string
33
+ orgId: GenericId<'org'>
34
+ status: string
35
+ userId: GenericId<'users'>
36
+ }
37
+ user: null | OrgUserLike
38
+ }
39
+
40
+ interface OrgDocLike {
41
+ [k: string]: unknown
42
+ _creationTime: number
43
+ _id: GenericId<'org'>
44
+ avatarId?: GenericId<'_storage'>
45
+ name: string
46
+ slug: string
47
+ updatedAt: number
48
+ userId: GenericId<'users'>
49
+ }
50
+
51
+ interface OrgMemberItem {
52
+ memberId?: GenericId<'orgMember'>
53
+ role: OrgRole
54
+ user: null | OrgUserLike
55
+ userId: GenericId<'users'>
56
+ }
57
+
58
+ interface OrgUserLike {
59
+ [k: string]: unknown
60
+ _id: GenericId<'users'>
61
+ email?: string
62
+ image?: string
63
+ name?: string
64
+ }
65
+
66
+ const generateToken = () => {
67
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
68
+ let token = ''
69
+ for (let i = 0; i < 32; i += 1) token += chars.charAt(Math.floor(Math.random() * chars.length))
70
+ return token
71
+ },
72
+ SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000,
73
+ makeOrg = <DM extends GenericDataModel, S extends ZodRawShape>({
74
+ cascadeTables,
75
+ getAuthUserId,
76
+ mutation,
77
+ query,
78
+ schema: orgSchema
79
+ }: {
80
+ cascadeTables?: string[]
81
+ getAuthUserId: (ctx: never) => Promise<null | string>
82
+ mutation: MutationBuilder<DM, 'public'>
83
+ query: QueryBuilder<DM, 'public'>
84
+ schema: ZodObject<S>
85
+ }) => {
86
+ const mb = zCustomMutation(
87
+ mutation,
88
+ customCtx(async (c: GenericMutationCtx<DM>) => ({
89
+ user: await getUser({ ctx: c as never, db: c.db as never as DbLike, getAuthUserId })
90
+ }))
91
+ ) as never as Mb,
92
+ qb = zCustomQuery(
93
+ query,
94
+ customCtx(async (c: GenericQueryCtx<DM>) => ({
95
+ user: await getUser({ ctx: c as never, db: c.db as never as DbLike, getAuthUserId })
96
+ }))
97
+ ) as never as Qb,
98
+ pqb = zCustomQuery(
99
+ query,
100
+ customCtx(() => ({}))
101
+ ) as never as Qb,
102
+ m = mb,
103
+ q = qb,
104
+ pq = pqb,
105
+ create = m({
106
+ args: { data: orgSchema },
107
+ handler: async (c: Rec, { data }: { data: Rec }) => {
108
+ const existing = await (c.db as DbLike)
109
+ .query('org')
110
+ .withIndex('by_slug', ((o: IndexLike) => o.eq('slug', data.slug)) as never)
111
+ .unique()
112
+ if (existing) return err('ORG_SLUG_TAKEN')
113
+ const orgId = await (c.db as DbLike).insert('org', {
114
+ avatarId: (data.avatarId as string) ?? undefined,
115
+ name: data.name,
116
+ slug: data.slug,
117
+ userId: (c.user as Rec)._id,
118
+ ...time()
119
+ })
120
+ return { orgId } as { orgId: GenericId<'org'> }
121
+ }
122
+ }),
123
+ update = m({
124
+ args: { data: orgSchema.partial(), orgId: zid('org') },
125
+ handler: async (c: Rec, { data, orgId }: { data: Rec; orgId: string }) => {
126
+ await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: (c.user as Rec)._id as string })
127
+ const newSlug = data.slug as string | undefined
128
+ if (newSlug !== undefined) {
129
+ const existing = await (c.db as DbLike)
130
+ .query('org')
131
+ .withIndex('by_slug', ((o: IndexLike) => o.eq('slug', newSlug)) as never)
132
+ .unique()
133
+ if (existing && existing._id !== orgId) return err('ORG_SLUG_TAKEN')
134
+ }
135
+ const patchData: Rec = {}
136
+ if (data.name !== undefined) patchData.name = data.name
137
+ if (newSlug !== undefined) patchData.slug = newSlug
138
+ if (data.avatarId !== undefined && data.avatarId !== null) patchData.avatarId = data.avatarId
139
+ await (c.db as DbLike).patch(orgId, { ...patchData, ...time() })
140
+ }
141
+ }),
142
+ get = q({
143
+ args: { orgId: zid('org') },
144
+ handler: async (c: Rec, { orgId }: { orgId: string }): Promise<null | OrgDocLike> => {
145
+ await requireOrgMember({ db: c.db, orgId, userId: (c.user as Rec)._id as string })
146
+ return (c.db as DbLike).get(orgId) as Promise<null | OrgDocLike>
147
+ }
148
+ }),
149
+ getBySlug = pq({
150
+ args: { slug: z.string() },
151
+ handler: async (c: Rec, { slug }: { slug: string }): Promise<null | OrgDocLike> =>
152
+ (c.db as DbLike)
153
+ .query('org')
154
+ .withIndex('by_slug', ((o: IndexLike) => o.eq('slug', slug)) as never)
155
+ .unique() as Promise<null | OrgDocLike>
156
+ }),
157
+ getPublic = pq({
158
+ args: { slug: z.string() },
159
+ handler: async (
160
+ c: Rec,
161
+ { slug }: { slug: string }
162
+ ): Promise<null | { _id: GenericId<'org'>; avatarId?: GenericId<'_storage'>; name: string; slug: string }> => {
163
+ const orgDoc = await (c.db as DbLike)
164
+ .query('org')
165
+ .withIndex('by_slug', ((o: IndexLike) => o.eq('slug', slug)) as never)
166
+ .unique()
167
+ if (!orgDoc) return null
168
+ return {
169
+ _id: orgDoc._id as GenericId<'org'>,
170
+ avatarId: orgDoc.avatarId as GenericId<'_storage'> | undefined,
171
+ name: orgDoc.name as string,
172
+ slug: orgDoc.slug as string
173
+ }
174
+ }
175
+ }),
176
+ myOrgs = q({
177
+ args: {},
178
+ handler: async (c: Rec): Promise<{ org: OrgDocLike; role: OrgRole }[]> => {
179
+ const uid = (c.user as Rec)._id as string,
180
+ db = c.db as DbLike,
181
+ ownedOrgs = await db
182
+ .query('org')
183
+ .withIndex('by_user', ((o: IndexLike) => o.eq('userId', uid)) as never)
184
+ .collect(),
185
+ memberships = await db
186
+ .query('orgMember')
187
+ .withIndex('by_user', ((o: IndexLike) => o.eq('userId', uid)) as never)
188
+ .collect(),
189
+ memberOrgIds = memberships.map((x: Rec) => x.orgId as string),
190
+ memberOrgResults = await Promise.all(memberOrgIds.map(async (id: string) => db.get(id))),
191
+ memberOrgs: Rec[] = []
192
+ for (const orgDoc of memberOrgResults) if (orgDoc) memberOrgs.push(orgDoc)
193
+ const ownedIds = new Set(ownedOrgs.map((o: Rec) => o._id as string)),
194
+ result: { org: OrgDocLike; role: OrgRole }[] = []
195
+ for (const o of ownedOrgs) result.push({ org: o as OrgDocLike, role: 'owner' })
196
+ for (const o of memberOrgs)
197
+ if (!ownedIds.has(o._id as string)) {
198
+ const member = memberships.find((x: Rec) => x.orgId === o._id),
199
+ role: OrgRole = member?.isAdmin ? 'admin' : 'member'
200
+ result.push({ org: o as OrgDocLike, role })
201
+ }
202
+ return result
203
+ }
204
+ }),
205
+ remove = m({
206
+ args: { orgId: zid('org') },
207
+
208
+ handler: async (c: Rec, { orgId }: { orgId: string }) => {
209
+ const db = c.db as DbLike,
210
+ orgDoc = await db.get(orgId)
211
+ if (!orgDoc) return err('NOT_FOUND')
212
+ if (orgDoc.userId !== (c.user as Rec)._id) return err('FORBIDDEN')
213
+ if (cascadeTables)
214
+ for (const table of cascadeTables) {
215
+ const docs = await db
216
+ .query(table)
217
+ .filter((o: FilterLike) => o.eq(o.field('orgId'), orgId))
218
+ .collect()
219
+ await Promise.all(docs.map(async (d: Rec) => db.delete(d._id as string)))
220
+ }
221
+
222
+ const joinRequests = await db
223
+ .query('orgJoinRequest')
224
+ .withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
225
+ .collect()
226
+ await Promise.all(joinRequests.map(async (r: Rec) => db.delete(r._id as string)))
227
+ const invites = await db
228
+ .query('orgInvite')
229
+ .withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
230
+ .collect()
231
+ await Promise.all(invites.map(async (i: Rec) => db.delete(i._id as string)))
232
+ const orgMembers = await db
233
+ .query('orgMember')
234
+ .withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
235
+ .collect()
236
+ await Promise.all(orgMembers.map(async (x: Rec) => db.delete(x._id as string)))
237
+ await db.delete(orgId)
238
+ }
239
+ }),
240
+ isSlugAvailable = pq({
241
+ args: { slug: z.string() },
242
+ handler: async (c: Rec, { slug }: { slug: string }) => {
243
+ const existing = await (c.db as DbLike)
244
+ .query('org')
245
+ .withIndex('by_slug', ((o: IndexLike) => o.eq('slug', slug)) as never)
246
+ .unique()
247
+ return { available: !existing } as { available: boolean }
248
+ }
249
+ }),
250
+ membership = q({
251
+ args: { orgId: zid('org') },
252
+ handler: async (
253
+ c: Rec,
254
+ { orgId }: { orgId: string }
255
+ ): Promise<null | { memberId: GenericId<'orgMember'> | null; role: OrgRole }> => {
256
+ const db = c.db as DbLike,
257
+ orgDoc = await db.get(orgId)
258
+ if (!orgDoc) return err('NOT_FOUND')
259
+ const userId = (c.user as Rec)._id as string,
260
+ member = await getOrgMember({ db, orgId, userId }),
261
+ role = getOrgRole({ member, org: orgDoc, userId })
262
+ if (!role) return null
263
+ return { memberId: ((member as null | Rec)?._id as GenericId<'orgMember'>) ?? null, role }
264
+ }
265
+ }),
266
+ members = q({
267
+ args: { orgId: zid('org') },
268
+ handler: async (c: Rec, { orgId }: { orgId: string }): Promise<OrgMemberItem[]> => {
269
+ const db = c.db as DbLike,
270
+ userId = (c.user as Rec)._id as string
271
+ await requireOrgMember({ db, orgId, userId })
272
+ const orgDoc = await db.get(orgId)
273
+ if (!orgDoc) return err('NOT_FOUND')
274
+ const result: OrgMemberItem[] = [],
275
+ ownerUser = await db.get(orgDoc.userId as string)
276
+ result.push({
277
+ role: 'owner',
278
+ user: ownerUser as null | OrgUserLike,
279
+ userId: orgDoc.userId as GenericId<'users'>
280
+ })
281
+ const memberDocs = await db
282
+ .query('orgMember')
283
+ .withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
284
+ .collect(),
285
+ userDocs = await Promise.all(memberDocs.map(async (x: Rec) => db.get(x.userId as string)))
286
+ for (let i = 0; i < memberDocs.length; i += 1) {
287
+ const memberDoc = memberDocs[i],
288
+ userDoc = userDocs[i]
289
+ if (memberDoc)
290
+ result.push({
291
+ memberId: memberDoc._id as GenericId<'orgMember'>,
292
+ role: memberDoc.isAdmin ? 'admin' : 'member',
293
+ user: (userDoc as null | OrgUserLike) ?? null,
294
+ userId: memberDoc.userId as GenericId<'users'>
295
+ })
296
+ }
297
+ return result
298
+ }
299
+ }),
300
+ setAdmin = m({
301
+ args: { isAdmin: z.boolean(), memberId: zid('orgMember') },
302
+ handler: async (c: Rec, { isAdmin, memberId }: { isAdmin: boolean; memberId: string }) => {
303
+ const db = c.db as DbLike,
304
+ memberDoc = await db.get(memberId)
305
+ if (!memberDoc) return err('NOT_FOUND')
306
+ const orgDoc = await db.get(memberDoc.orgId as string)
307
+ if (!orgDoc) return err('NOT_FOUND')
308
+ if (orgDoc.userId !== (c.user as Rec)._id) return err('FORBIDDEN')
309
+ if (memberDoc.userId === orgDoc.userId) return err('CANNOT_MODIFY_OWNER')
310
+ await db.patch(memberId, { isAdmin, ...time() })
311
+ }
312
+ }),
313
+ removeMember = m({
314
+ args: { memberId: zid('orgMember') },
315
+ handler: async (c: Rec, { memberId }: { memberId: string }) => {
316
+ const db = c.db as DbLike,
317
+ memberDoc = await db.get(memberId)
318
+ if (!memberDoc) return err('NOT_FOUND')
319
+ const orgDoc = await db.get(memberDoc.orgId as string)
320
+ if (!orgDoc) return err('NOT_FOUND')
321
+ if (memberDoc.userId === orgDoc.userId) return err('CANNOT_MODIFY_OWNER')
322
+ const { role } = await requireOrgRole({
323
+ db,
324
+ minRole: 'admin',
325
+ orgId: memberDoc.orgId as string,
326
+ userId: (c.user as Rec)._id as string
327
+ })
328
+ if (role === 'admin' && memberDoc.isAdmin) return err('CANNOT_MODIFY_ADMIN')
329
+ await db.delete(memberId)
330
+ }
331
+ }),
332
+ leave = m({
333
+ args: { orgId: zid('org') },
334
+ handler: async (c: Rec, { orgId }: { orgId: string }) => {
335
+ const db = c.db as DbLike,
336
+ orgDoc = await db.get(orgId)
337
+ if (!orgDoc) return err('NOT_FOUND')
338
+ const userId = (c.user as Rec)._id as string
339
+ if (orgDoc.userId === userId) return err('MUST_TRANSFER_OWNERSHIP')
340
+ const member = await getOrgMember({ db, orgId, userId })
341
+ if (!member) return err('NOT_ORG_MEMBER')
342
+ await db.delete((member as Rec)._id as string)
343
+ }
344
+ }),
345
+ transferOwnership = m({
346
+ args: { newOwnerId: zid('users'), orgId: zid('org') },
347
+ handler: async (c: Rec, { newOwnerId, orgId }: { newOwnerId: string; orgId: string }) => {
348
+ const db = c.db as DbLike,
349
+ orgDoc = await db.get(orgId)
350
+ if (!orgDoc) return err('NOT_FOUND')
351
+ if (orgDoc.userId !== (c.user as Rec)._id) return err('FORBIDDEN')
352
+ const targetMember = await getOrgMember({ db, orgId, userId: newOwnerId })
353
+ if (!targetMember) return err('NOT_ORG_MEMBER')
354
+ if (!(targetMember as Rec).isAdmin) return err('TARGET_MUST_BE_ADMIN')
355
+ await db.patch(orgId, { userId: newOwnerId, ...time() })
356
+ await db.delete((targetMember as Rec)._id as string)
357
+ await db.insert('orgMember', {
358
+ isAdmin: true,
359
+ orgId,
360
+ userId: (c.user as Rec)._id,
361
+ ...time()
362
+ })
363
+ }
364
+ }),
365
+ invite = m({
366
+ args: { email: z.email(), isAdmin: z.boolean(), orgId: zid('org') },
367
+ handler: async (c: Rec, { email, isAdmin, orgId }: { email: string; isAdmin: boolean; orgId: string }) => {
368
+ await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: (c.user as Rec)._id as string })
369
+ const token = generateToken(),
370
+ expiresAt = Date.now() + SEVEN_DAYS_MS,
371
+ inviteId = await (c.db as DbLike).insert('orgInvite', {
372
+ email,
373
+ expiresAt,
374
+ isAdmin,
375
+ orgId,
376
+ token
377
+ })
378
+ return { inviteId, token } as { inviteId: GenericId<'orgInvite'>; token: string }
379
+ }
380
+ }),
381
+ acceptInvite = m({
382
+ args: { token: z.string() },
383
+ handler: async (c: Rec, { token }: { token: string }) => {
384
+ const db = c.db as DbLike,
385
+ userId = (c.user as Rec)._id as string,
386
+ inviteDoc = await db
387
+ .query('orgInvite')
388
+ .withIndex('by_token', ((o: IndexLike) => o.eq('token', token)) as never)
389
+ .unique()
390
+ if (!inviteDoc) return err('INVALID_INVITE')
391
+ if ((inviteDoc.expiresAt as number) < Date.now()) return err('INVITE_EXPIRED')
392
+ const existingMember = await getOrgMember({ db, orgId: inviteDoc.orgId as string, userId }),
393
+ orgDoc = await db.get(inviteDoc.orgId as string)
394
+ if (!orgDoc) return err('NOT_FOUND')
395
+ if (existingMember || orgDoc.userId === userId) return err('ALREADY_ORG_MEMBER')
396
+ const pendingRequest = await db
397
+ .query('orgJoinRequest')
398
+ .withIndex('by_org_status', ((o: IndexLike) =>
399
+ o.eq('orgId', inviteDoc.orgId).eq('status', 'pending')) as never)
400
+ .filter((o: FilterLike) => o.eq(o.field('userId'), userId))
401
+ .unique()
402
+ if (pendingRequest) await db.patch(pendingRequest._id as string, { status: 'approved', ...time() })
403
+ await db.insert('orgMember', {
404
+ isAdmin: inviteDoc.isAdmin,
405
+ orgId: inviteDoc.orgId,
406
+ userId,
407
+ ...time()
408
+ })
409
+ await db.delete(inviteDoc._id as string)
410
+ return { orgId: inviteDoc.orgId } as { orgId: GenericId<'org'> }
411
+ }
412
+ }),
413
+ revokeInvite = m({
414
+ args: { inviteId: zid('orgInvite') },
415
+ handler: async (c: Rec, { inviteId }: { inviteId: string }) => {
416
+ const db = c.db as DbLike,
417
+ inviteDoc = await db.get(inviteId)
418
+ if (!inviteDoc) return err('NOT_FOUND')
419
+ await requireOrgRole({
420
+ db,
421
+ minRole: 'admin',
422
+ orgId: inviteDoc.orgId as string,
423
+ userId: (c.user as Rec)._id as string
424
+ })
425
+ await db.delete(inviteId)
426
+ }
427
+ }),
428
+ pendingInvites = q({
429
+ args: { orgId: zid('org') },
430
+ handler: async (c: Rec, { orgId }: { orgId: string }): Promise<InviteDocLike[]> => {
431
+ await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: (c.user as Rec)._id as string })
432
+ return (c.db as DbLike)
433
+ .query('orgInvite')
434
+ .withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
435
+ .collect() as Promise<InviteDocLike[]>
436
+ }
437
+ }),
438
+ requestJoin = m({
439
+ args: { message: z.string().optional(), orgId: zid('org') },
440
+ handler: async (c: Rec, { message, orgId }: { message?: string; orgId: string }) => {
441
+ const db = c.db as DbLike,
442
+ userId = (c.user as Rec)._id as string,
443
+ orgDoc = await db.get(orgId)
444
+ if (!orgDoc) return err('NOT_FOUND')
445
+ const existingMember = await getOrgMember({ db, orgId, userId })
446
+ if (existingMember || orgDoc.userId === userId) return err('ALREADY_ORG_MEMBER')
447
+ const existingRequest = await db
448
+ .query('orgJoinRequest')
449
+ .withIndex('by_org_status', ((o: IndexLike) => o.eq('orgId', orgId).eq('status', 'pending')) as never)
450
+ .filter((o: FilterLike) => o.eq(o.field('userId'), userId))
451
+ .unique()
452
+ if (existingRequest) return err('JOIN_REQUEST_EXISTS')
453
+ const requestId = await db.insert('orgJoinRequest', {
454
+ message,
455
+ orgId,
456
+ status: 'pending',
457
+ userId
458
+ })
459
+ return { requestId } as { requestId: GenericId<'orgJoinRequest'> }
460
+ }
461
+ }),
462
+ approveJoinRequest = m({
463
+ args: { isAdmin: z.boolean().optional(), requestId: zid('orgJoinRequest') },
464
+ handler: async (c: Rec, { isAdmin, requestId }: { isAdmin?: boolean; requestId: string }) => {
465
+ const db = c.db as DbLike,
466
+ requestDoc = await db.get(requestId)
467
+ if (!requestDoc) return err('NOT_FOUND')
468
+ await requireOrgRole({
469
+ db,
470
+ minRole: 'admin',
471
+ orgId: requestDoc.orgId as string,
472
+ userId: (c.user as Rec)._id as string
473
+ })
474
+ await db.insert('orgMember', {
475
+ isAdmin: isAdmin ?? false,
476
+ orgId: requestDoc.orgId,
477
+ userId: requestDoc.userId,
478
+ ...time()
479
+ })
480
+ await db.patch(requestId, { status: 'approved' })
481
+ }
482
+ }),
483
+ rejectJoinRequest = m({
484
+ args: { requestId: zid('orgJoinRequest') },
485
+ handler: async (c: Rec, { requestId }: { requestId: string }) => {
486
+ const db = c.db as DbLike,
487
+ requestDoc = await db.get(requestId)
488
+ if (!requestDoc) return err('NOT_FOUND')
489
+ await requireOrgRole({
490
+ db,
491
+ minRole: 'admin',
492
+ orgId: requestDoc.orgId as string,
493
+ userId: (c.user as Rec)._id as string
494
+ })
495
+ await db.patch(requestId, { status: 'rejected' })
496
+ }
497
+ }),
498
+ cancelJoinRequest = m({
499
+ args: { requestId: zid('orgJoinRequest') },
500
+ handler: async (c: Rec, { requestId }: { requestId: string }) => {
501
+ const db = c.db as DbLike,
502
+ requestDoc = await db.get(requestId)
503
+ if (!requestDoc) return err('NOT_FOUND')
504
+ if (requestDoc.userId !== (c.user as Rec)._id) return err('FORBIDDEN')
505
+ if (requestDoc.status !== 'pending') return err('NOT_FOUND')
506
+ await db.delete(requestId)
507
+ }
508
+ }),
509
+ pendingJoinRequests = q({
510
+ args: { orgId: zid('org') },
511
+ handler: async (c: Rec, { orgId }: { orgId: string }): Promise<JoinRequestItem[]> => {
512
+ const db = c.db as DbLike
513
+ await requireOrgRole({ db, minRole: 'admin', orgId, userId: (c.user as Rec)._id as string })
514
+ const requests = await db
515
+ .query('orgJoinRequest')
516
+ .withIndex('by_org_status', ((o: IndexLike) => o.eq('orgId', orgId).eq('status', 'pending')) as never)
517
+ .collect(),
518
+ users = await Promise.all(requests.map(async (r: Rec) => db.get(r.userId as string))),
519
+ result: JoinRequestItem[] = []
520
+ for (let i = 0; i < requests.length; i += 1) {
521
+ const req = requests[i],
522
+ usr = users[i]
523
+ if (req) result.push({ request: req as JoinRequestItem['request'], user: (usr as null | OrgUserLike) ?? null })
524
+ }
525
+ return result
526
+ }
527
+ }),
528
+ myJoinRequest = q({
529
+ args: { orgId: zid('org') },
530
+ handler: async (c: Rec, { orgId }: { orgId: string }) =>
531
+ (c.db as DbLike)
532
+ .query('orgJoinRequest')
533
+ .withIndex('by_org_status', ((o: IndexLike) => o.eq('orgId', orgId).eq('status', 'pending')) as never)
534
+ .filter((o: FilterLike) => o.eq(o.field('userId'), (c.user as Rec)._id))
535
+ .unique() as Promise<null | {
536
+ _id: GenericId<'orgJoinRequest'>
537
+ message?: string
538
+ orgId: GenericId<'org'>
539
+ status: string
540
+ userId: GenericId<'users'>
541
+ }>
542
+ })
543
+ return {
544
+ acceptInvite,
545
+ approveJoinRequest,
546
+ cancelJoinRequest,
547
+ create,
548
+ get,
549
+ getBySlug,
550
+ getPublic,
551
+ invite,
552
+ isSlugAvailable,
553
+ leave,
554
+ members,
555
+ membership,
556
+ myJoinRequest,
557
+ myOrgs,
558
+ pendingInvites,
559
+ pendingJoinRequests,
560
+ rejectJoinRequest,
561
+ remove,
562
+ removeMember,
563
+ requestJoin,
564
+ revokeInvite,
565
+ setAdmin,
566
+ transferOwnership,
567
+ update
568
+ }
569
+ }
570
+
571
+ export { makeOrg }
572
+ export type { InviteDocLike, JoinRequestItem, OrgDocLike, OrgMemberItem, OrgUserLike }
@@ -0,0 +1,107 @@
1
+ import type { ZodObject, ZodRawShape } from 'zod/v4'
2
+
3
+ import { zodOutputToConvexFields as z2c } from 'convex-helpers/server/zod4'
4
+ import { defineTable } from 'convex/server'
5
+ import { v } from 'convex/values'
6
+
7
+ const baseTable = <T extends ZodRawShape>(s: ZodObject<T>) =>
8
+ defineTable({ ...z2c(s.shape), updatedAt: v.optional(v.number()) }),
9
+ ownedTable = <T extends ZodRawShape>(s: ZodObject<T>) =>
10
+ defineTable({ ...z2c(s.shape), updatedAt: v.number(), userId: v.id('users') }).index('by_user', ['userId' as never]),
11
+ orgTable = <T extends ZodRawShape>(s: ZodObject<T>) =>
12
+ defineTable({
13
+ ...z2c(s.shape),
14
+ orgId: v.id('org'),
15
+ updatedAt: v.number(),
16
+ userId: v.id('users')
17
+ })
18
+ .index('by_org', ['orgId' as never])
19
+ .index('by_org_user', ['orgId' as never, 'userId' as never]),
20
+ orgChildTable = <T extends ZodRawShape>(
21
+ s: ZodObject<T>,
22
+ parent: {
23
+ foreignKey: string
24
+ table: string
25
+ }
26
+ ) =>
27
+ defineTable({
28
+ ...z2c(s.shape),
29
+ orgId: v.id('org'),
30
+ updatedAt: v.number(),
31
+ userId: v.id('users')
32
+ })
33
+ .index('by_org', ['orgId' as never])
34
+ .index('by_parent', [parent.foreignKey as never]),
35
+ childTable = <T extends ZodRawShape>(s: ZodObject<T>, indexField: string, indexName?: string) =>
36
+ defineTable({
37
+ ...z2c(s.shape),
38
+ updatedAt: v.number()
39
+ }).index(indexName ?? `by_${indexField}`, [indexField as never]),
40
+ orgTables = () => ({
41
+ org: defineTable({
42
+ avatarId: v.optional(v.id('_storage')),
43
+ name: v.string(),
44
+ slug: v.string(),
45
+ updatedAt: v.number(),
46
+ userId: v.id('users')
47
+ })
48
+ .index('by_slug', ['slug'])
49
+ .index('by_user', ['userId']),
50
+ orgInvite: defineTable({
51
+ email: v.string(),
52
+ expiresAt: v.number(),
53
+ isAdmin: v.boolean(),
54
+ orgId: v.id('org'),
55
+ token: v.string()
56
+ })
57
+ .index('by_org', ['orgId'])
58
+ .index('by_token', ['token']),
59
+ orgJoinRequest: defineTable({
60
+ message: v.optional(v.string()),
61
+ orgId: v.id('org'),
62
+ status: v.union(v.literal('pending'), v.literal('approved'), v.literal('rejected')),
63
+ userId: v.id('users')
64
+ })
65
+ .index('by_org', ['orgId'])
66
+ .index('by_org_status', ['orgId', 'status'])
67
+ .index('by_user', ['userId']),
68
+ orgMember: defineTable({
69
+ isAdmin: v.boolean(),
70
+ orgId: v.id('org'),
71
+ updatedAt: v.number(),
72
+ userId: v.id('users')
73
+ })
74
+ .index('by_org', ['orgId'])
75
+ .index('by_org_user', ['orgId', 'userId'])
76
+ .index('by_user', ['userId'])
77
+ }),
78
+ uploadTables = () => ({
79
+ uploadChunk: defineTable({
80
+ chunkIndex: v.number(),
81
+ storageId: v.id('_storage'),
82
+ totalChunks: v.number(),
83
+ uploadId: v.string(),
84
+ userId: v.id('users')
85
+ })
86
+ .index('by_upload', ['uploadId'])
87
+ .index('by_user', ['userId']),
88
+ uploadRateLimit: defineTable({
89
+ timestamp: v.number(),
90
+ userId: v.id('users')
91
+ }).index('by_user', ['userId']),
92
+ uploadSession: defineTable({
93
+ completedChunks: v.number(),
94
+ contentType: v.string(),
95
+ fileName: v.string(),
96
+ finalStorageId: v.optional(v.id('_storage')),
97
+ status: v.union(v.literal('pending'), v.literal('assembling'), v.literal('completed'), v.literal('failed')),
98
+ totalChunks: v.number(),
99
+ totalSize: v.number(),
100
+ uploadId: v.string(),
101
+ userId: v.id('users')
102
+ })
103
+ .index('by_upload_id', ['uploadId'])
104
+ .index('by_user', ['userId'])
105
+ })
106
+
107
+ export { baseTable, childTable, orgChildTable, orgTable, orgTables, ownedTable, uploadTables }