@shipfox/api-workspaces 4.0.0 → 6.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +58 -0
- package/README.md +2 -2
- package/dist/config.d.ts +0 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -50
- package/dist/config.js.map +1 -1
- package/dist/core/entities/invitation.d.ts +1 -0
- package/dist/core/entities/invitation.d.ts.map +1 -1
- package/dist/core/entities/invitation.js.map +1 -1
- package/dist/core/invitations.d.ts +21 -2
- package/dist/core/invitations.d.ts.map +1 -1
- package/dist/core/invitations.js +49 -12
- package/dist/core/invitations.js.map +1 -1
- package/dist/core/workspaces.d.ts +1 -1
- package/dist/core/workspaces.d.ts.map +1 -1
- package/dist/core/workspaces.js +13 -1
- package/dist/core/workspaces.js.map +1 -1
- package/dist/db/db.d.ts +34 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/invitations.d.ts +15 -8
- package/dist/db/invitations.d.ts.map +1 -1
- package/dist/db/invitations.js +70 -22
- package/dist/db/invitations.js.map +1 -1
- package/dist/db/schema/invitations.d.ts +17 -0
- package/dist/db/schema/invitations.d.ts.map +1 -1
- package/dist/db/schema/invitations.js +4 -0
- package/dist/db/schema/invitations.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +4 -0
- package/dist/presentation/inter-module.d.ts.map +1 -0
- package/dist/presentation/inter-module.js +74 -0
- package/dist/presentation/inter-module.js.map +1 -0
- package/dist/presentation/subscribers/on-invitation-send-requested.js +1 -1
- package/dist/presentation/subscribers/on-invitation-send-requested.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0004_tired_talon.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +448 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +28 -30
- package/src/config.ts +1 -44
- package/src/core/entities/invitation.ts +1 -0
- package/src/core/invitations.test.ts +35 -2
- package/src/core/invitations.ts +72 -17
- package/src/core/workspaces.test.ts +28 -0
- package/src/core/workspaces.ts +19 -2
- package/src/db/index.ts +1 -6
- package/src/db/invitations.test.ts +237 -15
- package/src/db/invitations.ts +78 -50
- package/src/db/schema/invitations.ts +2 -0
- package/src/db/workspaces.test.ts +3 -2
- package/src/index.test.ts +10 -1
- package/src/index.ts +8 -1
- package/src/presentation/inter-module.ts +94 -0
- package/src/presentation/routes/invitations/create.test.ts +19 -1
- package/src/presentation/subscribers/invitation-send-requested.test.ts +1 -1
- package/src/presentation/subscribers/on-invitation-send-requested.ts +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/dist/db/invitations.d.ts
CHANGED
|
@@ -32,16 +32,23 @@ export declare function listOpenInvitationsByWorkspace(params: {
|
|
|
32
32
|
export declare function revokeInvitation(params: {
|
|
33
33
|
invitationId: string;
|
|
34
34
|
}): Promise<void>;
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
acceptedByUserId: string;
|
|
38
|
-
acceptedByUserName?: string | null | undefined;
|
|
39
|
-
}
|
|
40
|
-
export interface AcceptInvitationResult {
|
|
35
|
+
export type ReconcileInvitationAcceptanceResult = {
|
|
36
|
+
status: 'accepted';
|
|
41
37
|
invitation: Invitation;
|
|
42
38
|
membership: Membership;
|
|
43
39
|
alreadyMember: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
40
|
+
} | {
|
|
41
|
+
status: 'already_accepted';
|
|
42
|
+
invitation: Invitation;
|
|
43
|
+
membership: Membership;
|
|
44
|
+
} | {
|
|
45
|
+
status: 'invalid' | 'expired' | 'revoked' | 'consumed_by_another_user' | 'email_mismatch';
|
|
46
|
+
};
|
|
47
|
+
export declare function reconcileInvitationAcceptance(params: {
|
|
48
|
+
invitationId: string;
|
|
49
|
+
acceptedByUserId: string;
|
|
50
|
+
email: string;
|
|
51
|
+
acceptedByUserName?: string | null | undefined;
|
|
52
|
+
}): Promise<ReconcileInvitationAcceptanceResult>;
|
|
46
53
|
export {};
|
|
47
54
|
//# sourceMappingURL=invitations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../src/db/invitations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../src/db/invitations.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,8BAA8B,CAAC;AAa7D,UAAU,0BAA0B;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,MAAM,sBAAsB,GAAG,0BAA0B,GAC7D,CACI;IACE,SAAS,EAAE;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB,GACD;IAAC,SAAS,CAAC,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAC,CACvC,CAAC;AAEJ,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAuE1F;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAUlC;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAAC,EAAE,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAM9F;AAED,wBAAsB,8BAA8B,CAAC,MAAM,EAAE;IAC3D,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAcxB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,YAAY,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAKpF;AAED,MAAM,MAAM,mCAAmC,GAC3C;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAC,GAC5F;IAAC,MAAM,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAC,GAC5E;IAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,0BAA0B,GAAG,gBAAgB,CAAA;CAAC,CAAC;AAEhG,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAChD,GAAG,OAAO,CAAC,mCAAmC,CAAC,CAwF/C"}
|
package/dist/db/invitations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WORKSPACES_INVITATION_SEND_REQUESTED } from '@shipfox/api-workspaces-dto';
|
|
1
|
+
import { WORKSPACES_INVITATION_SEND_REQUESTED, WORKSPACES_MEMBER_INVITED, WORKSPACES_MEMBER_JOINED } from '@shipfox/api-workspaces-dto';
|
|
2
2
|
import { writeOutboxEvent } from '@shipfox/node-outbox';
|
|
3
3
|
import { and, eq, gt, isNull, lt, sql } from 'drizzle-orm';
|
|
4
4
|
import { OpenInvitationExistsError } from '#core/errors.js';
|
|
@@ -10,8 +10,8 @@ import { memberships, toMembership } from './schema/memberships.js';
|
|
|
10
10
|
import { workspacesOutbox } from './schema/outbox.js';
|
|
11
11
|
export async function createInvitation(params) {
|
|
12
12
|
const result = await db().transaction(async (tx)=>{
|
|
13
|
-
await tx.delete(invitations).where(and(eq(invitations.workspaceId, params.workspaceId), eq(invitations.email, params.email), isNull(invitations.acceptedAt), lt(invitations.expiresAt, sql`now()`)));
|
|
14
|
-
const open = await tx.select().from(invitations).where(and(eq(invitations.workspaceId, params.workspaceId), eq(invitations.email, params.email), isNull(invitations.acceptedAt))).limit(1);
|
|
13
|
+
await tx.delete(invitations).where(and(eq(invitations.workspaceId, params.workspaceId), eq(invitations.email, params.email), isNull(invitations.acceptedAt), isNull(invitations.revokedAt), lt(invitations.expiresAt, sql`now()`)));
|
|
14
|
+
const open = await tx.select().from(invitations).where(and(eq(invitations.workspaceId, params.workspaceId), eq(invitations.email, params.email), isNull(invitations.acceptedAt), isNull(invitations.revokedAt))).limit(1);
|
|
15
15
|
if (open.length > 0) {
|
|
16
16
|
throw new OpenInvitationExistsError(params.email);
|
|
17
17
|
}
|
|
@@ -25,6 +25,15 @@ export async function createInvitation(params) {
|
|
|
25
25
|
}).returning();
|
|
26
26
|
const row = rows[0];
|
|
27
27
|
if (!row) throw new Error('Insert returned no rows');
|
|
28
|
+
await writeOutboxEvent(tx, workspacesOutbox, {
|
|
29
|
+
type: WORKSPACES_MEMBER_INVITED,
|
|
30
|
+
payload: {
|
|
31
|
+
workspaceId: params.workspaceId,
|
|
32
|
+
invitedEmail: params.email,
|
|
33
|
+
inviterUserId: params.invitedByUserId,
|
|
34
|
+
role: 'admin'
|
|
35
|
+
}
|
|
36
|
+
});
|
|
28
37
|
if (params.sendEmail) {
|
|
29
38
|
await writeOutboxEvent(tx, workspacesOutbox, {
|
|
30
39
|
type: WORKSPACES_INVITATION_SEND_REQUESTED,
|
|
@@ -55,34 +64,72 @@ export async function findInvitationById(params) {
|
|
|
55
64
|
return toInvitation(row);
|
|
56
65
|
}
|
|
57
66
|
export async function listOpenInvitationsByWorkspace(params) {
|
|
58
|
-
const rows = await db().select().from(invitations).where(and(eq(invitations.workspaceId, params.workspaceId), isNull(invitations.acceptedAt), gt(invitations.expiresAt, sql`now()`)));
|
|
67
|
+
const rows = await db().select().from(invitations).where(and(eq(invitations.workspaceId, params.workspaceId), isNull(invitations.acceptedAt), isNull(invitations.revokedAt), gt(invitations.expiresAt, sql`now()`)));
|
|
59
68
|
return rows.map(toInvitation);
|
|
60
69
|
}
|
|
61
70
|
export async function revokeInvitation(params) {
|
|
62
|
-
await db().
|
|
71
|
+
await db().update(invitations).set({
|
|
72
|
+
revokedAt: sql`now()`,
|
|
73
|
+
updatedAt: sql`now()`
|
|
74
|
+
}).where(and(eq(invitations.id, params.invitationId), isNull(invitations.acceptedAt)));
|
|
63
75
|
}
|
|
64
|
-
export async function
|
|
76
|
+
export async function reconcileInvitationAcceptance(params) {
|
|
65
77
|
const result = await db().transaction(async (tx)=>{
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
const findMembership = async (userId, workspaceId)=>{
|
|
79
|
+
const rows = await tx.select().from(memberships).where(and(eq(memberships.userId, userId), eq(memberships.workspaceId, workspaceId))).limit(1);
|
|
80
|
+
const row = rows[0];
|
|
81
|
+
return row ? toMembership(row) : undefined;
|
|
82
|
+
};
|
|
83
|
+
const rows = await tx.select().from(invitations).where(eq(invitations.id, params.invitationId)).limit(1).for('update');
|
|
84
|
+
const row = rows[0];
|
|
85
|
+
if (!row) return {
|
|
86
|
+
status: 'invalid'
|
|
87
|
+
};
|
|
88
|
+
const invitation = toInvitation(row);
|
|
89
|
+
if (invitation.acceptedAt !== null) {
|
|
90
|
+
if (invitation.acceptedByUserId !== params.acceptedByUserId) {
|
|
91
|
+
return {
|
|
92
|
+
status: 'consumed_by_another_user'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const membership = await findMembership(params.acceptedByUserId, invitation.workspaceId);
|
|
96
|
+
if (!membership) throw new Error('Accepted invitation has no membership');
|
|
97
|
+
return {
|
|
98
|
+
status: 'already_accepted',
|
|
99
|
+
invitation,
|
|
100
|
+
membership
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (invitation.revokedAt !== null) return {
|
|
104
|
+
status: 'revoked'
|
|
105
|
+
};
|
|
106
|
+
if (invitation.expiresAt.getTime() <= Date.now()) return {
|
|
107
|
+
status: 'expired'
|
|
108
|
+
};
|
|
109
|
+
if (invitation.email !== params.email) return {
|
|
110
|
+
status: 'email_mismatch'
|
|
111
|
+
};
|
|
112
|
+
const existingMembership = await findMembership(params.acceptedByUserId, invitation.workspaceId);
|
|
113
|
+
let membership = existingMembership;
|
|
114
|
+
if (!membership) {
|
|
77
115
|
const created = await tx.insert(memberships).values(membershipValues({
|
|
78
116
|
userId: params.acceptedByUserId,
|
|
79
|
-
userEmail:
|
|
117
|
+
userEmail: invitation.email,
|
|
80
118
|
userName: params.acceptedByUserName ?? null,
|
|
81
|
-
workspaceId:
|
|
119
|
+
workspaceId: invitation.workspaceId
|
|
82
120
|
})).returning();
|
|
83
121
|
const createdRow = created[0];
|
|
84
122
|
if (!createdRow) throw new Error('Insert returned no rows');
|
|
85
123
|
membership = toMembership(createdRow);
|
|
124
|
+
await writeOutboxEvent(tx, workspacesOutbox, {
|
|
125
|
+
type: WORKSPACES_MEMBER_JOINED,
|
|
126
|
+
payload: {
|
|
127
|
+
workspaceId: invitation.workspaceId,
|
|
128
|
+
userId: params.acceptedByUserId,
|
|
129
|
+
email: invitation.email,
|
|
130
|
+
viaInvitation: true
|
|
131
|
+
}
|
|
132
|
+
});
|
|
86
133
|
}
|
|
87
134
|
const updated = await tx.update(invitations).set({
|
|
88
135
|
acceptedAt: sql`now()`,
|
|
@@ -92,13 +139,14 @@ export async function acceptInvitation(params) {
|
|
|
92
139
|
const updatedRow = updated[0];
|
|
93
140
|
if (!updatedRow) throw new Error('Update returned no rows');
|
|
94
141
|
return {
|
|
142
|
+
status: 'accepted',
|
|
95
143
|
invitation: toInvitation(updatedRow),
|
|
96
144
|
membership,
|
|
97
|
-
alreadyMember
|
|
145
|
+
alreadyMember: existingMembership !== undefined
|
|
98
146
|
};
|
|
99
147
|
});
|
|
100
|
-
if (result
|
|
101
|
-
|
|
148
|
+
if (result.status === 'accepted') {
|
|
149
|
+
if (!result.alreadyMember) recordWorkspaceMembershipChanged('added');
|
|
102
150
|
recordWorkspaceInvitationAccepted(result.alreadyMember ? 'already_member' : 'added');
|
|
103
151
|
}
|
|
104
152
|
return result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/invitations.ts"],"sourcesContent":["import {\n WORKSPACES_INVITATION_SEND_REQUESTED,\n type WorkspacesEventMap,\n} from '@shipfox/api-workspaces-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, eq, gt, isNull, lt, sql} from 'drizzle-orm';\nimport type {Invitation} from '#core/entities/invitation.js';\nimport type {Membership} from '#core/entities/membership.js';\nimport {OpenInvitationExistsError} from '#core/errors.js';\nimport {\n recordWorkspaceInvitationAccepted,\n recordWorkspaceInvitationCreated,\n recordWorkspaceMembershipChanged,\n} from '#metrics/instance.js';\nimport {db} from './db.js';\nimport {membershipValues} from './memberships.js';\nimport {invitations, toInvitation} from './schema/invitations.js';\nimport {memberships, toMembership} from './schema/memberships.js';\nimport {workspacesOutbox} from './schema/outbox.js';\n\ninterface CreateInvitationBaseParams {\n workspaceId: string;\n email: string;\n hashedToken: string;\n expiresAt: Date;\n invitedByUserId: string;\n invitedByDisplay?: string | null;\n}\n\nexport type CreateInvitationParams = CreateInvitationBaseParams &\n (\n | {\n sendEmail: {\n workspaceName: string;\n inviterName: string;\n inviteLink: string;\n };\n skipEmail?: never;\n }\n | {sendEmail?: never; skipEmail: true}\n );\n\nexport async function createInvitation(params: CreateInvitationParams): Promise<Invitation> {\n const result = await db().transaction(async (tx) => {\n await tx\n .delete(invitations)\n .where(\n and(\n eq(invitations.workspaceId, params.workspaceId),\n eq(invitations.email, params.email),\n isNull(invitations.acceptedAt),\n lt(invitations.expiresAt, sql`now()`),\n ),\n );\n\n const open = await tx\n .select()\n .from(invitations)\n .where(\n and(\n eq(invitations.workspaceId, params.workspaceId),\n eq(invitations.email, params.email),\n isNull(invitations.acceptedAt),\n ),\n )\n .limit(1);\n\n if (open.length > 0) {\n throw new OpenInvitationExistsError(params.email);\n }\n\n const rows = await tx\n .insert(invitations)\n .values({\n workspaceId: params.workspaceId,\n email: params.email,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n invitedByUserId: params.invitedByUserId,\n invitedByDisplay: params.invitedByDisplay ?? null,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n if (params.sendEmail) {\n await writeOutboxEvent<WorkspacesEventMap>(tx, workspacesOutbox, {\n type: WORKSPACES_INVITATION_SEND_REQUESTED,\n payload: {\n email: params.email,\n ...params.sendEmail,\n },\n });\n }\n return {\n invitation: toInvitation(row),\n emailRequested: params.sendEmail ? ('requested' as const) : ('skipped' as const),\n };\n });\n\n recordWorkspaceInvitationCreated(result.emailRequested);\n return result.invitation;\n}\n\nexport async function findInvitationByToken(params: {\n hashedToken: string;\n}): Promise<Invitation | undefined> {\n const rows = await db()\n .select()\n .from(invitations)\n .where(eq(invitations.hashedToken, params.hashedToken))\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toInvitation(row);\n}\n\nexport async function findInvitationById(params: {id: string}): Promise<Invitation | undefined> {\n const rows = await db().select().from(invitations).where(eq(invitations.id, params.id)).limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toInvitation(row);\n}\n\nexport async function listOpenInvitationsByWorkspace(params: {\n workspaceId: string;\n}): Promise<Invitation[]> {\n const rows = await db()\n .select()\n .from(invitations)\n .where(\n and(\n eq(invitations.workspaceId, params.workspaceId),\n isNull(invitations.acceptedAt),\n gt(invitations.expiresAt, sql`now()`),\n ),\n );\n\n return rows.map(toInvitation);\n}\n\nexport async function revokeInvitation(params: {invitationId: string}): Promise<void> {\n await db().delete(invitations).where(eq(invitations.id, params.invitationId));\n}\n\nexport interface AcceptInvitationParams {\n invitationId: string;\n acceptedByUserId: string;\n acceptedByUserName?: string | null | undefined;\n}\n\nexport interface AcceptInvitationResult {\n invitation: Invitation;\n membership: Membership;\n alreadyMember: boolean;\n}\n\nexport async function acceptInvitation(\n params: AcceptInvitationParams,\n): Promise<AcceptInvitationResult | undefined> {\n const result = await db().transaction(async (tx) => {\n const inv = await tx\n .select()\n .from(invitations)\n .where(\n and(\n eq(invitations.id, params.invitationId),\n isNull(invitations.acceptedAt),\n gt(invitations.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const invRow = inv[0];\n if (!invRow) return undefined;\n\n const existing = await tx\n .select()\n .from(memberships)\n .where(\n and(\n eq(memberships.userId, params.acceptedByUserId),\n eq(memberships.workspaceId, invRow.workspaceId),\n ),\n )\n .limit(1);\n\n let membership: Membership;\n let alreadyMember = false;\n const existingRow = existing[0];\n\n if (existingRow) {\n alreadyMember = true;\n membership = toMembership(existingRow);\n } else {\n const created = await tx\n .insert(memberships)\n .values(\n membershipValues({\n userId: params.acceptedByUserId,\n userEmail: invRow.email,\n userName: params.acceptedByUserName ?? null,\n workspaceId: invRow.workspaceId,\n }),\n )\n .returning();\n const createdRow = created[0];\n if (!createdRow) throw new Error('Insert returned no rows');\n membership = toMembership(createdRow);\n }\n\n const updated = await tx\n .update(invitations)\n .set({\n acceptedAt: sql`now()`,\n acceptedByUserId: params.acceptedByUserId,\n updatedAt: sql`now()`,\n })\n .where(eq(invitations.id, params.invitationId))\n .returning();\n\n const updatedRow = updated[0];\n if (!updatedRow) throw new Error('Update returned no rows');\n\n return {invitation: toInvitation(updatedRow), membership, alreadyMember};\n });\n\n if (result && !result.alreadyMember) recordWorkspaceMembershipChanged('added');\n if (result) {\n recordWorkspaceInvitationAccepted(result.alreadyMember ? 'already_member' : 'added');\n }\n return result;\n}\n"],"names":["WORKSPACES_INVITATION_SEND_REQUESTED","writeOutboxEvent","and","eq","gt","isNull","lt","sql","OpenInvitationExistsError","recordWorkspaceInvitationAccepted","recordWorkspaceInvitationCreated","recordWorkspaceMembershipChanged","db","membershipValues","invitations","toInvitation","memberships","toMembership","workspacesOutbox","createInvitation","params","result","transaction","tx","delete","where","workspaceId","email","acceptedAt","expiresAt","open","select","from","limit","length","rows","insert","values","hashedToken","invitedByUserId","invitedByDisplay","returning","row","Error","sendEmail","type","payload","invitation","emailRequested","findInvitationByToken","undefined","findInvitationById","id","listOpenInvitationsByWorkspace","map","revokeInvitation","invitationId","acceptInvitation","inv","invRow","existing","userId","acceptedByUserId","membership","alreadyMember","existingRow","created","userEmail","userName","acceptedByUserName","createdRow","updated","update","set","updatedAt","updatedRow"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/invitations.ts"],"sourcesContent":["import {\n WORKSPACES_INVITATION_SEND_REQUESTED,\n WORKSPACES_MEMBER_INVITED,\n WORKSPACES_MEMBER_JOINED,\n type WorkspacesEventMap,\n} from '@shipfox/api-workspaces-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, eq, gt, isNull, lt, sql} from 'drizzle-orm';\nimport type {Invitation} from '#core/entities/invitation.js';\nimport type {Membership} from '#core/entities/membership.js';\nimport {OpenInvitationExistsError} from '#core/errors.js';\nimport {\n recordWorkspaceInvitationAccepted,\n recordWorkspaceInvitationCreated,\n recordWorkspaceMembershipChanged,\n} from '#metrics/instance.js';\nimport {db} from './db.js';\nimport {membershipValues} from './memberships.js';\nimport {invitations, toInvitation} from './schema/invitations.js';\nimport {memberships, toMembership} from './schema/memberships.js';\nimport {workspacesOutbox} from './schema/outbox.js';\n\ninterface CreateInvitationBaseParams {\n workspaceId: string;\n email: string;\n hashedToken: string;\n expiresAt: Date;\n invitedByUserId: string;\n invitedByDisplay?: string | null;\n}\n\nexport type CreateInvitationParams = CreateInvitationBaseParams &\n (\n | {\n sendEmail: {\n workspaceName: string;\n inviterName: string;\n inviteLink: string;\n };\n skipEmail?: never;\n }\n | {sendEmail?: never; skipEmail: true}\n );\n\nexport async function createInvitation(params: CreateInvitationParams): Promise<Invitation> {\n const result = await db().transaction(async (tx) => {\n await tx\n .delete(invitations)\n .where(\n and(\n eq(invitations.workspaceId, params.workspaceId),\n eq(invitations.email, params.email),\n isNull(invitations.acceptedAt),\n isNull(invitations.revokedAt),\n lt(invitations.expiresAt, sql`now()`),\n ),\n );\n\n const open = await tx\n .select()\n .from(invitations)\n .where(\n and(\n eq(invitations.workspaceId, params.workspaceId),\n eq(invitations.email, params.email),\n isNull(invitations.acceptedAt),\n isNull(invitations.revokedAt),\n ),\n )\n .limit(1);\n\n if (open.length > 0) {\n throw new OpenInvitationExistsError(params.email);\n }\n\n const rows = await tx\n .insert(invitations)\n .values({\n workspaceId: params.workspaceId,\n email: params.email,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n invitedByUserId: params.invitedByUserId,\n invitedByDisplay: params.invitedByDisplay ?? null,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n await writeOutboxEvent<WorkspacesEventMap>(tx, workspacesOutbox, {\n type: WORKSPACES_MEMBER_INVITED,\n payload: {\n workspaceId: params.workspaceId,\n invitedEmail: params.email,\n inviterUserId: params.invitedByUserId,\n role: 'admin',\n },\n });\n if (params.sendEmail) {\n await writeOutboxEvent<WorkspacesEventMap>(tx, workspacesOutbox, {\n type: WORKSPACES_INVITATION_SEND_REQUESTED,\n payload: {\n email: params.email,\n ...params.sendEmail,\n },\n });\n }\n return {\n invitation: toInvitation(row),\n emailRequested: params.sendEmail ? ('requested' as const) : ('skipped' as const),\n };\n });\n\n recordWorkspaceInvitationCreated(result.emailRequested);\n return result.invitation;\n}\n\nexport async function findInvitationByToken(params: {\n hashedToken: string;\n}): Promise<Invitation | undefined> {\n const rows = await db()\n .select()\n .from(invitations)\n .where(eq(invitations.hashedToken, params.hashedToken))\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toInvitation(row);\n}\n\nexport async function findInvitationById(params: {id: string}): Promise<Invitation | undefined> {\n const rows = await db().select().from(invitations).where(eq(invitations.id, params.id)).limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toInvitation(row);\n}\n\nexport async function listOpenInvitationsByWorkspace(params: {\n workspaceId: string;\n}): Promise<Invitation[]> {\n const rows = await db()\n .select()\n .from(invitations)\n .where(\n and(\n eq(invitations.workspaceId, params.workspaceId),\n isNull(invitations.acceptedAt),\n isNull(invitations.revokedAt),\n gt(invitations.expiresAt, sql`now()`),\n ),\n );\n\n return rows.map(toInvitation);\n}\n\nexport async function revokeInvitation(params: {invitationId: string}): Promise<void> {\n await db()\n .update(invitations)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(and(eq(invitations.id, params.invitationId), isNull(invitations.acceptedAt)));\n}\n\nexport type ReconcileInvitationAcceptanceResult =\n | {status: 'accepted'; invitation: Invitation; membership: Membership; alreadyMember: boolean}\n | {status: 'already_accepted'; invitation: Invitation; membership: Membership}\n | {status: 'invalid' | 'expired' | 'revoked' | 'consumed_by_another_user' | 'email_mismatch'};\n\nexport async function reconcileInvitationAcceptance(params: {\n invitationId: string;\n acceptedByUserId: string;\n email: string;\n acceptedByUserName?: string | null | undefined;\n}): Promise<ReconcileInvitationAcceptanceResult> {\n const result = await db().transaction(async (tx) => {\n const findMembership = async (userId: string, workspaceId: string) => {\n const rows = await tx\n .select()\n .from(memberships)\n .where(and(eq(memberships.userId, userId), eq(memberships.workspaceId, workspaceId)))\n .limit(1);\n const row = rows[0];\n return row ? toMembership(row) : undefined;\n };\n const rows = await tx\n .select()\n .from(invitations)\n .where(eq(invitations.id, params.invitationId))\n .limit(1)\n .for('update');\n const row = rows[0];\n if (!row) return {status: 'invalid'} as const;\n\n const invitation = toInvitation(row);\n if (invitation.acceptedAt !== null) {\n if (invitation.acceptedByUserId !== params.acceptedByUserId) {\n return {status: 'consumed_by_another_user'} as const;\n }\n const membership = await findMembership(params.acceptedByUserId, invitation.workspaceId);\n if (!membership) throw new Error('Accepted invitation has no membership');\n return {status: 'already_accepted', invitation, membership} as const;\n }\n if (invitation.revokedAt !== null) return {status: 'revoked'} as const;\n if (invitation.expiresAt.getTime() <= Date.now()) return {status: 'expired'} as const;\n if (invitation.email !== params.email) return {status: 'email_mismatch'} as const;\n\n const existingMembership = await findMembership(\n params.acceptedByUserId,\n invitation.workspaceId,\n );\n let membership = existingMembership;\n if (!membership) {\n const created = await tx\n .insert(memberships)\n .values(\n membershipValues({\n userId: params.acceptedByUserId,\n userEmail: invitation.email,\n userName: params.acceptedByUserName ?? null,\n workspaceId: invitation.workspaceId,\n }),\n )\n .returning();\n const createdRow = created[0];\n if (!createdRow) throw new Error('Insert returned no rows');\n membership = toMembership(createdRow);\n await writeOutboxEvent<WorkspacesEventMap>(tx, workspacesOutbox, {\n type: WORKSPACES_MEMBER_JOINED,\n payload: {\n workspaceId: invitation.workspaceId,\n userId: params.acceptedByUserId,\n email: invitation.email,\n viaInvitation: true,\n },\n });\n }\n\n const updated = await tx\n .update(invitations)\n .set({\n acceptedAt: sql`now()`,\n acceptedByUserId: params.acceptedByUserId,\n updatedAt: sql`now()`,\n })\n .where(eq(invitations.id, params.invitationId))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new Error('Update returned no rows');\n return {\n status: 'accepted',\n invitation: toInvitation(updatedRow),\n membership,\n alreadyMember: existingMembership !== undefined,\n } as const;\n });\n\n if (result.status === 'accepted') {\n if (!result.alreadyMember) recordWorkspaceMembershipChanged('added');\n recordWorkspaceInvitationAccepted(result.alreadyMember ? 'already_member' : 'added');\n }\n return result;\n}\n"],"names":["WORKSPACES_INVITATION_SEND_REQUESTED","WORKSPACES_MEMBER_INVITED","WORKSPACES_MEMBER_JOINED","writeOutboxEvent","and","eq","gt","isNull","lt","sql","OpenInvitationExistsError","recordWorkspaceInvitationAccepted","recordWorkspaceInvitationCreated","recordWorkspaceMembershipChanged","db","membershipValues","invitations","toInvitation","memberships","toMembership","workspacesOutbox","createInvitation","params","result","transaction","tx","delete","where","workspaceId","email","acceptedAt","revokedAt","expiresAt","open","select","from","limit","length","rows","insert","values","hashedToken","invitedByUserId","invitedByDisplay","returning","row","Error","type","payload","invitedEmail","inviterUserId","role","sendEmail","invitation","emailRequested","findInvitationByToken","undefined","findInvitationById","id","listOpenInvitationsByWorkspace","map","revokeInvitation","update","set","updatedAt","invitationId","reconcileInvitationAcceptance","findMembership","userId","for","status","acceptedByUserId","membership","getTime","Date","now","existingMembership","created","userEmail","userName","acceptedByUserName","createdRow","viaInvitation","updated","updatedRow","alreadyMember"],"mappings":"AAAA,SACEA,oCAAoC,EACpCC,yBAAyB,EACzBC,wBAAwB,QAEnB,8BAA8B;AACrC,SAAQC,gBAAgB,QAAO,uBAAuB;AACtD,SAAQC,GAAG,EAAEC,EAAE,EAAEC,EAAE,EAAEC,MAAM,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AAGzD,SAAQC,yBAAyB,QAAO,kBAAkB;AAC1D,SACEC,iCAAiC,EACjCC,gCAAgC,EAChCC,gCAAgC,QAC3B,uBAAuB;AAC9B,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,gBAAgB,QAAO,mBAAmB;AAClD,SAAQC,WAAW,EAAEC,YAAY,QAAO,0BAA0B;AAClE,SAAQC,WAAW,EAAEC,YAAY,QAAO,0BAA0B;AAClE,SAAQC,gBAAgB,QAAO,qBAAqB;AAwBpD,OAAO,eAAeC,iBAAiBC,MAA8B;IACnE,MAAMC,SAAS,MAAMT,KAAKU,WAAW,CAAC,OAAOC;QAC3C,MAAMA,GACHC,MAAM,CAACV,aACPW,KAAK,CACJvB,IACEC,GAAGW,YAAYY,WAAW,EAAEN,OAAOM,WAAW,GAC9CvB,GAAGW,YAAYa,KAAK,EAAEP,OAAOO,KAAK,GAClCtB,OAAOS,YAAYc,UAAU,GAC7BvB,OAAOS,YAAYe,SAAS,GAC5BvB,GAAGQ,YAAYgB,SAAS,EAAEvB,GAAG,CAAC,KAAK,CAAC;QAI1C,MAAMwB,OAAO,MAAMR,GAChBS,MAAM,GACNC,IAAI,CAACnB,aACLW,KAAK,CACJvB,IACEC,GAAGW,YAAYY,WAAW,EAAEN,OAAOM,WAAW,GAC9CvB,GAAGW,YAAYa,KAAK,EAAEP,OAAOO,KAAK,GAClCtB,OAAOS,YAAYc,UAAU,GAC7BvB,OAAOS,YAAYe,SAAS,IAG/BK,KAAK,CAAC;QAET,IAAIH,KAAKI,MAAM,GAAG,GAAG;YACnB,MAAM,IAAI3B,0BAA0BY,OAAOO,KAAK;QAClD;QAEA,MAAMS,OAAO,MAAMb,GAChBc,MAAM,CAACvB,aACPwB,MAAM,CAAC;YACNZ,aAAaN,OAAOM,WAAW;YAC/BC,OAAOP,OAAOO,KAAK;YACnBY,aAAanB,OAAOmB,WAAW;YAC/BT,WAAWV,OAAOU,SAAS;YAC3BU,iBAAiBpB,OAAOoB,eAAe;YACvCC,kBAAkBrB,OAAOqB,gBAAgB,IAAI;QAC/C,GACCC,SAAS;QAEZ,MAAMC,MAAMP,IAAI,CAAC,EAAE;QACnB,IAAI,CAACO,KAAK,MAAM,IAAIC,MAAM;QAC1B,MAAM3C,iBAAqCsB,IAAIL,kBAAkB;YAC/D2B,MAAM9C;YACN+C,SAAS;gBACPpB,aAAaN,OAAOM,WAAW;gBAC/BqB,cAAc3B,OAAOO,KAAK;gBAC1BqB,eAAe5B,OAAOoB,eAAe;gBACrCS,MAAM;YACR;QACF;QACA,IAAI7B,OAAO8B,SAAS,EAAE;YACpB,MAAMjD,iBAAqCsB,IAAIL,kBAAkB;gBAC/D2B,MAAM/C;gBACNgD,SAAS;oBACPnB,OAAOP,OAAOO,KAAK;oBACnB,GAAGP,OAAO8B,SAAS;gBACrB;YACF;QACF;QACA,OAAO;YACLC,YAAYpC,aAAa4B;YACzBS,gBAAgBhC,OAAO8B,SAAS,GAAI,cAAyB;QAC/D;IACF;IAEAxC,iCAAiCW,OAAO+B,cAAc;IACtD,OAAO/B,OAAO8B,UAAU;AAC1B;AAEA,OAAO,eAAeE,sBAAsBjC,MAE3C;IACC,MAAMgB,OAAO,MAAMxB,KAChBoB,MAAM,GACNC,IAAI,CAACnB,aACLW,KAAK,CAACtB,GAAGW,YAAYyB,WAAW,EAAEnB,OAAOmB,WAAW,GACpDL,KAAK,CAAC;IAET,MAAMS,MAAMP,IAAI,CAAC,EAAE;IACnB,IAAI,CAACO,KAAK,OAAOW;IACjB,OAAOvC,aAAa4B;AACtB;AAEA,OAAO,eAAeY,mBAAmBnC,MAAoB;IAC3D,MAAMgB,OAAO,MAAMxB,KAAKoB,MAAM,GAAGC,IAAI,CAACnB,aAAaW,KAAK,CAACtB,GAAGW,YAAY0C,EAAE,EAAEpC,OAAOoC,EAAE,GAAGtB,KAAK,CAAC;IAE9F,MAAMS,MAAMP,IAAI,CAAC,EAAE;IACnB,IAAI,CAACO,KAAK,OAAOW;IACjB,OAAOvC,aAAa4B;AACtB;AAEA,OAAO,eAAec,+BAA+BrC,MAEpD;IACC,MAAMgB,OAAO,MAAMxB,KAChBoB,MAAM,GACNC,IAAI,CAACnB,aACLW,KAAK,CACJvB,IACEC,GAAGW,YAAYY,WAAW,EAAEN,OAAOM,WAAW,GAC9CrB,OAAOS,YAAYc,UAAU,GAC7BvB,OAAOS,YAAYe,SAAS,GAC5BzB,GAAGU,YAAYgB,SAAS,EAAEvB,GAAG,CAAC,KAAK,CAAC;IAI1C,OAAO6B,KAAKsB,GAAG,CAAC3C;AAClB;AAEA,OAAO,eAAe4C,iBAAiBvC,MAA8B;IACnE,MAAMR,KACHgD,MAAM,CAAC9C,aACP+C,GAAG,CAAC;QAAChC,WAAWtB,GAAG,CAAC,KAAK,CAAC;QAAEuD,WAAWvD,GAAG,CAAC,KAAK,CAAC;IAAA,GACjDkB,KAAK,CAACvB,IAAIC,GAAGW,YAAY0C,EAAE,EAAEpC,OAAO2C,YAAY,GAAG1D,OAAOS,YAAYc,UAAU;AACrF;AAOA,OAAO,eAAeoC,8BAA8B5C,MAKnD;IACC,MAAMC,SAAS,MAAMT,KAAKU,WAAW,CAAC,OAAOC;QAC3C,MAAM0C,iBAAiB,OAAOC,QAAgBxC;YAC5C,MAAMU,OAAO,MAAMb,GAChBS,MAAM,GACNC,IAAI,CAACjB,aACLS,KAAK,CAACvB,IAAIC,GAAGa,YAAYkD,MAAM,EAAEA,SAAS/D,GAAGa,YAAYU,WAAW,EAAEA,eACtEQ,KAAK,CAAC;YACT,MAAMS,MAAMP,IAAI,CAAC,EAAE;YACnB,OAAOO,MAAM1B,aAAa0B,OAAOW;QACnC;QACA,MAAMlB,OAAO,MAAMb,GAChBS,MAAM,GACNC,IAAI,CAACnB,aACLW,KAAK,CAACtB,GAAGW,YAAY0C,EAAE,EAAEpC,OAAO2C,YAAY,GAC5C7B,KAAK,CAAC,GACNiC,GAAG,CAAC;QACP,MAAMxB,MAAMP,IAAI,CAAC,EAAE;QACnB,IAAI,CAACO,KAAK,OAAO;YAACyB,QAAQ;QAAS;QAEnC,MAAMjB,aAAapC,aAAa4B;QAChC,IAAIQ,WAAWvB,UAAU,KAAK,MAAM;YAClC,IAAIuB,WAAWkB,gBAAgB,KAAKjD,OAAOiD,gBAAgB,EAAE;gBAC3D,OAAO;oBAACD,QAAQ;gBAA0B;YAC5C;YACA,MAAME,aAAa,MAAML,eAAe7C,OAAOiD,gBAAgB,EAAElB,WAAWzB,WAAW;YACvF,IAAI,CAAC4C,YAAY,MAAM,IAAI1B,MAAM;YACjC,OAAO;gBAACwB,QAAQ;gBAAoBjB;gBAAYmB;YAAU;QAC5D;QACA,IAAInB,WAAWtB,SAAS,KAAK,MAAM,OAAO;YAACuC,QAAQ;QAAS;QAC5D,IAAIjB,WAAWrB,SAAS,CAACyC,OAAO,MAAMC,KAAKC,GAAG,IAAI,OAAO;YAACL,QAAQ;QAAS;QAC3E,IAAIjB,WAAWxB,KAAK,KAAKP,OAAOO,KAAK,EAAE,OAAO;YAACyC,QAAQ;QAAgB;QAEvE,MAAMM,qBAAqB,MAAMT,eAC/B7C,OAAOiD,gBAAgB,EACvBlB,WAAWzB,WAAW;QAExB,IAAI4C,aAAaI;QACjB,IAAI,CAACJ,YAAY;YACf,MAAMK,UAAU,MAAMpD,GACnBc,MAAM,CAACrB,aACPsB,MAAM,CACLzB,iBAAiB;gBACfqD,QAAQ9C,OAAOiD,gBAAgB;gBAC/BO,WAAWzB,WAAWxB,KAAK;gBAC3BkD,UAAUzD,OAAO0D,kBAAkB,IAAI;gBACvCpD,aAAayB,WAAWzB,WAAW;YACrC,IAEDgB,SAAS;YACZ,MAAMqC,aAAaJ,OAAO,CAAC,EAAE;YAC7B,IAAI,CAACI,YAAY,MAAM,IAAInC,MAAM;YACjC0B,aAAarD,aAAa8D;YAC1B,MAAM9E,iBAAqCsB,IAAIL,kBAAkB;gBAC/D2B,MAAM7C;gBACN8C,SAAS;oBACPpB,aAAayB,WAAWzB,WAAW;oBACnCwC,QAAQ9C,OAAOiD,gBAAgB;oBAC/B1C,OAAOwB,WAAWxB,KAAK;oBACvBqD,eAAe;gBACjB;YACF;QACF;QAEA,MAAMC,UAAU,MAAM1D,GACnBqC,MAAM,CAAC9C,aACP+C,GAAG,CAAC;YACHjC,YAAYrB,GAAG,CAAC,KAAK,CAAC;YACtB8D,kBAAkBjD,OAAOiD,gBAAgB;YACzCP,WAAWvD,GAAG,CAAC,KAAK,CAAC;QACvB,GACCkB,KAAK,CAACtB,GAAGW,YAAY0C,EAAE,EAAEpC,OAAO2C,YAAY,GAC5CrB,SAAS;QACZ,MAAMwC,aAAaD,OAAO,CAAC,EAAE;QAC7B,IAAI,CAACC,YAAY,MAAM,IAAItC,MAAM;QACjC,OAAO;YACLwB,QAAQ;YACRjB,YAAYpC,aAAamE;YACzBZ;YACAa,eAAeT,uBAAuBpB;QACxC;IACF;IAEA,IAAIjC,OAAO+C,MAAM,KAAK,YAAY;QAChC,IAAI,CAAC/C,OAAO8D,aAAa,EAAExE,iCAAiC;QAC5DF,kCAAkCY,OAAO8D,aAAa,GAAG,mBAAmB;IAC9E;IACA,OAAO9D;AACT"}
|
|
@@ -88,6 +88,23 @@ export declare const invitations: import("drizzle-orm/pg-core").PgTableWithColum
|
|
|
88
88
|
identity: undefined;
|
|
89
89
|
generated: undefined;
|
|
90
90
|
}, {}, {}>;
|
|
91
|
+
revokedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
92
|
+
name: "revoked_at";
|
|
93
|
+
tableName: "invitations";
|
|
94
|
+
dataType: "date";
|
|
95
|
+
columnType: "PgTimestamp";
|
|
96
|
+
data: Date;
|
|
97
|
+
driverParam: string;
|
|
98
|
+
notNull: false;
|
|
99
|
+
hasDefault: false;
|
|
100
|
+
isPrimaryKey: false;
|
|
101
|
+
isAutoincrement: false;
|
|
102
|
+
hasRuntimeDefault: false;
|
|
103
|
+
enumValues: undefined;
|
|
104
|
+
baseColumn: never;
|
|
105
|
+
identity: undefined;
|
|
106
|
+
generated: undefined;
|
|
107
|
+
}, {}, {}>;
|
|
91
108
|
acceptedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
92
109
|
name: "accepted_at";
|
|
93
110
|
tableName: "invitations";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../../src/db/schema/invitations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,8BAA8B,CAAC;AAI7D,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../../src/db/schema/invitations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,8BAA8B,CAAC;AAI7D,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC;AAEjE,wBAAgB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,UAAU,CAe1D"}
|
|
@@ -12,6 +12,9 @@ export const invitations = pgTable('invitations', {
|
|
|
12
12
|
expiresAt: timestamp('expires_at', {
|
|
13
13
|
withTimezone: true
|
|
14
14
|
}).notNull(),
|
|
15
|
+
revokedAt: timestamp('revoked_at', {
|
|
16
|
+
withTimezone: true
|
|
17
|
+
}),
|
|
15
18
|
acceptedAt: timestamp('accepted_at', {
|
|
16
19
|
withTimezone: true
|
|
17
20
|
}),
|
|
@@ -35,6 +38,7 @@ export function toInvitation(row) {
|
|
|
35
38
|
email: row.email,
|
|
36
39
|
hashedToken: row.hashedToken,
|
|
37
40
|
expiresAt: row.expiresAt,
|
|
41
|
+
revokedAt: row.revokedAt,
|
|
38
42
|
acceptedAt: row.acceptedAt,
|
|
39
43
|
acceptedByUserId: row.acceptedByUserId,
|
|
40
44
|
invitedByUserId: row.invitedByUserId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/db/schema/invitations.ts"],"sourcesContent":["import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';\nimport {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';\nimport type {Invitation} from '#core/entities/invitation.js';\nimport {pgTable} from './common.js';\nimport {workspaces} from './workspaces.js';\n\nexport const invitations = pgTable(\n 'invitations',\n {\n id: uuidv7PrimaryKey(),\n workspaceId: uuid('workspace_id')\n .notNull()\n .references(() => workspaces.id, {onDelete: 'cascade'}),\n email: text('email').notNull(),\n hashedToken: text('hashed_token').notNull(),\n expiresAt: timestamp('expires_at', {withTimezone: true}).notNull(),\n acceptedAt: timestamp('accepted_at', {withTimezone: true}),\n acceptedByUserId: uuid('accepted_by_user_id'),\n invitedByUserId: uuid('invited_by_user_id').notNull(),\n invitedByDisplay: text('invited_by_display'),\n createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),\n },\n (table) => [\n uniqueIndex('workspaces_invitations_hashed_token_unique').on(table.hashedToken),\n index('workspaces_invitations_workspace_email_idx').on(table.workspaceId, table.email),\n ],\n);\n\nexport type InvitationDb = typeof invitations.$inferSelect;\nexport type InvitationCreateDb = typeof invitations.$inferInsert;\n\nexport function toInvitation(row: InvitationDb): Invitation {\n return {\n id: row.id,\n workspaceId: row.workspaceId,\n email: row.email,\n hashedToken: row.hashedToken,\n expiresAt: row.expiresAt,\n acceptedAt: row.acceptedAt,\n acceptedByUserId: row.acceptedByUserId,\n invitedByUserId: row.invitedByUserId,\n invitedByDisplay: row.invitedByDisplay,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n"],"names":["uuidv7PrimaryKey","index","text","timestamp","uniqueIndex","uuid","pgTable","workspaces","invitations","id","workspaceId","notNull","references","onDelete","email","hashedToken","expiresAt","withTimezone","acceptedAt","acceptedByUserId","invitedByUserId","invitedByDisplay","createdAt","defaultNow","updatedAt","table","on","toInvitation","row"],"mappings":"AAAA,SAAQA,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,KAAK,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,IAAI,QAAO,sBAAsB;AAE9E,SAAQC,OAAO,QAAO,cAAc;AACpC,SAAQC,UAAU,QAAO,kBAAkB;AAE3C,OAAO,MAAMC,cAAcF,QACzB,eACA;IACEG,IAAIT;IACJU,aAAaL,KAAK,gBACfM,OAAO,GACPC,UAAU,CAAC,IAAML,WAAWE,EAAE,EAAE;QAACI,UAAU;IAAS;IACvDC,OAAOZ,KAAK,SAASS,OAAO;IAC5BI,aAAab,KAAK,gBAAgBS,OAAO;IACzCK,WAAWb,UAAU,cAAc;QAACc,cAAc;IAAI,GAAGN,OAAO;IAChEO,
|
|
1
|
+
{"version":3,"sources":["../../../src/db/schema/invitations.ts"],"sourcesContent":["import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';\nimport {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';\nimport type {Invitation} from '#core/entities/invitation.js';\nimport {pgTable} from './common.js';\nimport {workspaces} from './workspaces.js';\n\nexport const invitations = pgTable(\n 'invitations',\n {\n id: uuidv7PrimaryKey(),\n workspaceId: uuid('workspace_id')\n .notNull()\n .references(() => workspaces.id, {onDelete: 'cascade'}),\n email: text('email').notNull(),\n hashedToken: text('hashed_token').notNull(),\n expiresAt: timestamp('expires_at', {withTimezone: true}).notNull(),\n revokedAt: timestamp('revoked_at', {withTimezone: true}),\n acceptedAt: timestamp('accepted_at', {withTimezone: true}),\n acceptedByUserId: uuid('accepted_by_user_id'),\n invitedByUserId: uuid('invited_by_user_id').notNull(),\n invitedByDisplay: text('invited_by_display'),\n createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),\n },\n (table) => [\n uniqueIndex('workspaces_invitations_hashed_token_unique').on(table.hashedToken),\n index('workspaces_invitations_workspace_email_idx').on(table.workspaceId, table.email),\n ],\n);\n\nexport type InvitationDb = typeof invitations.$inferSelect;\nexport type InvitationCreateDb = typeof invitations.$inferInsert;\n\nexport function toInvitation(row: InvitationDb): Invitation {\n return {\n id: row.id,\n workspaceId: row.workspaceId,\n email: row.email,\n hashedToken: row.hashedToken,\n expiresAt: row.expiresAt,\n revokedAt: row.revokedAt,\n acceptedAt: row.acceptedAt,\n acceptedByUserId: row.acceptedByUserId,\n invitedByUserId: row.invitedByUserId,\n invitedByDisplay: row.invitedByDisplay,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n"],"names":["uuidv7PrimaryKey","index","text","timestamp","uniqueIndex","uuid","pgTable","workspaces","invitations","id","workspaceId","notNull","references","onDelete","email","hashedToken","expiresAt","withTimezone","revokedAt","acceptedAt","acceptedByUserId","invitedByUserId","invitedByDisplay","createdAt","defaultNow","updatedAt","table","on","toInvitation","row"],"mappings":"AAAA,SAAQA,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,KAAK,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,IAAI,QAAO,sBAAsB;AAE9E,SAAQC,OAAO,QAAO,cAAc;AACpC,SAAQC,UAAU,QAAO,kBAAkB;AAE3C,OAAO,MAAMC,cAAcF,QACzB,eACA;IACEG,IAAIT;IACJU,aAAaL,KAAK,gBACfM,OAAO,GACPC,UAAU,CAAC,IAAML,WAAWE,EAAE,EAAE;QAACI,UAAU;IAAS;IACvDC,OAAOZ,KAAK,SAASS,OAAO;IAC5BI,aAAab,KAAK,gBAAgBS,OAAO;IACzCK,WAAWb,UAAU,cAAc;QAACc,cAAc;IAAI,GAAGN,OAAO;IAChEO,WAAWf,UAAU,cAAc;QAACc,cAAc;IAAI;IACtDE,YAAYhB,UAAU,eAAe;QAACc,cAAc;IAAI;IACxDG,kBAAkBf,KAAK;IACvBgB,iBAAiBhB,KAAK,sBAAsBM,OAAO;IACnDW,kBAAkBpB,KAAK;IACvBqB,WAAWpB,UAAU,cAAc;QAACc,cAAc;IAAI,GAAGN,OAAO,GAAGa,UAAU;IAC7EC,WAAWtB,UAAU,cAAc;QAACc,cAAc;IAAI,GAAGN,OAAO,GAAGa,UAAU;AAC/E,GACA,CAACE,QAAU;QACTtB,YAAY,8CAA8CuB,EAAE,CAACD,MAAMX,WAAW;QAC9Ed,MAAM,8CAA8C0B,EAAE,CAACD,MAAMhB,WAAW,EAAEgB,MAAMZ,KAAK;KACtF,EACD;AAKF,OAAO,SAASc,aAAaC,GAAiB;IAC5C,OAAO;QACLpB,IAAIoB,IAAIpB,EAAE;QACVC,aAAamB,IAAInB,WAAW;QAC5BI,OAAOe,IAAIf,KAAK;QAChBC,aAAac,IAAId,WAAW;QAC5BC,WAAWa,IAAIb,SAAS;QACxBE,WAAWW,IAAIX,SAAS;QACxBC,YAAYU,IAAIV,UAAU;QAC1BC,kBAAkBS,IAAIT,gBAAgB;QACtCC,iBAAiBQ,IAAIR,eAAe;QACpCC,kBAAkBO,IAAIP,gBAAgB;QACtCC,WAAWM,IAAIN,SAAS;QACxBE,WAAWI,IAAIJ,SAAS;IAC1B;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { Invitation } from '#core/entities/invitation.js';
|
|
|
3
3
|
export type { Membership } from '#core/entities/membership.js';
|
|
4
4
|
export type { Workspace, WorkspaceStatus } from '#core/entities/workspace.js';
|
|
5
5
|
export { InvitationEmailMismatchError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, WorkspaceNotFoundError, } from '#core/errors.js';
|
|
6
|
-
export { acceptWorkspaceInvitation, peekInvitationByRawToken } from '#core/invitations.js';
|
|
6
|
+
export { acceptWorkspaceInvitation, peekInvitationByRawToken, reconcileWorkspaceInvitationAcceptance, type WorkspaceInvitationReconciliation, } from '#core/invitations.js';
|
|
7
7
|
export { type EnsureMembershipParams, ensureMembership } from '#core/memberships.js';
|
|
8
8
|
export { getWorkspace, requireWorkspaceMembership } from '#core/workspaces.js';
|
|
9
9
|
export { db, migrationsPath } from '#db/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AASxD,YAAY,EAAC,UAAU,EAAC,MAAM,8BAA8B,CAAC;AAC7D,YAAY,EAAC,UAAU,EAAC,MAAM,8BAA8B,CAAC;AAC7D,YAAY,EAAC,SAAS,EAAE,eAAe,EAAC,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,sCAAsC,EACtC,KAAK,iCAAiC,GACvC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,KAAK,sBAAsB,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAC,YAAY,EAAE,0BAA0B,EAAC,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAC,EAAE,EAAE,cAAc,EAAC,MAAM,cAAc,CAAC;AAChD,OAAO,EAAC,qBAAqB,EAAC,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAC,gBAAgB,IAAI,MAAM,EAAC,MAAM,+BAA+B,CAAC;AAIzE,eAAO,MAAM,gBAAgB,EAAE,aAW9B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,11 @@ import { subscriberFactory } from '@shipfox/node-module';
|
|
|
3
3
|
import { db, migrationsPath, workspacesOutbox } from '#db/index.js';
|
|
4
4
|
import { registerWorkspacesServiceMetrics } from '#metrics/index.js';
|
|
5
5
|
import { workspacesE2eRoutes } from '#presentation/e2eRoutes/index.js';
|
|
6
|
+
import { createWorkspacesInterModulePresentation } from '#presentation/inter-module.js';
|
|
6
7
|
import { workspacesRoutes } from '#presentation/routes/index.js';
|
|
7
8
|
import { onInvitationSendRequested } from '#presentation/subscribers/index.js';
|
|
8
9
|
export { InvitationEmailMismatchError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, WorkspaceNotFoundError } from '#core/errors.js';
|
|
9
|
-
export { acceptWorkspaceInvitation, peekInvitationByRawToken } from '#core/invitations.js';
|
|
10
|
+
export { acceptWorkspaceInvitation, peekInvitationByRawToken, reconcileWorkspaceInvitationAcceptance } from '#core/invitations.js';
|
|
10
11
|
export { ensureMembership } from '#core/memberships.js';
|
|
11
12
|
export { getWorkspace, requireWorkspaceMembership } from '#core/workspaces.js';
|
|
12
13
|
export { db, migrationsPath } from '#db/index.js';
|
|
@@ -34,7 +35,10 @@ export const workspacesModule = {
|
|
|
34
35
|
subscribers: [
|
|
35
36
|
subscriber(WORKSPACES_INVITATION_SEND_REQUESTED, onInvitationSendRequested)
|
|
36
37
|
],
|
|
37
|
-
metrics: registerWorkspacesServiceMetrics
|
|
38
|
+
metrics: registerWorkspacesServiceMetrics,
|
|
39
|
+
interModulePresentations: [
|
|
40
|
+
createWorkspacesInterModulePresentation()
|
|
41
|
+
]
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n WORKSPACES_INVITATION_SEND_REQUESTED,\n type WorkspacesEventMap,\n workspacesEventSchemas,\n} from '@shipfox/api-workspaces-dto';\nimport type {ShipfoxModule} from '@shipfox/node-module';\nimport {subscriberFactory} from '@shipfox/node-module';\nimport {db, migrationsPath, workspacesOutbox} from '#db/index.js';\nimport {registerWorkspacesServiceMetrics} from '#metrics/index.js';\nimport {workspacesE2eRoutes} from '#presentation/e2eRoutes/index.js';\nimport {workspacesRoutes} from '#presentation/routes/index.js';\nimport {onInvitationSendRequested} from '#presentation/subscribers/index.js';\n\nexport type {Invitation} from '#core/entities/invitation.js';\nexport type {Membership} from '#core/entities/membership.js';\nexport type {Workspace, WorkspaceStatus} from '#core/entities/workspace.js';\nexport {\n InvitationEmailMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n WorkspaceNotFoundError,\n} from '#core/errors.js';\nexport {acceptWorkspaceInvitation
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n WORKSPACES_INVITATION_SEND_REQUESTED,\n type WorkspacesEventMap,\n workspacesEventSchemas,\n} from '@shipfox/api-workspaces-dto';\nimport type {ShipfoxModule} from '@shipfox/node-module';\nimport {subscriberFactory} from '@shipfox/node-module';\nimport {db, migrationsPath, workspacesOutbox} from '#db/index.js';\nimport {registerWorkspacesServiceMetrics} from '#metrics/index.js';\nimport {workspacesE2eRoutes} from '#presentation/e2eRoutes/index.js';\nimport {createWorkspacesInterModulePresentation} from '#presentation/inter-module.js';\nimport {workspacesRoutes} from '#presentation/routes/index.js';\nimport {onInvitationSendRequested} from '#presentation/subscribers/index.js';\n\nexport type {Invitation} from '#core/entities/invitation.js';\nexport type {Membership} from '#core/entities/membership.js';\nexport type {Workspace, WorkspaceStatus} from '#core/entities/workspace.js';\nexport {\n InvitationEmailMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n WorkspaceNotFoundError,\n} from '#core/errors.js';\nexport {\n acceptWorkspaceInvitation,\n peekInvitationByRawToken,\n reconcileWorkspaceInvitationAcceptance,\n type WorkspaceInvitationReconciliation,\n} from '#core/invitations.js';\nexport {type EnsureMembershipParams, ensureMembership} from '#core/memberships.js';\nexport {getWorkspace, requireWorkspaceMembership} from '#core/workspaces.js';\nexport {db, migrationsPath} from '#db/index.js';\nexport {listMembershipsByUser} from '#db/memberships.js';\nexport {workspacesRoutes as routes} from '#presentation/routes/index.js';\n\nconst subscriber = subscriberFactory<WorkspacesEventMap>();\n\nexport const workspacesModule: ShipfoxModule = {\n name: 'workspaces',\n database: {db, migrationsPath},\n routes: workspacesRoutes,\n e2eRoutes: [workspacesE2eRoutes],\n publishers: [\n {name: 'workspaces', table: workspacesOutbox, db, eventSchemas: workspacesEventSchemas},\n ],\n subscribers: [subscriber(WORKSPACES_INVITATION_SEND_REQUESTED, onInvitationSendRequested)],\n metrics: registerWorkspacesServiceMetrics,\n interModulePresentations: [createWorkspacesInterModulePresentation()],\n};\n"],"names":["WORKSPACES_INVITATION_SEND_REQUESTED","workspacesEventSchemas","subscriberFactory","db","migrationsPath","workspacesOutbox","registerWorkspacesServiceMetrics","workspacesE2eRoutes","createWorkspacesInterModulePresentation","workspacesRoutes","onInvitationSendRequested","InvitationEmailMismatchError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","WorkspaceNotFoundError","acceptWorkspaceInvitation","peekInvitationByRawToken","reconcileWorkspaceInvitationAcceptance","ensureMembership","getWorkspace","requireWorkspaceMembership","listMembershipsByUser","routes","subscriber","workspacesModule","name","database","e2eRoutes","publishers","table","eventSchemas","subscribers","metrics","interModulePresentations"],"mappings":"AAAA,SACEA,oCAAoC,EAEpCC,sBAAsB,QACjB,8BAA8B;AAErC,SAAQC,iBAAiB,QAAO,uBAAuB;AACvD,SAAQC,EAAE,EAAEC,cAAc,EAAEC,gBAAgB,QAAO,eAAe;AAClE,SAAQC,gCAAgC,QAAO,oBAAoB;AACnE,SAAQC,mBAAmB,QAAO,mCAAmC;AACrE,SAAQC,uCAAuC,QAAO,gCAAgC;AACtF,SAAQC,gBAAgB,QAAO,gCAAgC;AAC/D,SAAQC,yBAAyB,QAAO,qCAAqC;AAK7E,SACEC,4BAA4B,EAC5BC,qBAAqB,EACrBC,iBAAiB,EACjBC,iBAAiB,EACjBC,sBAAsB,QACjB,kBAAkB;AACzB,SACEC,yBAAyB,EACzBC,wBAAwB,EACxBC,sCAAsC,QAEjC,uBAAuB;AAC9B,SAAqCC,gBAAgB,QAAO,uBAAuB;AACnF,SAAQC,YAAY,EAAEC,0BAA0B,QAAO,sBAAsB;AAC7E,SAAQlB,EAAE,EAAEC,cAAc,QAAO,eAAe;AAChD,SAAQkB,qBAAqB,QAAO,qBAAqB;AACzD,SAAQb,oBAAoBc,MAAM,QAAO,gCAAgC;AAEzE,MAAMC,aAAatB;AAEnB,OAAO,MAAMuB,mBAAkC;IAC7CC,MAAM;IACNC,UAAU;QAACxB;QAAIC;IAAc;IAC7BmB,QAAQd;IACRmB,WAAW;QAACrB;KAAoB;IAChCsB,YAAY;QACV;YAACH,MAAM;YAAcI,OAAOzB;YAAkBF;YAAI4B,cAAc9B;QAAsB;KACvF;IACD+B,aAAa;QAACR,WAAWxB,sCAAsCU;KAA2B;IAC1FuB,SAAS3B;IACT4B,0BAA0B;QAAC1B;KAA0C;AACvE,EAAE"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { workspacesInterModuleContract } from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
|
+
import { type InterModulePresentation } from '@shipfox/inter-module';
|
|
3
|
+
export declare function createWorkspacesInterModulePresentation(): InterModulePresentation<typeof workspacesInterModuleContract>;
|
|
4
|
+
//# sourceMappingURL=inter-module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inter-module.d.ts","sourceRoot":"","sources":["../../src/presentation/inter-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,6BAA6B,EAAC,MAAM,0CAA0C,CAAC;AACvF,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAC;AAc/B,wBAAgB,uCAAuC,IAAI,uBAAuB,CAChF,OAAO,6BAA6B,CACrC,CAwDA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { workspacesInterModuleContract } from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
|
+
import { createInterModuleKnownError, defineInterModulePresentation } from '@shipfox/inter-module';
|
|
3
|
+
import { InvitationEmailMismatchError, MembershipRequiredError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, WorkspaceInactiveError, WorkspaceNotFoundError } from '#core/errors.js';
|
|
4
|
+
import { acceptWorkspaceInvitation, peekInvitationByRawToken } from '#core/invitations.js';
|
|
5
|
+
import { requireWorkspaceMembership } from '#core/workspaces.js';
|
|
6
|
+
import { listMembershipsByUser } from '#db/memberships.js';
|
|
7
|
+
export function createWorkspacesInterModulePresentation() {
|
|
8
|
+
return defineInterModulePresentation(workspacesInterModuleContract, {
|
|
9
|
+
listMembershipsForTokenClaims: async ({ userId })=>({
|
|
10
|
+
memberships: (await listMembershipsByUser({
|
|
11
|
+
userId
|
|
12
|
+
})).map(({ workspaceId })=>({
|
|
13
|
+
workspaceId,
|
|
14
|
+
role: 'admin'
|
|
15
|
+
}))
|
|
16
|
+
}),
|
|
17
|
+
preflightInvitationAcceptance: async (input)=>{
|
|
18
|
+
try {
|
|
19
|
+
const invitation = await peekInvitationByRawToken({
|
|
20
|
+
token: input.token
|
|
21
|
+
});
|
|
22
|
+
if (!invitation) throw new TokenInvalidError('Invitation token is invalid');
|
|
23
|
+
if (invitation.acceptedAt !== null) throw new TokenAlreadyUsedError();
|
|
24
|
+
if (invitation.expiresAt.getTime() <= Date.now()) throw new TokenExpiredError();
|
|
25
|
+
if (invitation.email !== input.email) throw new InvitationEmailMismatchError();
|
|
26
|
+
return {};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw toInvitationKnownError('preflightInvitationAcceptance', error);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
acceptInvitation: async (input)=>{
|
|
32
|
+
try {
|
|
33
|
+
const result = await acceptWorkspaceInvitation(input);
|
|
34
|
+
return {
|
|
35
|
+
membership: {
|
|
36
|
+
id: result.membership.id,
|
|
37
|
+
userId: result.membership.userId,
|
|
38
|
+
workspaceId: result.membership.workspaceId
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw toInvitationKnownError('acceptInvitation', error);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
requireActiveMembership: async (input)=>{
|
|
46
|
+
try {
|
|
47
|
+
await requireWorkspaceMembership(input);
|
|
48
|
+
return {};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const method = workspacesInterModuleContract.methods.requireActiveMembership;
|
|
51
|
+
if (error instanceof MembershipRequiredError) throw createInterModuleKnownError(method, 'membership-required', {
|
|
52
|
+
workspaceId: input.workspaceId
|
|
53
|
+
});
|
|
54
|
+
if (error instanceof WorkspaceNotFoundError) throw createInterModuleKnownError(method, 'workspace-not-found', {
|
|
55
|
+
workspaceId: input.workspaceId
|
|
56
|
+
});
|
|
57
|
+
if (error instanceof WorkspaceInactiveError) throw createInterModuleKnownError(method, 'workspace-inactive', {
|
|
58
|
+
workspaceId: input.workspaceId
|
|
59
|
+
});
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function toInvitationKnownError(methodName, error) {
|
|
66
|
+
const method = workspacesInterModuleContract.methods[methodName];
|
|
67
|
+
if (error instanceof TokenInvalidError) return createInterModuleKnownError(method, 'invitation-token-invalid', {});
|
|
68
|
+
if (error instanceof TokenAlreadyUsedError) return createInterModuleKnownError(method, 'invitation-token-used', {});
|
|
69
|
+
if (error instanceof TokenExpiredError) return createInterModuleKnownError(method, 'invitation-token-expired', {});
|
|
70
|
+
if (error instanceof InvitationEmailMismatchError) return createInterModuleKnownError(method, 'invitation-email-mismatch', {});
|
|
71
|
+
return error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=inter-module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/presentation/inter-module.ts"],"sourcesContent":["import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';\nimport {\n createInterModuleKnownError,\n defineInterModulePresentation,\n type InterModulePresentation,\n} from '@shipfox/inter-module';\nimport {\n InvitationEmailMismatchError,\n MembershipRequiredError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n WorkspaceInactiveError,\n WorkspaceNotFoundError,\n} from '#core/errors.js';\nimport {acceptWorkspaceInvitation, peekInvitationByRawToken} from '#core/invitations.js';\nimport {requireWorkspaceMembership} from '#core/workspaces.js';\nimport {listMembershipsByUser} from '#db/memberships.js';\n\nexport function createWorkspacesInterModulePresentation(): InterModulePresentation<\n typeof workspacesInterModuleContract\n> {\n return defineInterModulePresentation(workspacesInterModuleContract, {\n listMembershipsForTokenClaims: async ({userId}) => ({\n memberships: (await listMembershipsByUser({userId})).map(({workspaceId}) => ({\n workspaceId,\n role: 'admin' as const,\n })),\n }),\n preflightInvitationAcceptance: async (input) => {\n try {\n const invitation = await peekInvitationByRawToken({token: input.token});\n if (!invitation) throw new TokenInvalidError('Invitation token is invalid');\n if (invitation.acceptedAt !== null) throw new TokenAlreadyUsedError();\n if (invitation.expiresAt.getTime() <= Date.now()) throw new TokenExpiredError();\n if (invitation.email !== input.email) throw new InvitationEmailMismatchError();\n return {};\n } catch (error) {\n throw toInvitationKnownError('preflightInvitationAcceptance', error);\n }\n },\n acceptInvitation: async (input) => {\n try {\n const result = await acceptWorkspaceInvitation(input);\n return {\n membership: {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n },\n };\n } catch (error) {\n throw toInvitationKnownError('acceptInvitation', error);\n }\n },\n requireActiveMembership: async (input) => {\n try {\n await requireWorkspaceMembership(input);\n return {};\n } catch (error) {\n const method = workspacesInterModuleContract.methods.requireActiveMembership;\n if (error instanceof MembershipRequiredError)\n throw createInterModuleKnownError(method, 'membership-required', {\n workspaceId: input.workspaceId,\n });\n if (error instanceof WorkspaceNotFoundError)\n throw createInterModuleKnownError(method, 'workspace-not-found', {\n workspaceId: input.workspaceId,\n });\n if (error instanceof WorkspaceInactiveError)\n throw createInterModuleKnownError(method, 'workspace-inactive', {\n workspaceId: input.workspaceId,\n });\n throw error;\n }\n },\n });\n}\n\nfunction toInvitationKnownError(\n methodName: 'preflightInvitationAcceptance' | 'acceptInvitation',\n error: unknown,\n): unknown {\n const method = workspacesInterModuleContract.methods[methodName];\n if (error instanceof TokenInvalidError)\n return createInterModuleKnownError(method, 'invitation-token-invalid', {});\n if (error instanceof TokenAlreadyUsedError)\n return createInterModuleKnownError(method, 'invitation-token-used', {});\n if (error instanceof TokenExpiredError)\n return createInterModuleKnownError(method, 'invitation-token-expired', {});\n if (error instanceof InvitationEmailMismatchError)\n return createInterModuleKnownError(method, 'invitation-email-mismatch', {});\n return error;\n}\n"],"names":["workspacesInterModuleContract","createInterModuleKnownError","defineInterModulePresentation","InvitationEmailMismatchError","MembershipRequiredError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","WorkspaceInactiveError","WorkspaceNotFoundError","acceptWorkspaceInvitation","peekInvitationByRawToken","requireWorkspaceMembership","listMembershipsByUser","createWorkspacesInterModulePresentation","listMembershipsForTokenClaims","userId","memberships","map","workspaceId","role","preflightInvitationAcceptance","input","invitation","token","acceptedAt","expiresAt","getTime","Date","now","email","error","toInvitationKnownError","acceptInvitation","result","membership","id","requireActiveMembership","method","methods","methodName"],"mappings":"AAAA,SAAQA,6BAA6B,QAAO,2CAA2C;AACvF,SACEC,2BAA2B,EAC3BC,6BAA6B,QAExB,wBAAwB;AAC/B,SACEC,4BAA4B,EAC5BC,uBAAuB,EACvBC,qBAAqB,EACrBC,iBAAiB,EACjBC,iBAAiB,EACjBC,sBAAsB,EACtBC,sBAAsB,QACjB,kBAAkB;AACzB,SAAQC,yBAAyB,EAAEC,wBAAwB,QAAO,uBAAuB;AACzF,SAAQC,0BAA0B,QAAO,sBAAsB;AAC/D,SAAQC,qBAAqB,QAAO,qBAAqB;AAEzD,OAAO,SAASC;IAGd,OAAOZ,8BAA8BF,+BAA+B;QAClEe,+BAA+B,OAAO,EAACC,MAAM,EAAC,GAAM,CAAA;gBAClDC,aAAa,AAAC,CAAA,MAAMJ,sBAAsB;oBAACG;gBAAM,EAAC,EAAGE,GAAG,CAAC,CAAC,EAACC,WAAW,EAAC,GAAM,CAAA;wBAC3EA;wBACAC,MAAM;oBACR,CAAA;YACF,CAAA;QACAC,+BAA+B,OAAOC;YACpC,IAAI;gBACF,MAAMC,aAAa,MAAMZ,yBAAyB;oBAACa,OAAOF,MAAME,KAAK;gBAAA;gBACrE,IAAI,CAACD,YAAY,MAAM,IAAIhB,kBAAkB;gBAC7C,IAAIgB,WAAWE,UAAU,KAAK,MAAM,MAAM,IAAIpB;gBAC9C,IAAIkB,WAAWG,SAAS,CAACC,OAAO,MAAMC,KAAKC,GAAG,IAAI,MAAM,IAAIvB;gBAC5D,IAAIiB,WAAWO,KAAK,KAAKR,MAAMQ,KAAK,EAAE,MAAM,IAAI3B;gBAChD,OAAO,CAAC;YACV,EAAE,OAAO4B,OAAO;gBACd,MAAMC,uBAAuB,iCAAiCD;YAChE;QACF;QACAE,kBAAkB,OAAOX;YACvB,IAAI;gBACF,MAAMY,SAAS,MAAMxB,0BAA0BY;gBAC/C,OAAO;oBACLa,YAAY;wBACVC,IAAIF,OAAOC,UAAU,CAACC,EAAE;wBACxBpB,QAAQkB,OAAOC,UAAU,CAACnB,MAAM;wBAChCG,aAAae,OAAOC,UAAU,CAAChB,WAAW;oBAC5C;gBACF;YACF,EAAE,OAAOY,OAAO;gBACd,MAAMC,uBAAuB,oBAAoBD;YACnD;QACF;QACAM,yBAAyB,OAAOf;YAC9B,IAAI;gBACF,MAAMV,2BAA2BU;gBACjC,OAAO,CAAC;YACV,EAAE,OAAOS,OAAO;gBACd,MAAMO,SAAStC,8BAA8BuC,OAAO,CAACF,uBAAuB;gBAC5E,IAAIN,iBAAiB3B,yBACnB,MAAMH,4BAA4BqC,QAAQ,uBAAuB;oBAC/DnB,aAAaG,MAAMH,WAAW;gBAChC;gBACF,IAAIY,iBAAiBtB,wBACnB,MAAMR,4BAA4BqC,QAAQ,uBAAuB;oBAC/DnB,aAAaG,MAAMH,WAAW;gBAChC;gBACF,IAAIY,iBAAiBvB,wBACnB,MAAMP,4BAA4BqC,QAAQ,sBAAsB;oBAC9DnB,aAAaG,MAAMH,WAAW;gBAChC;gBACF,MAAMY;YACR;QACF;IACF;AACF;AAEA,SAASC,uBACPQ,UAAgE,EAChET,KAAc;IAEd,MAAMO,SAAStC,8BAA8BuC,OAAO,CAACC,WAAW;IAChE,IAAIT,iBAAiBxB,mBACnB,OAAON,4BAA4BqC,QAAQ,4BAA4B,CAAC;IAC1E,IAAIP,iBAAiB1B,uBACnB,OAAOJ,4BAA4BqC,QAAQ,yBAAyB,CAAC;IACvE,IAAIP,iBAAiBzB,mBACnB,OAAOL,4BAA4BqC,QAAQ,4BAA4B,CAAC;IAC1E,IAAIP,iBAAiB5B,8BACnB,OAAOF,4BAA4BqC,QAAQ,6BAA6B,CAAC;IAC3E,OAAOP;AACT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { renderEmail } from '@shipfox/node-email';
|
|
2
|
-
import { mailer } from '
|
|
2
|
+
import { mailer } from '@shipfox/node-mailer';
|
|
3
3
|
export async function onInvitationSendRequested(payload) {
|
|
4
4
|
const email = await renderEmail('workspace-invitation', {
|
|
5
5
|
email: payload.email,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/subscribers/on-invitation-send-requested.ts"],"sourcesContent":["import type {WorkspacesInvitationSendRequestedEvent} from '@shipfox/api-workspaces-dto';\nimport {renderEmail} from '@shipfox/node-email';\nimport {mailer} from '
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/subscribers/on-invitation-send-requested.ts"],"sourcesContent":["import type {WorkspacesInvitationSendRequestedEvent} from '@shipfox/api-workspaces-dto';\nimport {renderEmail} from '@shipfox/node-email';\nimport {mailer} from '@shipfox/node-mailer';\n\nexport async function onInvitationSendRequested(\n payload: WorkspacesInvitationSendRequestedEvent,\n): Promise<void> {\n const email = await renderEmail('workspace-invitation', {\n email: payload.email,\n workspaceName: payload.workspaceName,\n inviterName: payload.inviterName,\n inviteLink: payload.inviteLink,\n });\n await mailer.send({to: payload.email, ...email});\n}\n"],"names":["renderEmail","mailer","onInvitationSendRequested","payload","email","workspaceName","inviterName","inviteLink","send","to"],"mappings":"AACA,SAAQA,WAAW,QAAO,sBAAsB;AAChD,SAAQC,MAAM,QAAO,uBAAuB;AAE5C,OAAO,eAAeC,0BACpBC,OAA+C;IAE/C,MAAMC,QAAQ,MAAMJ,YAAY,wBAAwB;QACtDI,OAAOD,QAAQC,KAAK;QACpBC,eAAeF,QAAQE,aAAa;QACpCC,aAAaH,QAAQG,WAAW;QAChCC,YAAYJ,QAAQI,UAAU;IAChC;IACA,MAAMN,OAAOO,IAAI,CAAC;QAACC,IAAIN,QAAQC,KAAK;QAAE,GAAGA,KAAK;IAAA;AAChD"}
|