@robelest/convex-auth 0.0.4-preview.32 → 0.0.4-preview.34
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/dist/component/_generated/component.d.ts +1611 -2039
- package/dist/component/account.js +3 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/factor/device.js +3 -0
- package/dist/component/factor/passkey.js +3 -0
- package/dist/component/factor/totp.js +3 -0
- package/dist/component/group/invite.js +3 -0
- package/dist/component/group/member.js +3 -0
- package/dist/component/group.js +3 -0
- package/dist/component/model.d.ts +342 -30
- package/dist/component/model.js +22 -4
- package/dist/component/modules.js +24 -21
- package/dist/component/public/factors/devices.js +37 -106
- package/dist/component/public/factors/passkeys.js +29 -149
- package/dist/component/public/factors/totp.js +32 -159
- package/dist/component/public/groups/core.js +19 -82
- package/dist/component/public/groups/invites.js +15 -104
- package/dist/component/public/groups/members.js +26 -149
- package/dist/component/public/identity/accounts.js +12 -94
- package/dist/component/public/identity/codes.js +13 -73
- package/dist/component/public/identity/sessions.js +5 -107
- package/dist/component/public/identity/tokens.js +13 -103
- package/dist/component/public/identity/users.js +188 -185
- package/dist/component/public/identity/verifiers.js +17 -80
- package/dist/component/public/security/keys.js +13 -120
- package/dist/component/public/security/limits.js +0 -43
- package/dist/component/public/sso/audit.js +0 -28
- package/dist/component/public/sso/core.js +31 -104
- package/dist/component/public/sso/domains.js +0 -71
- package/dist/component/public/sso/scim.js +63 -239
- package/dist/component/public/sso/secrets.js +0 -30
- package/dist/component/public/sso/webhooks.js +23 -128
- package/dist/component/rateLimit.js +3 -0
- package/dist/component/schema.d.ts +378 -342
- package/dist/component/schema.js +11 -1
- package/dist/component/session.js +3 -0
- package/dist/component/sso/audit.js +3 -0
- package/dist/component/sso/connection/domain/verification.js +3 -0
- package/dist/component/sso/connection/domain.js +3 -0
- package/dist/component/sso/connection/scim/config.js +3 -0
- package/dist/component/sso/connection/scim/identity.js +3 -0
- package/dist/component/sso/connection/secret.js +3 -0
- package/dist/component/sso/connection.js +3 -0
- package/dist/component/sso/webhook/delivery.js +3 -0
- package/dist/component/sso/webhook/endpoint.js +3 -0
- package/dist/component/token/pkce.js +3 -0
- package/dist/component/token/refresh.js +3 -0
- package/dist/component/token/verification.js +3 -0
- package/dist/component/user/email.js +3 -0
- package/dist/component/user/key.js +3 -0
- package/dist/component/user.js +62 -0
- package/dist/core/index.d.ts +131 -28
- package/dist/core/index.js +2 -0
- package/dist/model.js +391 -0
- package/dist/providers/credentials.d.ts +1 -1
- package/dist/providers/github.js +6 -0
- package/dist/providers/password.js +1 -2
- package/dist/server/auth.d.ts +73 -7
- package/dist/server/auth.js +4 -1
- package/dist/server/context.js +30 -3
- package/dist/server/contract.js +42 -42
- package/dist/server/core.js +224 -86
- package/dist/server/db.js +45 -37
- package/dist/server/facade.d.ts +39 -0
- package/dist/server/facade.js +16 -0
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -1
- package/dist/server/mounts.d.ts +101 -101
- package/dist/server/mutations/credentials/signin.js +3 -7
- package/dist/server/mutations/oauth.js +9 -6
- package/dist/server/runtime.d.ts +147 -46
- package/dist/server/runtime.js +10 -8
- package/dist/server/services/group.js +9 -9
- package/dist/server/sso/domain.d.ts +1 -1
- package/dist/server/sso/domain.js +40 -40
- package/dist/server/sso/http.js +18 -18
- package/dist/server/sso/oidc.js +1 -1
- package/dist/server/sso/policies.js +3 -3
- package/dist/server/sso/policy.js +12 -4
- package/dist/server/sso/provision.js +9 -9
- package/dist/server/sso/validators.js +2 -2
- package/dist/server/sso/webhook.js +8 -8
- package/dist/server/types.d.ts +185 -124
- package/dist/server/types.js +29 -24
- package/dist/server/users.js +49 -2
- package/dist/server/validators.d.ts +745 -0
- package/dist/server/validators.js +60 -0
- package/package.json +1 -1
- package/dist/component/public.js +0 -22
|
@@ -39,15 +39,6 @@ function normalizeTags(tags) {
|
|
|
39
39
|
* @param args.extend - An optional arbitrary payload for application-specific metadata.
|
|
40
40
|
* @returns The `Id<"Group">` of the newly created group document.
|
|
41
41
|
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```ts
|
|
44
|
-
* const groupId = await ctx.runMutation(components.auth.groups.groupCreate, {
|
|
45
|
-
* name: "Acme Corp",
|
|
46
|
-
* slug: "acme-corp",
|
|
47
|
-
* type: "organization",
|
|
48
|
-
* tags: [{ key: "plan", value: "group-sso" }],
|
|
49
|
-
* });
|
|
50
|
-
* ```
|
|
51
42
|
*/
|
|
52
43
|
const groupCreate = mutation({
|
|
53
44
|
args: {
|
|
@@ -81,52 +72,26 @@ const groupCreate = mutation({
|
|
|
81
72
|
}
|
|
82
73
|
});
|
|
83
74
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* document, or `null` if no group exists with the given ID.
|
|
88
|
-
*
|
|
89
|
-
* @param args.groupId - The `Id<"Group">` of the group to retrieve.
|
|
90
|
-
* @returns The group document (including `name`, `slug`, `type`, `tags`, hierarchy fields, etc.) or `null` if not found.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```ts
|
|
94
|
-
* const group = await ctx.runQuery(components.auth.groups.groupGet, {
|
|
95
|
-
* groupId: existingGroupId,
|
|
96
|
-
* });
|
|
97
|
-
* if (group !== null) {
|
|
98
|
-
* console.log(group.name, group.slug);
|
|
99
|
-
* }
|
|
100
|
-
* ```
|
|
75
|
+
* Read a group by identity — one function, all-optional args, unioned
|
|
76
|
+
* return: `{ id }` → `Doc<"Group"> | null`, or `{ ids }` → ordered
|
|
77
|
+
* `(Doc<"Group"> | null)[]` (deduped).
|
|
101
78
|
*/
|
|
102
79
|
const groupGet = query({
|
|
103
|
-
args: {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
* @param args.groupIds - Array of group document IDs.
|
|
119
|
-
* @returns Array of group documents (or `null` entries) in input order.
|
|
120
|
-
*/
|
|
121
|
-
const groupGetMany = query({
|
|
122
|
-
args: { groupIds: v.array(v.id("Group")) },
|
|
123
|
-
returns: v.array(v.union(vGroupDoc, v.null())),
|
|
124
|
-
handler: async (ctx, { groupIds }) => {
|
|
125
|
-
if (groupIds.length === 0) return [];
|
|
126
|
-
const unique = Array.from(new Set(groupIds));
|
|
127
|
-
const docs = await Promise.all(unique.map((id) => ctx.db.get("Group", id)));
|
|
128
|
-
const byId = new Map(unique.map((id, i) => [id, docs[i] ?? null]));
|
|
129
|
-
return groupIds.map((id) => byId.get(id) ?? null);
|
|
80
|
+
args: {
|
|
81
|
+
id: v.optional(v.id("Group")),
|
|
82
|
+
ids: v.optional(v.array(v.id("Group")))
|
|
83
|
+
},
|
|
84
|
+
returns: v.union(vGroupDoc, v.null(), v.array(v.union(vGroupDoc, v.null()))),
|
|
85
|
+
handler: async (ctx, args) => {
|
|
86
|
+
if (args.ids !== void 0) {
|
|
87
|
+
if (args.ids.length === 0) return [];
|
|
88
|
+
const unique = Array.from(new Set(args.ids));
|
|
89
|
+
const docs = await Promise.all(unique.map((id) => ctx.db.get("Group", id)));
|
|
90
|
+
const byId = new Map(unique.map((id, i) => [id, docs[i] ?? null]));
|
|
91
|
+
return args.ids.map((id) => byId.get(id) ?? null);
|
|
92
|
+
}
|
|
93
|
+
if (args.id === void 0) return null;
|
|
94
|
+
return await ctx.db.get("Group", args.id);
|
|
130
95
|
}
|
|
131
96
|
});
|
|
132
97
|
/**
|
|
@@ -216,17 +181,6 @@ const groupAncestors = query({
|
|
|
216
181
|
* @param args.order - Sort direction: `"asc"` or `"desc"` (defaults to `"desc"`).
|
|
217
182
|
* @returns An object `{ items, nextCursor }` where `items` is an array of group documents and `nextCursor` is `null` when there are no more pages.
|
|
218
183
|
*
|
|
219
|
-
* @example
|
|
220
|
-
* ```ts
|
|
221
|
-
* const { items, nextCursor } = await ctx.runQuery(
|
|
222
|
-
* components.auth.groups.groupList,
|
|
223
|
-
* {
|
|
224
|
-
* where: { type: "team", isRoot: false },
|
|
225
|
-
* limit: 20,
|
|
226
|
-
* order: "asc",
|
|
227
|
-
* },
|
|
228
|
-
* );
|
|
229
|
-
* ```
|
|
230
184
|
*/
|
|
231
185
|
const groupList = query({
|
|
232
186
|
args: {
|
|
@@ -314,16 +268,6 @@ const groupList = query({
|
|
|
314
268
|
* @param args.data - A partial object of fields to patch. Supported keys include `name`, `slug`, `type`, `parentGroupId`, `tags`, and `extend`.
|
|
315
269
|
* @returns `null` on success.
|
|
316
270
|
*
|
|
317
|
-
* @example
|
|
318
|
-
* ```ts
|
|
319
|
-
* await ctx.runMutation(components.auth.groups.groupUpdate, {
|
|
320
|
-
* groupId: existingGroupId,
|
|
321
|
-
* data: {
|
|
322
|
-
* name: "Acme Corp (renamed)",
|
|
323
|
-
* tags: [{ key: "plan", value: "pro" }],
|
|
324
|
-
* },
|
|
325
|
-
* });
|
|
326
|
-
* ```
|
|
327
271
|
*/
|
|
328
272
|
const groupUpdate = mutation({
|
|
329
273
|
args: {
|
|
@@ -376,13 +320,6 @@ const groupUpdate = mutation({
|
|
|
376
320
|
* @param args.groupId - The `Id<"Group">` of the group to delete. All children are deleted recursively.
|
|
377
321
|
* @returns `null` on success.
|
|
378
322
|
*
|
|
379
|
-
* @example
|
|
380
|
-
* ```ts
|
|
381
|
-
* // Delete an organization and everything nested under it
|
|
382
|
-
* await ctx.runMutation(components.auth.groups.groupDelete, {
|
|
383
|
-
* groupId: organizationGroupId,
|
|
384
|
-
* });
|
|
385
|
-
* ```
|
|
386
323
|
*/
|
|
387
324
|
const groupDelete = mutation({
|
|
388
325
|
args: { groupId: v.id("Group") },
|
|
@@ -405,5 +342,5 @@ const groupDelete = mutation({
|
|
|
405
342
|
});
|
|
406
343
|
|
|
407
344
|
//#endregion
|
|
408
|
-
export { groupAncestors, groupCreate, groupDelete, groupGet,
|
|
345
|
+
export { groupAncestors, groupCreate, groupDelete, groupGet, groupList, groupUpdate };
|
|
409
346
|
//# sourceMappingURL=core.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { vGroupInviteDoc,
|
|
1
|
+
import { vGroupInviteDoc, vInviteRedeemResult, vInviteStatus, vPaginated } from "../../model.js";
|
|
2
2
|
import { mutation, query } from "../../functions.js";
|
|
3
3
|
import { ConvexError, v } from "convex/values";
|
|
4
4
|
|
|
@@ -28,21 +28,6 @@ import { ConvexError, v } from "convex/values";
|
|
|
28
28
|
* @param args.extend - Optional arbitrary payload for application-specific metadata.
|
|
29
29
|
* @returns The `Id<"GroupInvite">` of the newly created invite document.
|
|
30
30
|
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```ts
|
|
33
|
-
* const inviteId = await ctx.runMutation(
|
|
34
|
-
* components.auth.groups.inviteCreate,
|
|
35
|
-
* {
|
|
36
|
-
* groupId: teamGroupId,
|
|
37
|
-
* invitedByUserId: currentUserId,
|
|
38
|
-
* email: "alice@example.com",
|
|
39
|
-
* tokenHash: hashedToken,
|
|
40
|
-
* roleIds: ["editor"],
|
|
41
|
-
* status: "pending",
|
|
42
|
-
* expiresTime: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
43
|
-
* },
|
|
44
|
-
* );
|
|
45
|
-
* ```
|
|
46
31
|
*/
|
|
47
32
|
const inviteCreate = mutation({
|
|
48
33
|
args: {
|
|
@@ -92,57 +77,20 @@ const inviteCreate = mutation({
|
|
|
92
77
|
}
|
|
93
78
|
});
|
|
94
79
|
/**
|
|
95
|
-
*
|
|
80
|
+
* Read an invite by identity — one function, all-optional args, unioned
|
|
81
|
+
* return: `{ id }` (point lookup) or `{ tokenHash }` (token index).
|
|
96
82
|
*
|
|
97
|
-
* Performs a direct lookup in the `GroupInvite` table and returns the full
|
|
98
|
-
* invite document, or `null` if no invite exists with the given ID.
|
|
99
|
-
*
|
|
100
|
-
* @param args.inviteId - The `Id<"GroupInvite">` of the invite to retrieve.
|
|
101
|
-
* @returns The invite document (including `email`, `status`, `groupId`, `tokenHash`, etc.) or `null` if not found.
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* ```ts
|
|
105
|
-
* const invite = await ctx.runQuery(components.auth.groups.inviteGet, {
|
|
106
|
-
* inviteId: existingInviteId,
|
|
107
|
-
* });
|
|
108
|
-
* if (invite !== null) {
|
|
109
|
-
* console.log(invite.email, invite.status);
|
|
110
|
-
* }
|
|
111
|
-
* ```
|
|
112
83
|
*/
|
|
113
84
|
const inviteGet = query({
|
|
114
|
-
args: {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
/**
|
|
121
|
-
* Retrieve an invite by its hashed token.
|
|
122
|
-
*
|
|
123
|
-
* Looks up the `GroupInvite` table using the `token_hash` index. This is
|
|
124
|
-
* the primary mechanism for resolving an invite from a URL-embedded token.
|
|
125
|
-
* Returns `null` if no invite matches the given hash.
|
|
126
|
-
*
|
|
127
|
-
* @param args.tokenHash - The hashed token string to look up (must match the value stored at creation time).
|
|
128
|
-
* @returns The invite document or `null` if no invite exists with the given token hash.
|
|
129
|
-
*
|
|
130
|
-
* @example
|
|
131
|
-
* ```ts
|
|
132
|
-
* const invite = await ctx.runQuery(
|
|
133
|
-
* components.auth.groups.inviteGetByTokenHash,
|
|
134
|
-
* { tokenHash: "sha256_abc123..." },
|
|
135
|
-
* );
|
|
136
|
-
* if (invite !== null && invite.status === "pending") {
|
|
137
|
-
* // proceed with acceptance flow
|
|
138
|
-
* }
|
|
139
|
-
* ```
|
|
140
|
-
*/
|
|
141
|
-
const inviteGetByTokenHash = query({
|
|
142
|
-
args: { tokenHash: v.string() },
|
|
85
|
+
args: {
|
|
86
|
+
id: v.optional(v.id("GroupInvite")),
|
|
87
|
+
tokenHash: v.optional(v.string())
|
|
88
|
+
},
|
|
143
89
|
returns: v.union(vGroupInviteDoc, v.null()),
|
|
144
|
-
handler: async (ctx,
|
|
145
|
-
return await ctx.db.query("GroupInvite").withIndex("token_hash", (q) => q.eq("tokenHash", tokenHash)).first();
|
|
90
|
+
handler: async (ctx, args) => {
|
|
91
|
+
if (args.tokenHash !== void 0) return await ctx.db.query("GroupInvite").withIndex("token_hash", (q) => q.eq("tokenHash", args.tokenHash)).first();
|
|
92
|
+
if (args.id === void 0) return null;
|
|
93
|
+
return await ctx.db.get("GroupInvite", args.id);
|
|
146
94
|
}
|
|
147
95
|
});
|
|
148
96
|
/**
|
|
@@ -169,17 +117,6 @@ const inviteGetByTokenHash = query({
|
|
|
169
117
|
* @param args.order - Sort direction: `"asc"` or `"desc"` (defaults to `"desc"`).
|
|
170
118
|
* @returns An object `{ items, nextCursor }` where `items` is an array of invite documents and `nextCursor` is `null` when there are no more pages.
|
|
171
119
|
*
|
|
172
|
-
* @example
|
|
173
|
-
* ```ts
|
|
174
|
-
* const { items, nextCursor } = await ctx.runQuery(
|
|
175
|
-
* components.auth.groups.inviteList,
|
|
176
|
-
* {
|
|
177
|
-
* where: { groupId: teamGroupId, status: "pending" },
|
|
178
|
-
* limit: 25,
|
|
179
|
-
* order: "desc",
|
|
180
|
-
* },
|
|
181
|
-
* );
|
|
182
|
-
* ```
|
|
183
120
|
*/
|
|
184
121
|
const inviteList = query({
|
|
185
122
|
args: {
|
|
@@ -242,7 +179,7 @@ const inviteList = query({
|
|
|
242
179
|
* automatically transitioned to `"expired"` and the acceptance is rejected.
|
|
243
180
|
*
|
|
244
181
|
* The caller is responsible for creating the corresponding member record
|
|
245
|
-
* (see {@link
|
|
182
|
+
* (see {@link inviteRedeem} for an all-in-one alternative that also
|
|
246
183
|
* handles membership).
|
|
247
184
|
*
|
|
248
185
|
* @param args.inviteId - The `Id<"GroupInvite">` of the invite to accept.
|
|
@@ -252,13 +189,6 @@ const inviteList = query({
|
|
|
252
189
|
* @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has already been accepted, revoked, or otherwise finalized.
|
|
253
190
|
* @throws `ConvexError` with code `INVITE_EXPIRED` if the invite's `expiresTime` has passed.
|
|
254
191
|
*
|
|
255
|
-
* @example
|
|
256
|
-
* ```ts
|
|
257
|
-
* await ctx.runMutation(components.auth.groups.inviteAccept, {
|
|
258
|
-
* inviteId: pendingInviteId,
|
|
259
|
-
* acceptedByUserId: currentUserId,
|
|
260
|
-
* });
|
|
261
|
-
* ```
|
|
262
192
|
*/
|
|
263
193
|
const inviteAccept = mutation({
|
|
264
194
|
args: {
|
|
@@ -321,26 +251,13 @@ const inviteAccept = mutation({
|
|
|
321
251
|
* @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has been revoked or is in another non-pending state.
|
|
322
252
|
* @throws `ConvexError` with code `INVITE_EMAIL_MISMATCH` if the accepting user's email does not match the invite's email.
|
|
323
253
|
*
|
|
324
|
-
* @example
|
|
325
|
-
* ```ts
|
|
326
|
-
* const result = await ctx.runMutation(
|
|
327
|
-
* components.auth.groups.inviteAcceptByToken,
|
|
328
|
-
* {
|
|
329
|
-
* tokenHash: "sha256_abc123...",
|
|
330
|
-
* acceptedByUserId: currentUserId,
|
|
331
|
-
* },
|
|
332
|
-
* );
|
|
333
|
-
* if (result.membershipStatus === "joined") {
|
|
334
|
-
* console.log("Joined group", result.groupId, "as member", result.memberId);
|
|
335
|
-
* }
|
|
336
|
-
* ```
|
|
337
254
|
*/
|
|
338
|
-
const
|
|
255
|
+
const inviteRedeem = mutation({
|
|
339
256
|
args: {
|
|
340
257
|
tokenHash: v.string(),
|
|
341
258
|
acceptedByUserId: v.id("User")
|
|
342
259
|
},
|
|
343
|
-
returns:
|
|
260
|
+
returns: vInviteRedeemResult,
|
|
344
261
|
handler: async (ctx, { tokenHash, acceptedByUserId }) => {
|
|
345
262
|
const invite = await ctx.db.query("GroupInvite").withIndex("token_hash", (q) => q.eq("tokenHash", tokenHash)).first();
|
|
346
263
|
if (invite === null) throw new ConvexError({
|
|
@@ -424,12 +341,6 @@ const inviteAcceptByToken = mutation({
|
|
|
424
341
|
* @throws `ConvexError` with code `INVITE_NOT_FOUND` if the invite does not exist.
|
|
425
342
|
* @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has already been accepted, revoked, or expired.
|
|
426
343
|
*
|
|
427
|
-
* @example
|
|
428
|
-
* ```ts
|
|
429
|
-
* await ctx.runMutation(components.auth.groups.inviteRevoke, {
|
|
430
|
-
* inviteId: pendingInviteId,
|
|
431
|
-
* });
|
|
432
|
-
* ```
|
|
433
344
|
*/
|
|
434
345
|
const inviteRevoke = mutation({
|
|
435
346
|
args: { inviteId: v.id("GroupInvite") },
|
|
@@ -453,5 +364,5 @@ const inviteRevoke = mutation({
|
|
|
453
364
|
});
|
|
454
365
|
|
|
455
366
|
//#endregion
|
|
456
|
-
export { inviteAccept,
|
|
367
|
+
export { inviteAccept, inviteCreate, inviteGet, inviteList, inviteRedeem, inviteRevoke };
|
|
457
368
|
//# sourceMappingURL=invites.js.map
|
|
@@ -22,18 +22,6 @@ import { ConvexError, v } from "convex/values";
|
|
|
22
22
|
* @returns The `Id<"GroupMember">` of the newly created member document.
|
|
23
23
|
* @throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` if the user is already a member of this group.
|
|
24
24
|
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* const memberId = await ctx.runMutation(
|
|
28
|
-
* components.auth.groups.memberAdd,
|
|
29
|
-
* {
|
|
30
|
-
* groupId: teamGroupId,
|
|
31
|
-
* userId: newUserId,
|
|
32
|
-
* roleIds: ["viewer"],
|
|
33
|
-
* status: "active",
|
|
34
|
-
* },
|
|
35
|
-
* );
|
|
36
|
-
* ```
|
|
37
25
|
*/
|
|
38
26
|
const memberAdd = mutation({
|
|
39
27
|
args: {
|
|
@@ -57,29 +45,33 @@ const memberAdd = mutation({
|
|
|
57
45
|
}
|
|
58
46
|
});
|
|
59
47
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @returns The member document (including `groupId`, `userId`, `roleIds`, `status`, etc.) or `null` if not found.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```ts
|
|
70
|
-
* const member = await ctx.runQuery(components.auth.groups.memberGet, {
|
|
71
|
-
* memberId: existingMemberId,
|
|
72
|
-
* });
|
|
73
|
-
* if (member !== null) {
|
|
74
|
-
* console.log(member.userId, member.roleIds);
|
|
75
|
-
* }
|
|
76
|
-
* ```
|
|
48
|
+
* Read a membership by identity. Accepts exactly one selector:
|
|
49
|
+
* - `{ id }` — point lookup → `Doc | null`.
|
|
50
|
+
* - `{ groupId, userId }` — unique per group+user → `Doc | null`.
|
|
51
|
+
* - `{ userId, groupIds }` — batch: resolve this user's membership in
|
|
52
|
+
* each group, returning `(Doc | null)[]` aligned to `groupIds` order
|
|
53
|
+
* (duplicates de-duplicated internally).
|
|
77
54
|
*/
|
|
78
55
|
const memberGet = query({
|
|
79
|
-
args: {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
56
|
+
args: {
|
|
57
|
+
id: v.optional(v.id("GroupMember")),
|
|
58
|
+
groupId: v.optional(v.id("Group")),
|
|
59
|
+
userId: v.optional(v.id("User")),
|
|
60
|
+
groupIds: v.optional(v.array(v.id("Group")))
|
|
61
|
+
},
|
|
62
|
+
returns: v.union(vGroupMemberDoc, v.null(), v.array(v.union(vGroupMemberDoc, v.null()))),
|
|
63
|
+
handler: async (ctx, args) => {
|
|
64
|
+
if (args.userId !== void 0 && args.groupIds !== void 0) {
|
|
65
|
+
const groupIds = args.groupIds;
|
|
66
|
+
if (groupIds.length === 0) return [];
|
|
67
|
+
const unique = Array.from(new Set(groupIds));
|
|
68
|
+
const docs = await Promise.all(unique.map((groupId) => ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", groupId).eq("userId", args.userId)).unique()));
|
|
69
|
+
const byGroupId = new Map(unique.map((id, i) => [id, docs[i] ?? null]));
|
|
70
|
+
return groupIds.map((id) => byGroupId.get(id) ?? null);
|
|
71
|
+
}
|
|
72
|
+
if (args.groupId !== void 0 && args.userId !== void 0) return await ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", args.groupId).eq("userId", args.userId)).unique();
|
|
73
|
+
if (args.id === void 0) return null;
|
|
74
|
+
return await ctx.db.get("GroupMember", args.id);
|
|
83
75
|
}
|
|
84
76
|
});
|
|
85
77
|
/**
|
|
@@ -102,17 +94,6 @@ const memberGet = query({
|
|
|
102
94
|
* @param args.order - Sort direction: `"asc"` or `"desc"` (defaults to `"desc"`).
|
|
103
95
|
* @returns An object `{ items, nextCursor }` where `items` is an array of member documents and `nextCursor` is `null` when there are no more pages.
|
|
104
96
|
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```ts
|
|
107
|
-
* const { items, nextCursor } = await ctx.runQuery(
|
|
108
|
-
* components.auth.groups.memberList,
|
|
109
|
-
* {
|
|
110
|
-
* where: { groupId: teamGroupId, status: "active" },
|
|
111
|
-
* limit: 30,
|
|
112
|
-
* order: "asc",
|
|
113
|
-
* },
|
|
114
|
-
* );
|
|
115
|
-
* ```
|
|
116
97
|
*/
|
|
117
98
|
const memberList = query({
|
|
118
99
|
args: {
|
|
@@ -163,77 +144,6 @@ const memberList = query({
|
|
|
163
144
|
}
|
|
164
145
|
});
|
|
165
146
|
/**
|
|
166
|
-
* Look up a specific user's membership in a specific group.
|
|
167
|
-
*
|
|
168
|
-
* Uses the `group_id_user_id` compound index for an efficient exact-match
|
|
169
|
-
* lookup. Returns `null` if the user is not a member of the group. Unlike
|
|
170
|
-
* {@link memberResolve}, this does **not** walk the group hierarchy — it
|
|
171
|
-
* checks only the specified group.
|
|
172
|
-
*
|
|
173
|
-
* @param args.groupId - The `Id<"Group">` of the group to check.
|
|
174
|
-
* @param args.userId - The `Id<"User">` of the user whose membership to look up.
|
|
175
|
-
* @returns The member document or `null` if the user is not a direct member of the group.
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* ```ts
|
|
179
|
-
* const member = await ctx.runQuery(
|
|
180
|
-
* components.auth.groups.memberGetByGroupAndUser,
|
|
181
|
-
* { groupId: teamGroupId, userId: currentUserId },
|
|
182
|
-
* );
|
|
183
|
-
* if (member !== null) {
|
|
184
|
-
* console.log("User has roles:", member.roleIds);
|
|
185
|
-
* }
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
|
-
const memberGetByGroupAndUser = query({
|
|
189
|
-
args: {
|
|
190
|
-
groupId: v.id("Group"),
|
|
191
|
-
userId: v.id("User")
|
|
192
|
-
},
|
|
193
|
-
returns: v.union(vGroupMemberDoc, v.null()),
|
|
194
|
-
handler: async (ctx, { groupId, userId }) => {
|
|
195
|
-
return await ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", groupId).eq("userId", userId)).unique();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
/**
|
|
199
|
-
* Batched equivalent of {@link memberGetByGroupAndUser}. Resolves many
|
|
200
|
-
* `(groupId, userId)` pairs for the same user in a single component
|
|
201
|
-
* round-trip. Used by app-side handlers (e.g. `groups:getDashboard`) that
|
|
202
|
-
* need to inspect the current user's membership across every root group
|
|
203
|
-
* they can see.
|
|
204
|
-
*
|
|
205
|
-
* Each `groupId` is resolved using the `group_id_user_id` index; missing
|
|
206
|
-
* memberships come back as `null` in the same slot order as the input.
|
|
207
|
-
*
|
|
208
|
-
* @param args.userId - The user whose memberships to look up.
|
|
209
|
-
* @param args.groupIds - One or more groups to resolve. Order is preserved;
|
|
210
|
-
* duplicates tolerated (but de-duplicated internally so the DB only sees
|
|
211
|
-
* each pair once).
|
|
212
|
-
* @returns Array of member documents or `null` entries, in `groupIds` order.
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* ```ts
|
|
216
|
-
* const members = await ctx.runQuery(
|
|
217
|
-
* components.auth.groups.memberGetByGroupAndUserMany,
|
|
218
|
-
* { userId: viewerId, groupIds: rootGroupIds },
|
|
219
|
-
* );
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
const memberGetByGroupAndUserMany = query({
|
|
223
|
-
args: {
|
|
224
|
-
userId: v.id("User"),
|
|
225
|
-
groupIds: v.array(v.id("Group"))
|
|
226
|
-
},
|
|
227
|
-
returns: v.array(v.union(vGroupMemberDoc, v.null())),
|
|
228
|
-
handler: async (ctx, { userId, groupIds }) => {
|
|
229
|
-
if (groupIds.length === 0) return [];
|
|
230
|
-
const unique = Array.from(new Set(groupIds));
|
|
231
|
-
const docs = await Promise.all(unique.map((groupId) => ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", groupId).eq("userId", userId)).unique()));
|
|
232
|
-
const byGroupId = new Map(unique.map((id, i) => [id, docs[i] ?? null]));
|
|
233
|
-
return groupIds.map((id) => byGroupId.get(id) ?? null);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
/**
|
|
237
147
|
* Resolve a user's membership by walking the group hierarchy from the
|
|
238
148
|
* requested group up to the root. Returns the first matching membership
|
|
239
149
|
* found, enabling inherited (ancestor-level) access checks.
|
|
@@ -261,23 +171,6 @@ const memberGetByGroupAndUserMany = query({
|
|
|
261
171
|
* - `isInherited` — `true` when `depth > 0`.
|
|
262
172
|
* - `traversedGroupIds` — (only when `ancestry` is `true`) array of group IDs visited.
|
|
263
173
|
*
|
|
264
|
-
* @example
|
|
265
|
-
* ```ts
|
|
266
|
-
* const result = await ctx.runQuery(
|
|
267
|
-
* components.auth.groups.memberResolve,
|
|
268
|
-
* {
|
|
269
|
-
* userId: currentUserId,
|
|
270
|
-
* groupId: subTeamGroupId,
|
|
271
|
-
* maxDepth: 5,
|
|
272
|
-
* ancestry: true,
|
|
273
|
-
* },
|
|
274
|
-
* );
|
|
275
|
-
* if (result.membership !== null) {
|
|
276
|
-
* console.log(
|
|
277
|
-
* result.isDirect ? "Direct member" : `Inherited from depth ${result.depth}`,
|
|
278
|
-
* );
|
|
279
|
-
* }
|
|
280
|
-
* ```
|
|
281
174
|
*/
|
|
282
175
|
const memberResolve = query({
|
|
283
176
|
args: {
|
|
@@ -339,12 +232,6 @@ const memberResolve = query({
|
|
|
339
232
|
* @param args.memberId - The `Id<"GroupMember">` of the member record to delete.
|
|
340
233
|
* @returns `null` on success.
|
|
341
234
|
*
|
|
342
|
-
* @example
|
|
343
|
-
* ```ts
|
|
344
|
-
* await ctx.runMutation(components.auth.groups.memberRemove, {
|
|
345
|
-
* memberId: memberToRemoveId,
|
|
346
|
-
* });
|
|
347
|
-
* ```
|
|
348
235
|
*/
|
|
349
236
|
const memberRemove = mutation({
|
|
350
237
|
args: { memberId: v.id("GroupMember") },
|
|
@@ -365,16 +252,6 @@ const memberRemove = mutation({
|
|
|
365
252
|
* @param args.data - A partial object of fields to patch. Supported keys include `roleIds`, `status`, and `extend`.
|
|
366
253
|
* @returns `null` on success.
|
|
367
254
|
*
|
|
368
|
-
* @example
|
|
369
|
-
* ```ts
|
|
370
|
-
* await ctx.runMutation(components.auth.groups.memberUpdate, {
|
|
371
|
-
* memberId: existingMemberId,
|
|
372
|
-
* data: {
|
|
373
|
-
* roleIds: ["admin", "editor"],
|
|
374
|
-
* status: "active",
|
|
375
|
-
* },
|
|
376
|
-
* });
|
|
377
|
-
* ```
|
|
378
255
|
*/
|
|
379
256
|
const memberUpdate = mutation({
|
|
380
257
|
args: {
|
|
@@ -389,5 +266,5 @@ const memberUpdate = mutation({
|
|
|
389
266
|
});
|
|
390
267
|
|
|
391
268
|
//#endregion
|
|
392
|
-
export { memberAdd, memberGet,
|
|
269
|
+
export { memberAdd, memberGet, memberList, memberRemove, memberResolve, memberUpdate };
|
|
393
270
|
//# sourceMappingURL=members.js.map
|