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.
- package/README.md +926 -0
- package/dist/components/index.mjs +937 -0
- package/dist/error-D4GuI0ot.mjs +71 -0
- package/dist/file-field-BqVgy8xY.mjs +205 -0
- package/dist/form-BXJK_j10.d.mts +99 -0
- package/dist/index.d.mts +433 -0
- package/dist/index.mjs +1 -0
- package/dist/index2.d.mts +5 -0
- package/dist/index3.d.mts +35 -0
- package/dist/index4.d.mts +101 -0
- package/dist/index5.d.mts +842 -0
- package/dist/next/index.mjs +151 -0
- package/dist/org-CmJBb8z-.d.mts +56 -0
- package/dist/react/index.mjs +158 -0
- package/dist/retry.d.mts +12 -0
- package/dist/retry.mjs +35 -0
- package/dist/schema.d.mts +23 -0
- package/dist/schema.mjs +15 -0
- package/dist/server/index.mjs +2572 -0
- package/dist/types-DWBVRtit.d.mts +322 -0
- package/dist/use-online-status-CMr73Jlk.mjs +155 -0
- package/dist/use-upload-DtELytQi.mjs +95 -0
- package/dist/zod.d.mts +18 -0
- package/dist/zod.mjs +87 -0
- package/package.json +40 -0
- package/src/components/editors-section.tsx +86 -0
- package/src/components/fields.tsx +884 -0
- package/src/components/file-field.tsx +234 -0
- package/src/components/form.tsx +191 -0
- package/src/components/index.ts +11 -0
- package/src/components/offline-indicator.tsx +15 -0
- package/src/components/org-avatar.tsx +13 -0
- package/src/components/permission-guard.tsx +36 -0
- package/src/components/role-badge.tsx +14 -0
- package/src/components/suspense-wrap.tsx +8 -0
- package/src/index.ts +40 -0
- package/src/next/active-org.ts +33 -0
- package/src/next/auth.ts +9 -0
- package/src/next/image.ts +134 -0
- package/src/next/index.ts +3 -0
- package/src/react/form-meta.ts +53 -0
- package/src/react/form.ts +201 -0
- package/src/react/index.ts +8 -0
- package/src/react/org.tsx +96 -0
- package/src/react/use-active-org.ts +48 -0
- package/src/react/use-bulk-selection.ts +47 -0
- package/src/react/use-online-status.ts +21 -0
- package/src/react/use-optimistic.ts +54 -0
- package/src/react/use-upload.ts +101 -0
- package/src/retry.ts +47 -0
- package/src/schema.ts +30 -0
- package/src/server/cache-crud.ts +175 -0
- package/src/server/check-schema.ts +29 -0
- package/src/server/child.ts +98 -0
- package/src/server/crud.ts +384 -0
- package/src/server/db.ts +7 -0
- package/src/server/error.ts +39 -0
- package/src/server/file.ts +372 -0
- package/src/server/helpers.ts +214 -0
- package/src/server/index.ts +12 -0
- package/src/server/org-crud.ts +307 -0
- package/src/server/org-helpers.ts +54 -0
- package/src/server/org.ts +572 -0
- package/src/server/schema-helpers.ts +107 -0
- package/src/server/setup.ts +138 -0
- package/src/server/test-crud.ts +211 -0
- package/src/server/test.ts +554 -0
- package/src/server/types.ts +392 -0
- package/src/server/unique.ts +28 -0
- package/src/zod.ts +141 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop, no-continue, max-statements, @typescript-eslint/max-params, @typescript-eslint/no-unnecessary-condition */
|
|
2
|
+
// biome-ignore-all lint/performance/noAwaitInLoops: x
|
|
3
|
+
// biome-ignore-all lint/nursery/noContinue: x
|
|
4
|
+
import type { ZodObject, ZodRawShape } from 'zod/v4'
|
|
5
|
+
|
|
6
|
+
import { zid } from 'convex-helpers/server/zod4'
|
|
7
|
+
import { array, number } from 'zod/v4'
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
BaseBuilders,
|
|
11
|
+
DbLike,
|
|
12
|
+
FilterLike,
|
|
13
|
+
IndexLike,
|
|
14
|
+
MutCtx,
|
|
15
|
+
OrgCrudResult,
|
|
16
|
+
OrgEnrichedDoc,
|
|
17
|
+
ReadCtx,
|
|
18
|
+
Rec
|
|
19
|
+
} from './types'
|
|
20
|
+
|
|
21
|
+
import { dbDelete, dbInsert, dbPatch } from './db'
|
|
22
|
+
import { addUrls, cleanFiles, detectFiles, err, log, pgOpts, time } from './helpers'
|
|
23
|
+
import { canEdit, getOrgMember, requireOrgMember, requireOrgRole } from './org-helpers'
|
|
24
|
+
|
|
25
|
+
interface CascadeOption {
|
|
26
|
+
foreignKey: string
|
|
27
|
+
table: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface OrgCrudOptions {
|
|
31
|
+
acl?: boolean
|
|
32
|
+
aclFrom?: { field: string; table: string }
|
|
33
|
+
cascade?: CascadeOption
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getEditors = (doc: Rec): string[] => (doc.editors as string[] | undefined) ?? [],
|
|
37
|
+
requireOrgDoc = (doc: null | Rec, orgId: string, debug?: string): Rec => {
|
|
38
|
+
if (doc?.orgId !== orgId) return err('NOT_FOUND', debug)
|
|
39
|
+
return doc
|
|
40
|
+
},
|
|
41
|
+
resolveAclDoc = async (
|
|
42
|
+
db: DbLike,
|
|
43
|
+
doc: Rec,
|
|
44
|
+
opt?: OrgCrudOptions
|
|
45
|
+
): Promise<{
|
|
46
|
+
editors?: string[]
|
|
47
|
+
userId: string
|
|
48
|
+
}> => {
|
|
49
|
+
if (opt?.aclFrom) {
|
|
50
|
+
const parentId = doc[opt.aclFrom.field] as string,
|
|
51
|
+
parent = parentId ? await db.get(parentId) : null
|
|
52
|
+
return {
|
|
53
|
+
editors: parent ? getEditors(parent) : [],
|
|
54
|
+
userId: doc.userId as string
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return doc as { userId: string }
|
|
58
|
+
},
|
|
59
|
+
makeOrgCrud = <S extends ZodRawShape>({
|
|
60
|
+
builders,
|
|
61
|
+
options: opt,
|
|
62
|
+
schema,
|
|
63
|
+
table
|
|
64
|
+
}: {
|
|
65
|
+
builders: BaseBuilders
|
|
66
|
+
options?: OrgCrudOptions
|
|
67
|
+
schema: ZodObject<S>
|
|
68
|
+
table: string
|
|
69
|
+
}): OrgCrudResult<S> => {
|
|
70
|
+
const { m, q } = builders,
|
|
71
|
+
partial = schema.partial(),
|
|
72
|
+
bulkIdsSchema = array(zid(table)).max(100),
|
|
73
|
+
fileFs = detectFiles(schema.shape),
|
|
74
|
+
idArgs = { id: zid(table) },
|
|
75
|
+
orgIdArg = { orgId: zid('org') },
|
|
76
|
+
useAcl = Boolean(opt?.acl) || Boolean(opt?.aclFrom),
|
|
77
|
+
enrich = async (c: ReadCtx, docs: Rec[]) =>
|
|
78
|
+
// oxlint-disable-next-line promise/prefer-await-to-then
|
|
79
|
+
Promise.all(
|
|
80
|
+
(await c.withAuthor(docs as { userId: string }[])).map(async d =>
|
|
81
|
+
addUrls({ doc: d, fileFields: fileFs, storage: c.storage })
|
|
82
|
+
)
|
|
83
|
+
) as Promise<OrgEnrichedDoc<S>[]>,
|
|
84
|
+
cascadeDelete = async (db: DbLike, id: string) => {
|
|
85
|
+
if (!opt?.cascade) return
|
|
86
|
+
const { foreignKey, table: tbl } = opt.cascade,
|
|
87
|
+
kids = await db
|
|
88
|
+
.query(tbl)
|
|
89
|
+
.filter((f: FilterLike) => f.eq(f.field(foreignKey), id))
|
|
90
|
+
.collect()
|
|
91
|
+
for (const kid of kids) await dbDelete(db, kid._id as string)
|
|
92
|
+
},
|
|
93
|
+
create = m({
|
|
94
|
+
args: { ...orgIdArg, ...schema.shape },
|
|
95
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
96
|
+
const { orgId, ...data } = a as Rec & { orgId: string }
|
|
97
|
+
await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string })
|
|
98
|
+
return dbInsert(c.db, table, { ...data, orgId, userId: c.user._id, ...time() })
|
|
99
|
+
}) as never
|
|
100
|
+
}),
|
|
101
|
+
list = q({
|
|
102
|
+
args: { ...orgIdArg, paginationOpts: pgOpts },
|
|
103
|
+
handler: (async (c: MutCtx & ReadCtx, { orgId, paginationOpts }: { orgId: string; paginationOpts: Rec }) => {
|
|
104
|
+
await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string })
|
|
105
|
+
const { page, ...rest } = await c.db
|
|
106
|
+
.query(table)
|
|
107
|
+
.withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
|
|
108
|
+
.order('desc')
|
|
109
|
+
.paginate(paginationOpts)
|
|
110
|
+
return { ...rest, page: await enrich(c, page) }
|
|
111
|
+
}) as never
|
|
112
|
+
}),
|
|
113
|
+
all = q({
|
|
114
|
+
args: { ...orgIdArg },
|
|
115
|
+
handler: (async (c: MutCtx & ReadCtx, { orgId }: { orgId: string }) => {
|
|
116
|
+
await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string })
|
|
117
|
+
const docs = await c.db
|
|
118
|
+
.query(table)
|
|
119
|
+
.withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
|
|
120
|
+
.order('desc')
|
|
121
|
+
.collect()
|
|
122
|
+
return enrich(c, docs)
|
|
123
|
+
}) as never
|
|
124
|
+
}),
|
|
125
|
+
count = q({
|
|
126
|
+
args: { ...orgIdArg },
|
|
127
|
+
handler: (async (c: MutCtx & ReadCtx, { orgId }: { orgId: string }) => {
|
|
128
|
+
await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string })
|
|
129
|
+
const docs = await c.db
|
|
130
|
+
.query(table)
|
|
131
|
+
.withIndex('by_org', ((o: IndexLike) => o.eq('orgId', orgId)) as never)
|
|
132
|
+
.collect()
|
|
133
|
+
if (docs.length > 1000)
|
|
134
|
+
log('warn', 'crud:large_count', { count: docs.length, table, tip: 'Use indexed queries for large tables' })
|
|
135
|
+
return docs.length
|
|
136
|
+
}) as never
|
|
137
|
+
}),
|
|
138
|
+
read = q({
|
|
139
|
+
args: { ...orgIdArg, ...idArgs },
|
|
140
|
+
handler: (async (c: MutCtx & ReadCtx, { id, orgId }: { id: string; orgId: string }) => {
|
|
141
|
+
await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string })
|
|
142
|
+
const doc = requireOrgDoc(await c.db.get(id), orgId)
|
|
143
|
+
return (await enrich(c, [doc]))[0]
|
|
144
|
+
}) as never
|
|
145
|
+
}),
|
|
146
|
+
update = m({
|
|
147
|
+
args: { ...orgIdArg, ...idArgs, ...partial.shape, expectedUpdatedAt: number().optional() },
|
|
148
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
149
|
+
const { expectedUpdatedAt, id, orgId, ...patch } = a as Rec & {
|
|
150
|
+
expectedUpdatedAt?: number
|
|
151
|
+
id: string
|
|
152
|
+
orgId: string
|
|
153
|
+
},
|
|
154
|
+
{ role } = await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string }),
|
|
155
|
+
doc = requireOrgDoc(await c.db.get(id), orgId),
|
|
156
|
+
aclDoc = await resolveAclDoc(c.db, doc, opt)
|
|
157
|
+
if (
|
|
158
|
+
!canEdit({
|
|
159
|
+
acl: useAcl,
|
|
160
|
+
doc: aclDoc as { editors?: string[]; userId: string },
|
|
161
|
+
role,
|
|
162
|
+
userId: c.user._id as string
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
return err('FORBIDDEN', `${table}:update`)
|
|
166
|
+
if (expectedUpdatedAt !== undefined && doc.updatedAt !== expectedUpdatedAt)
|
|
167
|
+
return err('CONFLICT', `${table}:update`)
|
|
168
|
+
await dbPatch(c.db, id, { ...patch, ...time() })
|
|
169
|
+
return c.db.get(id)
|
|
170
|
+
}) as never
|
|
171
|
+
}),
|
|
172
|
+
rm = m({
|
|
173
|
+
args: { ...orgIdArg, ...idArgs },
|
|
174
|
+
handler: (async (c: MutCtx, { id, orgId }: { id: string; orgId: string }) => {
|
|
175
|
+
const { role } = await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string }),
|
|
176
|
+
doc = requireOrgDoc(await c.db.get(id), orgId),
|
|
177
|
+
aclDoc = await resolveAclDoc(c.db, doc, opt)
|
|
178
|
+
if (
|
|
179
|
+
!canEdit({
|
|
180
|
+
acl: useAcl,
|
|
181
|
+
doc: aclDoc as { editors?: string[]; userId: string },
|
|
182
|
+
role,
|
|
183
|
+
userId: c.user._id as string
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
return err('FORBIDDEN', `${table}:rm`)
|
|
187
|
+
await cascadeDelete(c.db, id)
|
|
188
|
+
await dbDelete(c.db, id)
|
|
189
|
+
await cleanFiles({ doc, fileFields: fileFs, storage: c.storage })
|
|
190
|
+
return doc
|
|
191
|
+
}) as never
|
|
192
|
+
}),
|
|
193
|
+
bulkUpdate = m({
|
|
194
|
+
args: { ...orgIdArg, data: partial, ids: bulkIdsSchema },
|
|
195
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
196
|
+
const { data, ids, orgId } = a as { data: Rec; ids: string[]; orgId: string }
|
|
197
|
+
if (ids.length > 100) return err('LIMIT_EXCEEDED', `${table}:bulkUpdate`)
|
|
198
|
+
await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: c.user._id as string })
|
|
199
|
+
const results: Rec[] = []
|
|
200
|
+
for (const id of ids) {
|
|
201
|
+
const doc = await c.db.get(id)
|
|
202
|
+
if (doc?.orgId !== orgId) continue
|
|
203
|
+
await dbPatch(c.db, id, { ...data, ...time() })
|
|
204
|
+
const updated = await c.db.get(id)
|
|
205
|
+
if (updated) results.push(updated)
|
|
206
|
+
}
|
|
207
|
+
return results
|
|
208
|
+
}) as never
|
|
209
|
+
}),
|
|
210
|
+
bulkRm = m({
|
|
211
|
+
args: { ...orgIdArg, ids: bulkIdsSchema },
|
|
212
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
213
|
+
const { ids, orgId } = a as { ids: string[]; orgId: string }
|
|
214
|
+
if (ids.length > 100) return err('LIMIT_EXCEEDED', `${table}:bulkRm`)
|
|
215
|
+
await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: c.user._id as string })
|
|
216
|
+
let deleted = 0
|
|
217
|
+
for (const id of ids) {
|
|
218
|
+
const doc = await c.db.get(id)
|
|
219
|
+
if (doc?.orgId !== orgId) continue
|
|
220
|
+
await cascadeDelete(c.db, id)
|
|
221
|
+
await dbDelete(c.db, id)
|
|
222
|
+
await cleanFiles({ doc, fileFields: fileFs, storage: c.storage })
|
|
223
|
+
deleted += 1
|
|
224
|
+
}
|
|
225
|
+
return deleted
|
|
226
|
+
}) as never
|
|
227
|
+
}),
|
|
228
|
+
base = { all, bulkRm, bulkUpdate, count, create, list, read, rm, update },
|
|
229
|
+
itemIdKey = `${table}Id` as const,
|
|
230
|
+
itemIdArg = { [itemIdKey]: zid(table) },
|
|
231
|
+
aclArgs = (a: unknown) => {
|
|
232
|
+
const args = a as Rec
|
|
233
|
+
return {
|
|
234
|
+
editorId: args.editorId as string,
|
|
235
|
+
editorIds: args.editorIds as string[] | undefined,
|
|
236
|
+
itemId: args[itemIdKey] as string,
|
|
237
|
+
orgId: args.orgId as string
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
addEditor = m({
|
|
241
|
+
args: { editorId: zid('users'), ...orgIdArg, ...itemIdArg },
|
|
242
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
243
|
+
const { editorId, itemId, orgId } = aclArgs(a)
|
|
244
|
+
await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: c.user._id as string })
|
|
245
|
+
const doc = requireOrgDoc(await c.db.get(itemId), orgId),
|
|
246
|
+
editorIsOwner = (await c.db.get(orgId))?.userId === editorId,
|
|
247
|
+
editorMember = await getOrgMember({ db: c.db, orgId, userId: editorId })
|
|
248
|
+
if (!(editorIsOwner || editorMember)) return err('NOT_ORG_MEMBER')
|
|
249
|
+
const eds = getEditors(doc),
|
|
250
|
+
already = eds.some((eid: string) => eid === editorId)
|
|
251
|
+
if (already) return doc
|
|
252
|
+
if (eds.length >= 100) return err('LIMIT_EXCEEDED')
|
|
253
|
+
await dbPatch(c.db, itemId, { editors: [...eds, editorId], ...time() })
|
|
254
|
+
return c.db.get(itemId)
|
|
255
|
+
}) as never
|
|
256
|
+
}),
|
|
257
|
+
removeEditor = m({
|
|
258
|
+
args: { editorId: zid('users'), ...orgIdArg, ...itemIdArg },
|
|
259
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
260
|
+
const { editorId, itemId, orgId } = aclArgs(a)
|
|
261
|
+
await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: c.user._id as string })
|
|
262
|
+
const doc = requireOrgDoc(await c.db.get(itemId), orgId),
|
|
263
|
+
eds = getEditors(doc),
|
|
264
|
+
filtered = eds.filter((eid: string) => eid !== editorId)
|
|
265
|
+
await dbPatch(c.db, itemId, { editors: filtered, ...time() })
|
|
266
|
+
return c.db.get(itemId)
|
|
267
|
+
}) as never
|
|
268
|
+
}),
|
|
269
|
+
editors = q({
|
|
270
|
+
args: { ...orgIdArg, ...itemIdArg },
|
|
271
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
272
|
+
const { itemId, orgId } = aclArgs(a)
|
|
273
|
+
await requireOrgMember({ db: c.db, orgId, userId: c.user._id as string })
|
|
274
|
+
const doc = requireOrgDoc(await c.db.get(itemId), orgId),
|
|
275
|
+
editorIds = getEditors(doc),
|
|
276
|
+
users = await Promise.all(editorIds.map(async (eid: string) => c.db.get(eid))),
|
|
277
|
+
result: { email: string; name: string; userId: string }[] = []
|
|
278
|
+
for (let i = 0; i < editorIds.length; i += 1) {
|
|
279
|
+
const u = users[i] as null | Rec,
|
|
280
|
+
eid = editorIds[i]
|
|
281
|
+
if (u && eid) result.push({ email: (u.email as string) ?? '', name: (u.name as string) ?? '', userId: eid })
|
|
282
|
+
}
|
|
283
|
+
return result
|
|
284
|
+
}) as never
|
|
285
|
+
}),
|
|
286
|
+
setEditors = m({
|
|
287
|
+
args: { editorIds: array(zid('users')).max(100), ...orgIdArg, ...itemIdArg },
|
|
288
|
+
handler: (async (c: MutCtx, a: Rec) => {
|
|
289
|
+
const { editorIds, itemId, orgId } = aclArgs(a)
|
|
290
|
+
await requireOrgRole({ db: c.db, minRole: 'admin', orgId, userId: c.user._id as string })
|
|
291
|
+
requireOrgDoc(await c.db.get(itemId), orgId)
|
|
292
|
+
if (editorIds)
|
|
293
|
+
for (const editorId of editorIds) {
|
|
294
|
+
const isOwner = (await c.db.get(orgId))?.userId === editorId,
|
|
295
|
+
member = await getOrgMember({ db: c.db, orgId, userId: editorId })
|
|
296
|
+
if (!(isOwner || member)) return err('NOT_ORG_MEMBER')
|
|
297
|
+
}
|
|
298
|
+
await dbPatch(c.db, itemId, { editors: editorIds ?? [], ...time() })
|
|
299
|
+
return c.db.get(itemId)
|
|
300
|
+
}) as never
|
|
301
|
+
})
|
|
302
|
+
return { ...base, addEditor, editors, removeEditor, setEditors } as unknown as OrgCrudResult<S>
|
|
303
|
+
},
|
|
304
|
+
orgCascade = (config: { foreignKey: string; table: string }): CascadeOption => config
|
|
305
|
+
|
|
306
|
+
export type { CascadeOption, OrgCrudOptions }
|
|
307
|
+
export { makeOrgCrud, orgCascade }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CanEditOpts, DbReadLike, FilterLike, IndexLike, OrgRole } from './types'
|
|
2
|
+
|
|
3
|
+
import { err } from './helpers'
|
|
4
|
+
|
|
5
|
+
const ROLE_LEVEL: Record<OrgRole, number> = { admin: 2, member: 1, owner: 3 },
|
|
6
|
+
getOrgRole = ({
|
|
7
|
+
member,
|
|
8
|
+
org,
|
|
9
|
+
userId
|
|
10
|
+
}: {
|
|
11
|
+
member: null | Record<string, unknown>
|
|
12
|
+
org: Record<string, unknown>
|
|
13
|
+
userId: string
|
|
14
|
+
}): null | OrgRole => {
|
|
15
|
+
if (org.userId === userId) return 'owner'
|
|
16
|
+
if (!member) return null
|
|
17
|
+
return (member as { isAdmin?: boolean }).isAdmin ? 'admin' : 'member'
|
|
18
|
+
},
|
|
19
|
+
getOrgMember = async ({ db, orgId, userId }: { db: unknown; orgId: string; userId: string }) =>
|
|
20
|
+
(db as DbReadLike)
|
|
21
|
+
.query('orgMember')
|
|
22
|
+
.withIndex('by_org_user', ((q: IndexLike) => q.eq('orgId', orgId)) as never)
|
|
23
|
+
.filter((f: FilterLike) => f.eq(f.field('userId'), userId))
|
|
24
|
+
.unique() as Promise<null | Record<string, unknown>>,
|
|
25
|
+
requireOrgMember = async ({ db, orgId, userId }: { db: unknown; orgId: string; userId: string }) => {
|
|
26
|
+
const dbl = db as DbReadLike,
|
|
27
|
+
org = await dbl.get(orgId)
|
|
28
|
+
if (!org) return err('NOT_FOUND')
|
|
29
|
+
const member = await getOrgMember({ db, orgId, userId }),
|
|
30
|
+
role = getOrgRole({ member, org, userId })
|
|
31
|
+
if (!role) return err('NOT_ORG_MEMBER')
|
|
32
|
+
return { member, org, role }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RequireOrgRoleArgs {
|
|
36
|
+
db: unknown
|
|
37
|
+
minRole: OrgRole
|
|
38
|
+
orgId: string
|
|
39
|
+
userId: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const requireOrgRole = async ({ db, minRole, orgId, userId }: RequireOrgRoleArgs) => {
|
|
43
|
+
const result = await requireOrgMember({ db, orgId, userId })
|
|
44
|
+
if (ROLE_LEVEL[result.role] < ROLE_LEVEL[minRole]) return err('INSUFFICIENT_ORG_ROLE')
|
|
45
|
+
return result
|
|
46
|
+
},
|
|
47
|
+
canEdit = ({ acl, doc, role, userId }: CanEditOpts): boolean => {
|
|
48
|
+
if (role === 'owner' || role === 'admin') return true
|
|
49
|
+
if (doc.userId === userId) return true
|
|
50
|
+
if (acl && doc.editors?.includes(userId)) return true
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { canEdit, getOrgMember, getOrgRole, requireOrgMember, requireOrgRole, ROLE_LEVEL }
|