@headless-lms/adapter-db 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +8 -6
- package/dist/migrate.js.map +1 -1
- package/dist/repositories/admin.d.ts +23 -0
- package/dist/repositories/admin.js +393 -0
- package/dist/repositories/admin.js.map +1 -0
- package/dist/repositories/assets.d.ts +6 -2
- package/dist/repositories/assets.js +53 -9
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +12 -6
- package/dist/repositories/automations.js +79 -11
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +55 -0
- package/dist/repositories/chats.js +454 -0
- package/dist/repositories/chats.js.map +1 -0
- package/dist/repositories/content.d.ts +44 -7
- package/dist/repositories/content.js +458 -24
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/credentials.js +2 -0
- package/dist/repositories/credentials.js.map +1 -1
- package/dist/repositories/discussion.d.ts +11 -4
- package/dist/repositories/discussion.js +72 -15
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +36 -10
- package/dist/repositories/entitlements.js +238 -23
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/grant-covers.d.ts +20 -0
- package/dist/repositories/grant-covers.js +55 -0
- package/dist/repositories/grant-covers.js.map +1 -0
- package/dist/repositories/identity.d.ts +4 -2
- package/dist/repositories/identity.js +34 -1
- package/dist/repositories/identity.js.map +1 -1
- package/dist/repositories/integrations.d.ts +1 -1
- package/dist/repositories/integrations.js +2 -0
- package/dist/repositories/integrations.js.map +1 -1
- package/dist/repositories/member.d.ts +16 -0
- package/dist/repositories/member.js +63 -0
- package/dist/repositories/member.js.map +1 -0
- package/dist/repositories/members.js +4 -2
- package/dist/repositories/members.js.map +1 -1
- package/dist/repositories/notifications.d.ts +23 -0
- package/dist/repositories/notifications.js +70 -0
- package/dist/repositories/notifications.js.map +1 -0
- package/dist/repositories/org-user-profile.d.ts +214 -1
- package/dist/repositories/org-user-profile.js +23 -2
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +6 -7
- package/dist/repositories/organizations.js +45 -52
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/outbox.js +3 -0
- package/dist/repositories/outbox.js.map +1 -1
- package/dist/repositories/pg-errors.d.ts +53 -5
- package/dist/repositories/pg-errors.js +99 -8
- package/dist/repositories/pg-errors.js.map +1 -1
- package/dist/repositories/posts.d.ts +39 -0
- package/dist/repositories/posts.js +255 -0
- package/dist/repositories/posts.js.map +1 -0
- package/dist/repositories/progress.d.ts +1 -1
- package/dist/repositories/progress.js +3 -0
- package/dist/repositories/progress.js.map +1 -1
- package/dist/repositories/settings.js +2 -0
- package/dist/repositories/settings.js.map +1 -1
- package/dist/schema/assets.d.ts +36 -2
- package/dist/schema/assets.js +12 -5
- package/dist/schema/assets.js.map +1 -1
- package/dist/schema/automations.d.ts +123 -8
- package/dist/schema/automations.js +11 -4
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/better-auth.d.ts +1613 -238
- package/dist/schema/better-auth.js +107 -22
- package/dist/schema/better-auth.js.map +1 -1
- package/dist/schema/content.d.ts +909 -22
- package/dist/schema/content.js +157 -13
- package/dist/schema/content.js.map +1 -1
- package/dist/schema/credentials.js +1 -1
- package/dist/schema/credentials.js.map +1 -1
- package/dist/schema/discussion.d.ts +2079 -55
- package/dist/schema/discussion.js +430 -13
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/entitlements.d.ts +484 -2
- package/dist/schema/entitlements.js +121 -6
- package/dist/schema/entitlements.js.map +1 -1
- package/dist/schema/identity.d.ts +517 -1
- package/dist/schema/identity.js +118 -2
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +6 -6
- package/dist/schema/index.js +32 -7
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/integrations.js +1 -1
- package/dist/schema/integrations.js.map +1 -1
- package/dist/schema/organizations.d.ts +21 -4
- package/dist/schema/organizations.js +4 -1
- package/dist/schema/organizations.js.map +1 -1
- package/dist/schema/outbox.d.ts +17 -0
- package/dist/schema/outbox.js +1 -0
- package/dist/schema/outbox.js.map +1 -1
- package/dist/schema/progress.d.ts +17 -0
- package/dist/schema/progress.js +1 -0
- package/dist/schema/progress.js.map +1 -1
- package/dist/schema/settings.js.map +1 -1
- package/dist/unit-of-work.js +7 -2
- package/dist/unit-of-work.js.map +1 -1
- package/drizzle/0000_baseline.sql +0 -1
- package/drizzle/0001_new_sentry.sql +43 -0
- package/drizzle/0002_automation_kinds.sql +7 -0
- package/drizzle/0003_community.sql +340 -0
- package/drizzle/0004_assets_sources.sql +1 -0
- package/drizzle/0005_notices_and_seed_index.sql +12 -0
- package/drizzle/0006_community_review_fixes.sql +24 -0
- package/drizzle/0007_notifications.sql +37 -0
- package/drizzle/0008_content_access.sql +13 -0
- package/drizzle/0009_community_indexes.sql +56 -0
- package/drizzle/0010_mcp_access.sql +114 -0
- package/drizzle/meta/0000_snapshot.json +167 -597
- package/drizzle/meta/0001_snapshot.json +3769 -0
- package/drizzle/meta/0002_snapshot.json +3770 -0
- package/drizzle/meta/0003_snapshot.json +7061 -0
- package/drizzle/meta/0004_snapshot.json +7068 -0
- package/drizzle/meta/0005_snapshot.json +7180 -0
- package/drizzle/meta/0006_snapshot.json +7298 -0
- package/drizzle/meta/0007_snapshot.json +7620 -0
- package/drizzle/meta/0008_snapshot.json +7666 -0
- package/drizzle/meta/0009_snapshot.json +8185 -0
- package/drizzle/meta/0010_snapshot.json +8813 -0
- package/drizzle/meta/_journal.json +71 -1
- package/package.json +3 -2
- package/dist/repositories/dashboard.d.ts +0 -14
- package/dist/repositories/dashboard.js +0 -55
- package/dist/repositories/dashboard.js.map +0 -1
- package/dist/repositories/learn.d.ts +0 -19
- package/dist/repositories/learn.js +0 -54
- package/dist/repositories/learn.js.map +0 -1
- package/dist/repositories/students.d.ts +0 -15
- package/dist/repositories/students.js +0 -83
- package/dist/repositories/students.js.map +0 -1
|
@@ -87,6 +87,219 @@ declare const orgUserProfileColumns: {
|
|
|
87
87
|
generated: undefined;
|
|
88
88
|
}, {}, {}>;
|
|
89
89
|
};
|
|
90
|
+
declare const authorProfileColumns: {
|
|
91
|
+
role: import("drizzle-orm/pg-core").PgColumn<{
|
|
92
|
+
name: "role";
|
|
93
|
+
tableName: "org_users";
|
|
94
|
+
dataType: "string";
|
|
95
|
+
columnType: "PgText";
|
|
96
|
+
data: "student" | "owner" | "admin" | "instructor" | "moderator";
|
|
97
|
+
driverParam: string;
|
|
98
|
+
notNull: true;
|
|
99
|
+
hasDefault: false;
|
|
100
|
+
isPrimaryKey: false;
|
|
101
|
+
isAutoincrement: false;
|
|
102
|
+
hasRuntimeDefault: false;
|
|
103
|
+
enumValues: ["owner", "admin", "instructor", "moderator", "student"];
|
|
104
|
+
baseColumn: never;
|
|
105
|
+
identity: undefined;
|
|
106
|
+
generated: undefined;
|
|
107
|
+
}, {}, {}>;
|
|
108
|
+
displayName: import("drizzle-orm/pg-core").PgColumn<{
|
|
109
|
+
name: "display_name";
|
|
110
|
+
tableName: "user_profiles";
|
|
111
|
+
dataType: "string";
|
|
112
|
+
columnType: "PgText";
|
|
113
|
+
data: string;
|
|
114
|
+
driverParam: string;
|
|
115
|
+
notNull: false;
|
|
116
|
+
hasDefault: false;
|
|
117
|
+
isPrimaryKey: false;
|
|
118
|
+
isAutoincrement: false;
|
|
119
|
+
hasRuntimeDefault: false;
|
|
120
|
+
enumValues: [string, ...string[]];
|
|
121
|
+
baseColumn: never;
|
|
122
|
+
identity: undefined;
|
|
123
|
+
generated: undefined;
|
|
124
|
+
}, {}, {}>;
|
|
125
|
+
headline: import("drizzle-orm/pg-core").PgColumn<{
|
|
126
|
+
name: "headline";
|
|
127
|
+
tableName: "user_profiles";
|
|
128
|
+
dataType: "string";
|
|
129
|
+
columnType: "PgText";
|
|
130
|
+
data: string;
|
|
131
|
+
driverParam: string;
|
|
132
|
+
notNull: false;
|
|
133
|
+
hasDefault: false;
|
|
134
|
+
isPrimaryKey: false;
|
|
135
|
+
isAutoincrement: false;
|
|
136
|
+
hasRuntimeDefault: false;
|
|
137
|
+
enumValues: [string, ...string[]];
|
|
138
|
+
baseColumn: never;
|
|
139
|
+
identity: undefined;
|
|
140
|
+
generated: undefined;
|
|
141
|
+
}, {}, {}>;
|
|
142
|
+
imageAssetOrgId: import("drizzle-orm/pg-core").PgColumn<{
|
|
143
|
+
name: "image_asset_org_id";
|
|
144
|
+
tableName: "user_profiles";
|
|
145
|
+
dataType: "string";
|
|
146
|
+
columnType: "PgText";
|
|
147
|
+
data: string;
|
|
148
|
+
driverParam: string;
|
|
149
|
+
notNull: false;
|
|
150
|
+
hasDefault: false;
|
|
151
|
+
isPrimaryKey: false;
|
|
152
|
+
isAutoincrement: false;
|
|
153
|
+
hasRuntimeDefault: false;
|
|
154
|
+
enumValues: [string, ...string[]];
|
|
155
|
+
baseColumn: never;
|
|
156
|
+
identity: undefined;
|
|
157
|
+
generated: undefined;
|
|
158
|
+
}, {}, {}>;
|
|
159
|
+
imageAssetId: import("drizzle-orm/pg-core").PgColumn<{
|
|
160
|
+
name: "image_asset_id";
|
|
161
|
+
tableName: "user_profiles";
|
|
162
|
+
dataType: "string";
|
|
163
|
+
columnType: "PgText";
|
|
164
|
+
data: string;
|
|
165
|
+
driverParam: string;
|
|
166
|
+
notNull: false;
|
|
167
|
+
hasDefault: false;
|
|
168
|
+
isPrimaryKey: false;
|
|
169
|
+
isAutoincrement: false;
|
|
170
|
+
hasRuntimeDefault: false;
|
|
171
|
+
enumValues: [string, ...string[]];
|
|
172
|
+
baseColumn: never;
|
|
173
|
+
identity: undefined;
|
|
174
|
+
generated: undefined;
|
|
175
|
+
}, {}, {}>;
|
|
176
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
177
|
+
name: "id";
|
|
178
|
+
tableName: "org_users";
|
|
179
|
+
dataType: "string";
|
|
180
|
+
columnType: "PgText";
|
|
181
|
+
data: string;
|
|
182
|
+
driverParam: string;
|
|
183
|
+
notNull: true;
|
|
184
|
+
hasDefault: true;
|
|
185
|
+
isPrimaryKey: false;
|
|
186
|
+
isAutoincrement: false;
|
|
187
|
+
hasRuntimeDefault: true;
|
|
188
|
+
enumValues: [string, ...string[]];
|
|
189
|
+
baseColumn: never;
|
|
190
|
+
identity: undefined;
|
|
191
|
+
generated: undefined;
|
|
192
|
+
}, {}, {}>;
|
|
193
|
+
firstName: import("drizzle-orm/pg-core").PgColumn<{
|
|
194
|
+
name: "first_name";
|
|
195
|
+
tableName: "users";
|
|
196
|
+
dataType: "string";
|
|
197
|
+
columnType: "PgText";
|
|
198
|
+
data: string;
|
|
199
|
+
driverParam: string;
|
|
200
|
+
notNull: false;
|
|
201
|
+
hasDefault: false;
|
|
202
|
+
isPrimaryKey: false;
|
|
203
|
+
isAutoincrement: false;
|
|
204
|
+
hasRuntimeDefault: false;
|
|
205
|
+
enumValues: [string, ...string[]];
|
|
206
|
+
baseColumn: never;
|
|
207
|
+
identity: undefined;
|
|
208
|
+
generated: undefined;
|
|
209
|
+
}, {}, {}>;
|
|
210
|
+
lastName: import("drizzle-orm/pg-core").PgColumn<{
|
|
211
|
+
name: "last_name";
|
|
212
|
+
tableName: "users";
|
|
213
|
+
dataType: "string";
|
|
214
|
+
columnType: "PgText";
|
|
215
|
+
data: string;
|
|
216
|
+
driverParam: string;
|
|
217
|
+
notNull: false;
|
|
218
|
+
hasDefault: false;
|
|
219
|
+
isPrimaryKey: false;
|
|
220
|
+
isAutoincrement: false;
|
|
221
|
+
hasRuntimeDefault: false;
|
|
222
|
+
enumValues: [string, ...string[]];
|
|
223
|
+
baseColumn: never;
|
|
224
|
+
identity: undefined;
|
|
225
|
+
generated: undefined;
|
|
226
|
+
}, {}, {}>;
|
|
227
|
+
email: import("drizzle-orm/pg-core").PgColumn<{
|
|
228
|
+
name: "email";
|
|
229
|
+
tableName: "users";
|
|
230
|
+
dataType: "string";
|
|
231
|
+
columnType: "PgText";
|
|
232
|
+
data: string;
|
|
233
|
+
driverParam: string;
|
|
234
|
+
notNull: true;
|
|
235
|
+
hasDefault: false;
|
|
236
|
+
isPrimaryKey: false;
|
|
237
|
+
isAutoincrement: false;
|
|
238
|
+
hasRuntimeDefault: false;
|
|
239
|
+
enumValues: [string, ...string[]];
|
|
240
|
+
baseColumn: never;
|
|
241
|
+
identity: undefined;
|
|
242
|
+
generated: undefined;
|
|
243
|
+
}, {}, {}>;
|
|
244
|
+
image: import("drizzle-orm/pg-core").PgColumn<{
|
|
245
|
+
name: "image";
|
|
246
|
+
tableName: "ba_user";
|
|
247
|
+
dataType: "string";
|
|
248
|
+
columnType: "PgText";
|
|
249
|
+
data: string;
|
|
250
|
+
driverParam: string;
|
|
251
|
+
notNull: false;
|
|
252
|
+
hasDefault: false;
|
|
253
|
+
isPrimaryKey: false;
|
|
254
|
+
isAutoincrement: false;
|
|
255
|
+
hasRuntimeDefault: false;
|
|
256
|
+
enumValues: [string, ...string[]];
|
|
257
|
+
baseColumn: never;
|
|
258
|
+
identity: undefined;
|
|
259
|
+
generated: undefined;
|
|
260
|
+
}, {}, {}>;
|
|
261
|
+
};
|
|
262
|
+
declare const studentFirstName: import("drizzle-orm").SQL<string | null>;
|
|
263
|
+
declare const studentLastName: import("drizzle-orm").SQL<string | null>;
|
|
264
|
+
declare const studentProfileColumns: {
|
|
265
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
266
|
+
name: "id";
|
|
267
|
+
tableName: "org_users";
|
|
268
|
+
dataType: "string";
|
|
269
|
+
columnType: "PgText";
|
|
270
|
+
data: string;
|
|
271
|
+
driverParam: string;
|
|
272
|
+
notNull: true;
|
|
273
|
+
hasDefault: true;
|
|
274
|
+
isPrimaryKey: false;
|
|
275
|
+
isAutoincrement: false;
|
|
276
|
+
hasRuntimeDefault: true;
|
|
277
|
+
enumValues: [string, ...string[]];
|
|
278
|
+
baseColumn: never;
|
|
279
|
+
identity: undefined;
|
|
280
|
+
generated: undefined;
|
|
281
|
+
}, {}, {}>;
|
|
282
|
+
firstName: import("drizzle-orm").SQL<string | null>;
|
|
283
|
+
lastName: import("drizzle-orm").SQL<string | null>;
|
|
284
|
+
email: import("drizzle-orm/pg-core").PgColumn<{
|
|
285
|
+
name: "email";
|
|
286
|
+
tableName: "users";
|
|
287
|
+
dataType: "string";
|
|
288
|
+
columnType: "PgText";
|
|
289
|
+
data: string;
|
|
290
|
+
driverParam: string;
|
|
291
|
+
notNull: true;
|
|
292
|
+
hasDefault: false;
|
|
293
|
+
isPrimaryKey: false;
|
|
294
|
+
isAutoincrement: false;
|
|
295
|
+
hasRuntimeDefault: false;
|
|
296
|
+
enumValues: [string, ...string[]];
|
|
297
|
+
baseColumn: never;
|
|
298
|
+
identity: undefined;
|
|
299
|
+
generated: undefined;
|
|
300
|
+
}, {}, {}>;
|
|
301
|
+
image: import("drizzle-orm").SQL<string | null>;
|
|
302
|
+
};
|
|
90
303
|
//#endregion
|
|
91
|
-
export { orgUserProfileColumns };
|
|
304
|
+
export { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns };
|
|
92
305
|
//# sourceMappingURL=org-user-profile.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { users } from "../schema/identity.js";
|
|
1
|
+
import { userProfiles, users } from "../schema/identity.js";
|
|
2
2
|
import { orgUsers } from "../schema/organizations.js";
|
|
3
3
|
import { user } from "../schema/better-auth.js";
|
|
4
|
+
import { sql } from "drizzle-orm";
|
|
4
5
|
//#region src/repositories/org-user-profile.ts
|
|
5
6
|
/** Spread into a `.select({ ... })` to get the profile columns. */
|
|
6
7
|
const orgUserProfileColumns = {
|
|
@@ -10,7 +11,27 @@ const orgUserProfileColumns = {
|
|
|
10
11
|
email: users.email,
|
|
11
12
|
image: user.image
|
|
12
13
|
};
|
|
14
|
+
const authorProfileColumns = {
|
|
15
|
+
...orgUserProfileColumns,
|
|
16
|
+
role: orgUsers.role,
|
|
17
|
+
displayName: userProfiles.displayName,
|
|
18
|
+
headline: userProfiles.headline,
|
|
19
|
+
imageAssetOrgId: userProfiles.imageAssetOrgId,
|
|
20
|
+
imageAssetId: userProfiles.imageAssetId
|
|
21
|
+
};
|
|
22
|
+
const profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (
|
|
23
|
+
select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}
|
|
24
|
+
)))`;
|
|
25
|
+
const studentFirstName = sql`case when ${profileVisible} then ${users.firstName} end`;
|
|
26
|
+
const studentLastName = sql`case when ${profileVisible} then ${users.lastName} end`;
|
|
27
|
+
const studentProfileColumns = {
|
|
28
|
+
id: orgUsers.id,
|
|
29
|
+
firstName: studentFirstName,
|
|
30
|
+
lastName: studentLastName,
|
|
31
|
+
email: users.email,
|
|
32
|
+
image: sql`case when ${profileVisible} then ${user.image} end`
|
|
33
|
+
};
|
|
13
34
|
//#endregion
|
|
14
|
-
export { orgUserProfileColumns };
|
|
35
|
+
export { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns };
|
|
15
36
|
|
|
16
37
|
//# sourceMappingURL=org-user-profile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-user-profile.js","names":[],"sources":["../../src/repositories/org-user-profile.ts"],"sourcesContent":["// The one display join for an org user's profile.\n//\n// The names and email live on the identity `users` row the org_users row links\n// to; the avatar lives on the auth engine's mirrored `user` table, reached\n// through that same identity row. The identity join is INNER (user_id is NOT\n// NULL); the auth join is LEFT, since only the avatar depends on it.\n//\n// Sanctioned by .eslintrc.cjs — \"db repositories read the auth adapter's\n// mirrored `user` table for display joins\".\n//\n// No `joinOrgUserProfile` helper: Drizzle's query builder type narrows on every\n// chained call, so a generic wrapper around `.innerJoin(...).leftJoin(...)`\n// can't be typed without widening it to `unknown` — each repository chains the\n// two joins inline instead.\nimport { orgUsers } from
|
|
1
|
+
{"version":3,"file":"org-user-profile.js","names":[],"sources":["../../src/repositories/org-user-profile.ts"],"sourcesContent":["// The one display join for an org user's profile.\n//\n// The names and email live on the identity `users` row the org_users row links\n// to; the avatar lives on the auth engine's mirrored `user` table, reached\n// through that same identity row. The identity join is INNER (user_id is NOT\n// NULL); the auth join is LEFT, since only the avatar depends on it.\n//\n// Sanctioned by .eslintrc.cjs — \"db repositories read the auth adapter's\n// mirrored `user` table for display joins\".\n//\n// No `joinOrgUserProfile` helper: Drizzle's query builder type narrows on every\n// chained call, so a generic wrapper around `.innerJoin(...).leftJoin(...)`\n// can't be typed without widening it to `unknown` — each repository chains the\n// two joins inline instead.\nimport { sql } from \"drizzle-orm\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\n\n/** Spread into a `.select({ ... })` to get the profile columns. */\nexport const orgUserProfileColumns = {\n id: orgUsers.id,\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n image: user.image,\n};\n\nexport const authorProfileColumns = {\n ...orgUserProfileColumns,\n role: orgUsers.role,\n displayName: userProfiles.displayName,\n headline: userProfiles.headline,\n imageAssetOrgId: userProfiles.imageAssetOrgId,\n imageAssetId: userProfiles.imageAssetId,\n};\n\n// A person is shared across organizations. Until an invited student accepts,\n// the inviting org sees the name and avatar only when the profile is its own:\n// no account yet and no other organization.\nconst profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (\n select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}\n)))`;\n\nexport const studentFirstName = sql<\n string | null\n>`case when ${profileVisible} then ${users.firstName} end`;\nexport const studentLastName = sql<\n string | null\n>`case when ${profileVisible} then ${users.lastName} end`;\n\nexport const studentProfileColumns = {\n id: orgUsers.id,\n firstName: studentFirstName,\n lastName: studentLastName,\n email: users.email,\n image: sql<string | null>`case when ${profileVisible} then ${user.image} end`,\n};\n"],"mappings":";;;;;;AAoBA,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW,MAAM;CACjB,UAAU,MAAM;CAChB,OAAO,MAAM;CACb,OAAO,KAAK;AACd;AAEA,MAAa,uBAAuB;CAClC,GAAG;CACH,MAAM,SAAS;CACf,aAAa,aAAa;CAC1B,UAAU,aAAa;CACvB,iBAAiB,aAAa;CAC9B,cAAc,aAAa;AAC7B;AAKA,MAAM,iBAAiB,GAAG,IAAI,SAAS,OAAO,oBAAoB,MAAM,WAAW;wDAC3B,MAAM,GAAG,uBAAuB,SAAS,MAAM;;AAGvG,MAAa,mBAAmB,GAE/B,aAAa,eAAe,QAAQ,MAAM,UAAU;AACrD,MAAa,kBAAkB,GAE9B,aAAa,eAAe,QAAQ,MAAM,SAAS;AAEpD,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW;CACX,UAAU;CACV,OAAO,MAAM;CACb,OAAO,GAAkB,aAAa,eAAe,QAAQ,KAAK,MAAM;AAC1E"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
-
import {
|
|
2
|
+
import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationsRepository, Role, UpdateOrgUserInput, UpdateOrganizationInput } from "@headless-lms/core/organizations";
|
|
3
3
|
import { Logger } from "@headless-lms/core/shared/ports";
|
|
4
4
|
//#region src/repositories/organizations.d.ts
|
|
5
5
|
declare class DrizzleOrganizationsRepository implements OrganizationsRepository {
|
|
@@ -12,20 +12,19 @@ declare class DrizzleOrganizationsRepository implements OrganizationsRepository
|
|
|
12
12
|
findById(id: string): Promise<Organization | null>;
|
|
13
13
|
findByExternalId(externalId: string): Promise<Organization | null>;
|
|
14
14
|
findBySlug(slug: string): Promise<Organization | null>;
|
|
15
|
-
upsertOrgUser(orgId: string, input: AddOrgUserInput): Promise<OrgUser>;
|
|
16
15
|
upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite>;
|
|
17
16
|
setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null>;
|
|
18
17
|
findInviteByTokenHash(tokenHash: string): Promise<Invite | null>;
|
|
18
|
+
findPendingInvite(orgId: string, email: string): Promise<Invite | null>;
|
|
19
|
+
setInviteToken(orgId: string, id: string, tokenHash: string, expiresAt: Date): Promise<Invite | null>;
|
|
19
20
|
findOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
|
|
20
21
|
findOrgUsersByUser(userId: string): Promise<OrgUser[]>;
|
|
21
22
|
findOrgUserById(orgId: string, id: string): Promise<OrgUser | null>;
|
|
23
|
+
findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]>;
|
|
24
|
+
findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]>;
|
|
22
25
|
createOrgUser(input: CreateOrgUserInput): Promise<OrgUser>;
|
|
23
|
-
|
|
24
|
-
orgUser: OrgUser;
|
|
25
|
-
created: boolean;
|
|
26
|
-
}>;
|
|
26
|
+
updateOrgUser(orgId: string, id: string, patch: UpdateOrgUserInput): Promise<OrgUser | null>;
|
|
27
27
|
findStudentOrgUsers(userId: string): Promise<OrgUser[]>;
|
|
28
|
-
cancelPendingInvite(orgId: string, email: string): Promise<Invite | null>;
|
|
29
28
|
deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null>;
|
|
30
29
|
}
|
|
31
30
|
//#endregion
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { invites, orgUsers, organizations } from "../schema/organizations.js";
|
|
2
|
-
import { isUniqueViolation } from "./pg-errors.js";
|
|
3
|
-
import { and, eq, sql } from "drizzle-orm";
|
|
4
|
-
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
2
|
+
import { isUniqueViolation, pgErrorFields, translateDbErrors } from "./pg-errors.js";
|
|
3
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
5
4
|
import { ConflictError } from "@headless-lms/core/shared/errors";
|
|
6
|
-
import {
|
|
5
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
6
|
+
import { parseRole } from "@headless-lms/core/organizations";
|
|
7
7
|
//#region src/repositories/organizations.ts
|
|
8
8
|
const INVITE_STATUSES = [
|
|
9
9
|
"pending",
|
|
@@ -22,7 +22,8 @@ function toInvite(row) {
|
|
|
22
22
|
invitedBy: row.invitedBy,
|
|
23
23
|
tokenHash: row.tokenHash,
|
|
24
24
|
expiresAt: row.expiresAt,
|
|
25
|
-
createdAt: row.createdAt
|
|
25
|
+
createdAt: row.createdAt,
|
|
26
|
+
updatedAt: row.updatedAt
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
var DrizzleOrganizationsRepository = class {
|
|
@@ -67,22 +68,6 @@ var DrizzleOrganizationsRepository = class {
|
|
|
67
68
|
const [row] = await this.db.select().from(organizations).where(eq(organizations.slug, slug)).limit(1);
|
|
68
69
|
return row ?? null;
|
|
69
70
|
}
|
|
70
|
-
async upsertOrgUser(orgId, input) {
|
|
71
|
-
const role = normalizeRole(input.role);
|
|
72
|
-
const [row] = await this.db.insert(orgUsers).values({
|
|
73
|
-
orgId,
|
|
74
|
-
userId: input.userId,
|
|
75
|
-
role
|
|
76
|
-
}).onConflictDoUpdate({
|
|
77
|
-
target: [orgUsers.orgId, orgUsers.userId],
|
|
78
|
-
set: { role }
|
|
79
|
-
}).returning();
|
|
80
|
-
if (!row) throw new Error("failed to upsert orgUser");
|
|
81
|
-
return {
|
|
82
|
-
...row,
|
|
83
|
-
role: parseRole(row.role)
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
71
|
async upsertPendingInvite(orgId, input) {
|
|
87
72
|
const [row] = await this.db.insert(invites).values({
|
|
88
73
|
orgId,
|
|
@@ -113,6 +98,17 @@ var DrizzleOrganizationsRepository = class {
|
|
|
113
98
|
const [row] = await this.db.select().from(invites).where(eq(invites.tokenHash, tokenHash)).limit(1);
|
|
114
99
|
return row ? toInvite(row) : null;
|
|
115
100
|
}
|
|
101
|
+
async findPendingInvite(orgId, email) {
|
|
102
|
+
const [row] = await this.db.select().from(invites).where(and(eq(invites.orgId, orgId), sql`lower(${invites.email}) = lower(${email})`, eq(invites.status, "pending"))).limit(1);
|
|
103
|
+
return row ? toInvite(row) : null;
|
|
104
|
+
}
|
|
105
|
+
async setInviteToken(orgId, id, tokenHash, expiresAt) {
|
|
106
|
+
const [row] = await this.db.update(invites).set({
|
|
107
|
+
tokenHash,
|
|
108
|
+
expiresAt
|
|
109
|
+
}).where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, "pending"))).returning();
|
|
110
|
+
return row ? toInvite(row) : null;
|
|
111
|
+
}
|
|
116
112
|
async findOrgUser(orgId, userId) {
|
|
117
113
|
const [row] = await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId))).limit(1);
|
|
118
114
|
return row ? {
|
|
@@ -133,6 +129,17 @@ var DrizzleOrganizationsRepository = class {
|
|
|
133
129
|
role: parseRole(row.role)
|
|
134
130
|
} : null;
|
|
135
131
|
}
|
|
132
|
+
async findOrgUsersByIds(orgId, ids) {
|
|
133
|
+
if (ids.length === 0) return [];
|
|
134
|
+
return (await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)))).map((row) => ({
|
|
135
|
+
...row,
|
|
136
|
+
role: parseRole(row.role)
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
async findOrgUserIdsByRole(orgId, roles) {
|
|
140
|
+
if (roles.length === 0) return [];
|
|
141
|
+
return (await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.status, "active"), inArray(orgUsers.role, roles))).orderBy(orgUsers.id)).map((row) => row.id);
|
|
142
|
+
}
|
|
136
143
|
async createOrgUser(input) {
|
|
137
144
|
try {
|
|
138
145
|
const [row] = await this.db.insert(orgUsers).values({
|
|
@@ -151,31 +158,15 @@ var DrizzleOrganizationsRepository = class {
|
|
|
151
158
|
throw err;
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
|
-
async
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
orgUser: {
|
|
164
|
-
...inserted,
|
|
165
|
-
role: parseRole(inserted.role)
|
|
166
|
-
},
|
|
167
|
-
created: true
|
|
168
|
-
};
|
|
169
|
-
const where = and(eq(orgUsers.orgId, input.orgId), eq(orgUsers.userId, input.userId));
|
|
170
|
-
const [existing] = status === "active" ? await this.db.update(orgUsers).set({ status }).where(where).returning() : await this.db.select().from(orgUsers).where(where).limit(1);
|
|
171
|
-
if (!existing) throw new Error("failed to ensure org user");
|
|
172
|
-
return {
|
|
173
|
-
orgUser: {
|
|
174
|
-
...existing,
|
|
175
|
-
role: parseRole(existing.role)
|
|
176
|
-
},
|
|
177
|
-
created: false
|
|
178
|
-
};
|
|
161
|
+
async updateOrgUser(orgId, id, patch) {
|
|
162
|
+
const [row] = await this.db.update(orgUsers).set({
|
|
163
|
+
...patch.role !== void 0 && { role: patch.role },
|
|
164
|
+
...patch.status !== void 0 && { status: patch.status }
|
|
165
|
+
}).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
|
|
166
|
+
return row ? {
|
|
167
|
+
...row,
|
|
168
|
+
role: parseRole(row.role)
|
|
169
|
+
} : null;
|
|
179
170
|
}
|
|
180
171
|
async findStudentOrgUsers(userId) {
|
|
181
172
|
return (await this.db.select().from(orgUsers).where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, "student"))).orderBy(orgUsers.createdAt)).map((row) => ({
|
|
@@ -183,15 +174,17 @@ var DrizzleOrganizationsRepository = class {
|
|
|
183
174
|
role: parseRole(row.role)
|
|
184
175
|
}));
|
|
185
176
|
}
|
|
186
|
-
async cancelPendingInvite(orgId, email) {
|
|
187
|
-
const [row] = await this.db.update(invites).set({ status: "canceled" }).where(and(eq(invites.orgId, orgId), sql`lower(${invites.email}) = lower(${email})`, eq(invites.status, "pending"))).returning();
|
|
188
|
-
return row ? toInvite(row) : null;
|
|
189
|
-
}
|
|
190
177
|
async deleteOrgUser(orgId, id) {
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
try {
|
|
179
|
+
const [row] = await this.db.delete(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
|
|
180
|
+
return row ?? null;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (pgErrorFields(err)?.code === "23503") throw Object.assign(new ConflictError("This person has written posts, comments or messages and cannot be deleted"), { cause: err });
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
193
185
|
}
|
|
194
186
|
};
|
|
187
|
+
translateDbErrors(DrizzleOrganizationsRepository);
|
|
195
188
|
//#endregion
|
|
196
189
|
export { DrizzleOrganizationsRepository };
|
|
197
190
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations.js","names":[],"sources":["../../src/repositories/organizations.ts"],"sourcesContent":["// organizations — Drizzle repository (implements the core outbound port).\nimport { and, eq, sql } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport {\n normalizeRole,\n parseRole,\n type AddOrgUserInput,\n type CreateOrgUserInput,\n type Invite,\n type NewInviteRow,\n type NewOrganizationRow,\n type Organization,\n type OrganizationsRepository,\n type OrgUser,\n type UpdateOrganizationInput,\n} from '@headless-lms/core/organizations';\nimport { invites, organizations, orgUsers } from '../schema/organizations.js';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { ConflictError } from '@headless-lms/core/shared/errors';\nimport { isUniqueViolation } from './pg-errors.js';\n\nconst INVITE_STATUSES = ['pending', 'accepted', 'rejected', 'canceled'] as const;\ntype InviteStatus = (typeof INVITE_STATUSES)[number];\nconst toStatus = (s: string): InviteStatus =>\n (INVITE_STATUSES as readonly string[]).includes(s) ? (s as InviteStatus) : 'pending';\n\nfunction toInvite(row: typeof invites.$inferSelect): Invite {\n return {\n id: row.id,\n orgId: row.orgId,\n email: row.email,\n role: row.role,\n status: row.status,\n invitedBy: row.invitedBy,\n tokenHash: row.tokenHash,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n };\n}\n\nexport class DrizzleOrganizationsRepository implements OrganizationsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async create(input: NewOrganizationRow): Promise<Organization> {\n const [row] = await this.db\n .insert(organizations)\n .values({\n externalId: input.externalId,\n name: input.name,\n slug: input.slug,\n ownerId: input.ownerId,\n id: input.id,\n })\n .returning();\n if (!row) {\n throw new Error('failed to insert organization');\n }\n return row!;\n }\n\n async update(id: string, input: UpdateOrganizationInput): Promise<Organization | null> {\n const [row] = await this.db\n .update(organizations)\n .set({ name: input.name, slug: input.slug })\n .where(eq(organizations.id, id))\n .returning();\n return row ?? null;\n }\n\n async delete(id: string): Promise<Organization> {\n const [row] = await this.db.delete(organizations).where(eq(organizations.id, id)).returning();\n\n if (!row) {\n throw new Error('failed to delete organization');\n }\n return row;\n }\n\n async findById(id: string): Promise<Organization | null> {\n const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));\n return row ?? null;\n }\n\n async findByExternalId(externalId: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n async findBySlug(slug: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.slug, slug))\n .limit(1);\n return row ?? null;\n }\n\n async upsertOrgUser(orgId: string, input: AddOrgUserInput): Promise<OrgUser> {\n const role = normalizeRole(input.role);\n const [row] = await this.db\n .insert(orgUsers)\n .values({ orgId, userId: input.userId, role })\n .onConflictDoUpdate({ target: [orgUsers.orgId, orgUsers.userId], set: { role } })\n .returning();\n if (!row) {\n throw new Error('failed to upsert orgUser');\n }\n return { ...row, role: parseRole(row.role) };\n }\n\n async upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite> {\n const [row] = await this.db\n .insert(invites)\n .values({\n orgId,\n email: input.email,\n role: input.role,\n status: 'pending',\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target: [invites.orgId, invites.email],\n targetWhere: sql`${invites.status} = 'pending'`,\n set: {\n role: input.role,\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n },\n })\n .returning();\n if (!row) {\n throw new Error('failed to upsert invite');\n }\n return toInvite(row);\n }\n\n async setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: toStatus(status) })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id)))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findInviteByTokenHash(tokenHash: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(eq(invites.tokenHash, tokenHash))\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async findOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByUser(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(eq(orgUsers.userId, userId))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async createOrgUser(input: CreateOrgUserInput): Promise<OrgUser> {\n try {\n const [row] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n role: input.role,\n status: input.status ?? 'active',\n })\n .returning();\n if (!row) {\n throw new Error('failed to insert org user');\n }\n return { ...row, role: parseRole(row.role) };\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError('That account already belongs to this organization');\n }\n throw err;\n }\n }\n\n async ensureOrgUser(input: CreateOrgUserInput): Promise<{ orgUser: OrgUser; created: boolean }> {\n const status = input.status ?? 'active';\n const [inserted] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n role: input.role,\n status,\n })\n .onConflictDoNothing({ target: [orgUsers.orgId, orgUsers.userId] })\n .returning();\n if (inserted) {\n return { orgUser: { ...inserted, role: parseRole(inserted.role) }, created: true };\n }\n\n const where = and(eq(orgUsers.orgId, input.orgId), eq(orgUsers.userId, input.userId));\n // Only an acceptance writes. A re-invite asks for 'invited' against a row\n // that already exists, and has nothing to say about it.\n const [existing] =\n status === 'active'\n ? await this.db.update(orgUsers).set({ status }).where(where).returning()\n : await this.db.select().from(orgUsers).where(where).limit(1);\n if (!existing) {\n throw new Error('failed to ensure org user');\n }\n return { orgUser: { ...existing, role: parseRole(existing.role) }, created: false };\n }\n\n async findStudentOrgUsers(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, 'student')))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async cancelPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: 'canceled' })\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, 'pending'),\n ),\n )\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n\n return row ?? null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"organizations.js","names":[],"sources":["../../src/repositories/organizations.ts"],"sourcesContent":["// organizations — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport {\n parseRole,\n type CreateOrgUserInput,\n type Invite,\n type NewInviteRow,\n type NewOrganizationRow,\n type Organization,\n type OrganizationsRepository,\n type OrgUser,\n type Role,\n type UpdateOrganizationInput,\n type UpdateOrgUserInput,\n} from \"@headless-lms/core/organizations\";\nimport { invites, organizations, orgUsers } from \"../schema/organizations.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\nconst INVITE_STATUSES = [\"pending\", \"accepted\", \"rejected\", \"canceled\"] as const;\ntype InviteStatus = (typeof INVITE_STATUSES)[number];\nconst toStatus = (s: string): InviteStatus =>\n (INVITE_STATUSES as readonly string[]).includes(s) ? (s as InviteStatus) : \"pending\";\n\nfunction toInvite(row: typeof invites.$inferSelect): Invite {\n return {\n id: row.id,\n orgId: row.orgId,\n email: row.email,\n role: row.role,\n status: row.status,\n invitedBy: row.invitedBy,\n tokenHash: row.tokenHash,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleOrganizationsRepository implements OrganizationsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async create(input: NewOrganizationRow): Promise<Organization> {\n const [row] = await this.db\n .insert(organizations)\n .values({\n externalId: input.externalId,\n name: input.name,\n slug: input.slug,\n ownerId: input.ownerId,\n id: input.id,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert organization\");\n }\n return row!;\n }\n\n async update(id: string, input: UpdateOrganizationInput): Promise<Organization | null> {\n const [row] = await this.db\n .update(organizations)\n .set({ name: input.name, slug: input.slug })\n .where(eq(organizations.id, id))\n .returning();\n return row ?? null;\n }\n\n async delete(id: string): Promise<Organization> {\n const [row] = await this.db.delete(organizations).where(eq(organizations.id, id)).returning();\n\n if (!row) {\n throw new Error(\"failed to delete organization\");\n }\n return row;\n }\n\n async findById(id: string): Promise<Organization | null> {\n const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));\n return row ?? null;\n }\n\n async findByExternalId(externalId: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n async findBySlug(slug: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.slug, slug))\n .limit(1);\n return row ?? null;\n }\n\n async upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite> {\n const [row] = await this.db\n .insert(invites)\n .values({\n orgId,\n email: input.email,\n role: input.role,\n status: \"pending\",\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target: [invites.orgId, invites.email],\n targetWhere: sql`${invites.status} = 'pending'`,\n set: {\n role: input.role,\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n },\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to upsert invite\");\n }\n return toInvite(row);\n }\n\n async setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: toStatus(status) })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id)))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findInviteByTokenHash(tokenHash: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(eq(invites.tokenHash, tokenHash))\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async findPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, \"pending\"),\n ),\n )\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async setInviteToken(\n orgId: string,\n id: string,\n tokenHash: string,\n expiresAt: Date,\n ): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ tokenHash, expiresAt })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, \"pending\")))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByUser(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(eq(orgUsers.userId, userId))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)));\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]> {\n if (roles.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n eq(orgUsers.status, \"active\"),\n inArray(orgUsers.role, roles),\n ),\n )\n .orderBy(orgUsers.id);\n return rows.map((row) => row.id);\n }\n\n async createOrgUser(input: CreateOrgUserInput): Promise<OrgUser> {\n try {\n const [row] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n role: input.role,\n status: input.status ?? \"active\",\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert org user\");\n }\n return { ...row, role: parseRole(row.role) };\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"That account already belongs to this organization\");\n }\n throw err;\n }\n }\n\n async updateOrgUser(\n orgId: string,\n id: string,\n patch: UpdateOrgUserInput,\n ): Promise<OrgUser | null> {\n const [row] = await this.db\n .update(orgUsers)\n .set({\n ...(patch.role !== undefined && { role: patch.role }),\n ...(patch.status !== undefined && { status: patch.status }),\n })\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findStudentOrgUsers(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, \"student\")))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n try {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ?? null;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(\n new ConflictError(\n \"This person has written posts, comments or messages and cannot be deleted\",\n ),\n { cause: err },\n );\n }\n throw err;\n }\n }\n}\ntranslateDbErrors(DrizzleOrganizationsRepository);\n"],"mappings":";;;;;;;AAsBA,MAAM,kBAAkB;CAAC;CAAW;CAAY;CAAY;AAAU;AAEtE,MAAM,YAAY,MACf,gBAAsC,SAAS,CAAC,IAAK,IAAqB;AAE7E,SAAS,SAAS,KAA0C;CAC1D,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,OAAO,IAAI;EACX,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,WAAW,IAAI;EACf,WAAW,IAAI;EACf,WAAW,IAAI;EACf,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,iCAAb,MAA+E;CAE1D;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAkD;EAC7D,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,aAAa,CAAC,CACrB,OAAO;GACN,YAAY,MAAM;GAClB,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,IAAI,MAAM;EACZ,CAAC,CAAC,CACD,UAAU;EACb,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,+BAA+B;EAEjD,OAAO;CACT;CAEA,MAAM,OAAO,IAAY,OAA8D;EACrF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,aAAa,CAAC,CACrB,IAAI;GAAE,MAAM,MAAM;GAAM,MAAM,MAAM;EAAK,CAAC,CAAC,CAC3C,MAAM,GAAG,cAAc,IAAI,EAAE,CAAC,CAAC,CAC/B,UAAU;EACb,OAAO,OAAO;CAChB;CAEA,MAAM,OAAO,IAAmC;EAC9C,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,OAAO,aAAa,CAAC,CAAC,MAAM,GAAG,cAAc,IAAI,EAAE,CAAC,CAAC,CAAC,UAAU;EAE5F,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,+BAA+B;EAEjD,OAAO;CACT;CAEA,MAAM,SAAS,IAA0C;EACvD,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,MAAM,GAAG,cAAc,IAAI,EAAE,CAAC;EACvF,OAAO,OAAO;CAChB;CAEA,MAAM,iBAAiB,YAAkD;EACvE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,aAAa,CAAC,CACnB,MAAM,GAAG,cAAc,YAAY,UAAU,CAAC,CAAC,CAC/C,MAAM,CAAC;EACV,OAAO,OAAO;CAChB;CAEA,MAAM,WAAW,MAA4C;EAC3D,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,aAAa,CAAC,CACnB,MAAM,GAAG,cAAc,MAAM,IAAI,CAAC,CAAC,CACnC,MAAM,CAAC;EACV,OAAO,OAAO;CAChB;CAEA,MAAM,oBAAoB,OAAe,OAAsC;EAC7E,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,OAAO,CAAC,CACf,OAAO;GACN;GACA,OAAO,MAAM;GACb,MAAM,MAAM;GACZ,QAAQ;GACR,WAAW,MAAM;GACjB,WAAW,MAAM;GACjB,WAAW,MAAM;EACnB,CAAC,CAAC,CACD,mBAAmB;GAClB,QAAQ,CAAC,QAAQ,OAAO,QAAQ,KAAK;GACrC,aAAa,GAAG,GAAG,QAAQ,OAAO;GAClC,KAAK;IACH,MAAM,MAAM;IACZ,WAAW,MAAM;IACjB,WAAW,MAAM;IACjB,WAAW,MAAM;GACnB;EACF,CAAC,CAAC,CACD,UAAU;EACb,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,yBAAyB;EAE3C,OAAO,SAAS,GAAG;CACrB;CAEA,MAAM,gBAAgB,OAAe,IAAY,QAAwC;EACvF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,OAAO,CAAC,CACf,IAAI,EAAE,QAAQ,SAAS,MAAM,EAAE,CAAC,CAAC,CACjC,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CACxD,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,sBAAsB,WAA2C;EACrE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,OAAO,CAAC,CACb,MAAM,GAAG,QAAQ,WAAW,SAAS,CAAC,CAAC,CACvC,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,kBAAkB,OAAe,OAAuC;EAC5E,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,OAAO,CAAC,CACb,MACC,IACE,GAAG,QAAQ,OAAO,KAAK,GACvB,GAAG,SAAS,QAAQ,MAAM,YAAY,MAAM,IAC5C,GAAG,QAAQ,QAAQ,SAAS,CAC9B,CACF,CAAC,CACA,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,eACJ,OACA,IACA,WACA,WACwB;EACxB,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,OAAO,CAAC,CACf,IAAI;GAAE;GAAW;EAAU,CAAC,CAAC,CAC7B,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,EAAE,GAAG,GAAG,QAAQ,QAAQ,SAAS,CAAC,CAAC,CAAC,CACvF,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,YAAY,OAAe,QAAyC;EACxE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,QAAQ,MAAM,CAAC,CAAC,CAAC,CAClE,MAAM,CAAC;EACV,OAAO,MAAM;GAAE,GAAG;GAAK,MAAM,UAAU,IAAI,IAAI;EAAE,IAAI;CACvD;CAEA,MAAM,mBAAmB,QAAoC;EAM3D,QAAO,MALY,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,GAAG,SAAS,QAAQ,MAAM,CAAC,CAAC,CAClC,QAAQ,SAAS,SAAS,EAAA,CACjB,KAAK,SAAS;GAAE,GAAG;GAAK,MAAM,UAAU,IAAI,IAAI;EAAE,EAAE;CAClE;CAEA,MAAM,gBAAgB,OAAe,IAAqC;EACxE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAC1D,MAAM,CAAC;EACV,OAAO,MAAM;GAAE,GAAG;GAAK,MAAM,UAAU,IAAI,IAAI;EAAE,IAAI;CACvD;CAEA,MAAM,kBAAkB,OAAe,KAAmC;EACxE,IAAI,IAAI,WAAW,GACjB,OAAO,CAAC;EAMV,QAAO,MAJY,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,QAAQ,SAAS,IAAI,GAAG,CAAC,CAAC,EAAA,CACtD,KAAK,SAAS;GAAE,GAAG;GAAK,MAAM,UAAU,IAAI,IAAI;EAAE,EAAE;CAClE;CAEA,MAAM,qBAAqB,OAAe,OAAkC;EAC1E,IAAI,MAAM,WAAW,GACnB,OAAO,CAAC;EAaV,QAAO,MAXY,KAAK,GACrB,OAAO,EAAE,IAAI,SAAS,GAAG,CAAC,CAAC,CAC3B,KAAK,QAAQ,CAAC,CACd,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,QAAQ,QAAQ,GAC5B,QAAQ,SAAS,MAAM,KAAK,CAC9B,CACF,CAAC,CACA,QAAQ,SAAS,EAAE,EAAA,CACV,KAAK,QAAQ,IAAI,EAAE;CACjC;CAEA,MAAM,cAAc,OAA6C;EAC/D,IAAI;GACF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,OAAO;IACN,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,MAAM,MAAM;IACZ,QAAQ,MAAM,UAAU;GAC1B,CAAC,CAAC,CACD,UAAU;GACb,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,2BAA2B;GAE7C,OAAO;IAAE,GAAG;IAAK,MAAM,UAAU,IAAI,IAAI;GAAE;EAC7C,SAAS,KAAK;GACZ,IAAI,kBAAkB,GAAG,GACvB,MAAM,IAAI,cAAc,mDAAmD;GAE7E,MAAM;EACR;CACF;CAEA,MAAM,cACJ,OACA,IACA,OACyB;EACzB,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,IAAI;GACH,GAAI,MAAM,SAAS,KAAA,KAAa,EAAE,MAAM,MAAM,KAAK;GACnD,GAAI,MAAM,WAAW,KAAA,KAAa,EAAE,QAAQ,MAAM,OAAO;EAC3D,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAC1D,UAAU;EACb,OAAO,MAAM;GAAE,GAAG;GAAK,MAAM,UAAU,IAAI,IAAI;EAAE,IAAI;CACvD;CAEA,MAAM,oBAAoB,QAAoC;EAM5D,QAAO,MALY,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,IAAI,GAAG,SAAS,QAAQ,MAAM,GAAG,GAAG,SAAS,MAAM,SAAS,CAAC,CAAC,CAAC,CACrE,QAAQ,SAAS,SAAS,EAAA,CACjB,KAAK,SAAS;GAAE,GAAG;GAAK,MAAM,UAAU,IAAI,IAAI;EAAE,EAAE;CAClE;CAEA,MAAM,cAAc,OAAe,IAAqC;EACtE,IAAI;GACF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAC1D,UAAU;GACb,OAAO,OAAO;EAChB,SAAS,KAAK;GACZ,IAAI,cAAc,GAAG,CAAC,EAAE,SAAS,SAC/B,MAAM,OAAO,OACX,IAAI,cACF,2EACF,GACA,EAAE,OAAO,IAAI,CACf;GAEF,MAAM;EACR;CACF;AACF;AACA,kBAAkB,8BAA8B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { eventOutbox } from "../schema/outbox.js";
|
|
2
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
2
3
|
import { and, asc, eq, isNull, lt, lte, sql } from "drizzle-orm";
|
|
3
4
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
5
|
//#region src/repositories/outbox.ts
|
|
@@ -50,6 +51,8 @@ var DrizzleOutboxStore = class {
|
|
|
50
51
|
}).where(eq(eventOutbox.id, id));
|
|
51
52
|
}
|
|
52
53
|
};
|
|
54
|
+
translateDbErrors(DrizzleOutboxAppender);
|
|
55
|
+
translateDbErrors(DrizzleOutboxStore);
|
|
53
56
|
//#endregion
|
|
54
57
|
export { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS };
|
|
55
58
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbox.js","names":[],"sources":["../../src/repositories/outbox.ts"],"sourcesContent":["import { and, asc, eq, isNull, lt, lte, sql } from
|
|
1
|
+
{"version":3,"file":"outbox.js","names":[],"sources":["../../src/repositories/outbox.ts"],"sourcesContent":["import { and, asc, eq, isNull, lt, lte, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DomainEvent,\n NewDomainEvent,\n OutboxAppender,\n OutboxMessage,\n OutboxStore,\n Logger,\n} from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { EventOutbox } from \"@headless-lms/core/schemas\";\nimport type { DbExecutor } from \"../client.js\";\nimport { eventOutbox } from \"../schema/outbox.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\n/** A message that fails this many dispatches is parked: fetchBatch stops\n * claiming it, leaving the row (with its last_error) for inspection. */\nexport const OUTBOX_MAX_ATTEMPTS = 10;\n\nexport class DrizzleOutboxAppender implements OutboxAppender {\n constructor(\n private readonly tx: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async append<E extends NewDomainEvent>(events: E[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n await this.tx.insert(eventOutbox).values(\n events.map((event) => ({\n type: event.type,\n orgId: event.orgId,\n payload: event as unknown as EventOutbox[\"payload\"],\n })),\n );\n }\n}\n\nexport class DrizzleOutboxStore implements OutboxStore {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async fetchBatch(limit: number): Promise<OutboxMessage[]> {\n const rows = await this.db.transaction((tx) =>\n tx\n .select()\n .from(eventOutbox)\n .where(\n and(\n isNull(eventOutbox.processedAt),\n lte(eventOutbox.nextAttemptAt, new Date()),\n lt(eventOutbox.attempts, OUTBOX_MAX_ATTEMPTS),\n ),\n )\n .orderBy(asc(eventOutbox.id))\n .limit(limit)\n .for(\"update\", { skipLocked: true }),\n );\n return rows.map((row) => ({\n id: row.id,\n attempts: row.attempts,\n payload: {\n ...row.payload,\n id: row.id,\n occurredAt: row.createdAt.toISOString(),\n } as unknown as DomainEvent,\n }));\n }\n\n async markProcessed(id: string): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({ processedAt: new Date() })\n .where(eq(eventOutbox.id, id));\n }\n\n async markFailed(id: string, error: string, nextAttemptAt: Date): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({\n attempts: sql`${eventOutbox.attempts} + 1`,\n lastError: error,\n nextAttemptAt,\n })\n .where(eq(eventOutbox.id, id));\n }\n}\ntranslateDbErrors(DrizzleOutboxAppender);\ntranslateDbErrors(DrizzleOutboxStore);\n"],"mappings":";;;;;;;AAkBA,MAAa,sBAAsB;AAEnC,IAAa,wBAAb,MAA6D;CAExC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAiC,QAA4B;EACjE,IAAI,OAAO,WAAW,GACpB;EAEF,MAAM,KAAK,GAAG,OAAO,WAAW,CAAC,CAAC,OAChC,OAAO,KAAK,WAAW;GACrB,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,SAAS;EACX,EAAE,CACJ;CACF;AACF;AAEA,IAAa,qBAAb,MAAuD;CAElC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,WAAW,OAAyC;EAgBxD,QAAO,MAfY,KAAK,GAAG,aAAa,OACtC,GACG,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MACC,IACE,OAAO,YAAY,WAAW,GAC9B,IAAI,YAAY,+BAAe,IAAI,KAAK,CAAC,GACzC,GAAG,YAAY,UAAA,EAA6B,CAC9C,CACF,CAAC,CACA,QAAQ,IAAI,YAAY,EAAE,CAAC,CAAC,CAC5B,MAAM,KAAK,CAAC,CACZ,IAAI,UAAU,EAAE,YAAY,KAAK,CAAC,CACvC,EAAA,CACY,KAAK,SAAS;GACxB,IAAI,IAAI;GACR,UAAU,IAAI;GACd,SAAS;IACP,GAAG,IAAI;IACP,IAAI,IAAI;IACR,YAAY,IAAI,UAAU,YAAY;GACxC;EACF,EAAE;CACJ;CAEA,MAAM,cAAc,IAA2B;EAC7C,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI,EAAE,6BAAa,IAAI,KAAK,EAAE,CAAC,CAAC,CAChC,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;CAEA,MAAM,WAAW,IAAY,OAAe,eAAoC;EAC9E,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI;GACH,UAAU,GAAG,GAAG,YAAY,SAAS;GACrC,WAAW;GACX;EACF,CAAC,CAAC,CACD,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;AACF;AACA,kBAAkB,qBAAqB;AACvC,kBAAkB,kBAAkB"}
|
|
@@ -1,11 +1,59 @@
|
|
|
1
|
+
import { ConflictError } from "@headless-lms/core/shared/errors";
|
|
1
2
|
//#region src/repositories/pg-errors.d.ts
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `cause`), so both levels are checked.
|
|
4
|
+
* Structured fields off a node-postgres DatabaseError. The driver (and
|
|
5
|
+
* drizzle's DrizzleQueryError) sometimes wraps the original error behind
|
|
6
|
+
* `cause`, so both levels are checked.
|
|
7
7
|
*/
|
|
8
|
+
type PgErrorFields = {
|
|
9
|
+
code: string;
|
|
10
|
+
constraint?: string;
|
|
11
|
+
table?: string;
|
|
12
|
+
column?: string;
|
|
13
|
+
detail?: string;
|
|
14
|
+
};
|
|
15
|
+
declare function pgErrorFields(err: unknown): PgErrorFields | undefined;
|
|
16
|
+
/** Postgres unique_violation (23505). */
|
|
8
17
|
declare function isUniqueViolation(err: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A database failure sanitized for logging: the driver's message, the SQL
|
|
20
|
+
* text, and the structured pg fields — never the bind params, which a
|
|
21
|
+
* DrizzleQueryError embeds in its message and can carry PII.
|
|
22
|
+
*/
|
|
23
|
+
declare class DbQueryError extends Error {
|
|
24
|
+
readonly pg?: PgErrorFields;
|
|
25
|
+
readonly query?: string;
|
|
26
|
+
constructor(message: string, opts: {
|
|
27
|
+
pg?: PgErrorFields;
|
|
28
|
+
query?: string;
|
|
29
|
+
cause?: Error;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What the unit of work rethrows: constraint violations become domain
|
|
34
|
+
* ConflictErrors, other query failures become sanitized DbQueryErrors, and
|
|
35
|
+
* anything non-database passes through untouched.
|
|
36
|
+
*/
|
|
37
|
+
declare function translateDbError(err: unknown): unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Route every method of a repository class through translateDbError, so no
|
|
40
|
+
* drizzle or pg error ever leaves this package — regardless of whether the
|
|
41
|
+
* call runs inside a unit of work. Applied once per class, right below its
|
|
42
|
+
* declaration.
|
|
43
|
+
*/
|
|
44
|
+
declare function translateDbErrors(cls: {
|
|
45
|
+
prototype: object;
|
|
46
|
+
}): void;
|
|
47
|
+
/**
|
|
48
|
+
* Only unique violations (23505) become ConflictErrors: a duplicate is the
|
|
49
|
+
* one constraint failure the caller can act on, so it earns a 409 with a
|
|
50
|
+
* client-facing message — the offending column and the caller's own value,
|
|
51
|
+
* never pg's `detail`. Every other constraint failure (foreign keys
|
|
52
|
+
* included) is a server-side integrity problem: it stays an internal error,
|
|
53
|
+
* sanitized by translateDbError and logged, and the client sees a generic
|
|
54
|
+
* 500. The raw error stays on `cause` for server-side logs.
|
|
55
|
+
*/
|
|
56
|
+
declare function translateConstraintViolation(err: unknown): ConflictError | undefined;
|
|
9
57
|
//#endregion
|
|
10
|
-
export { isUniqueViolation };
|
|
58
|
+
export { DbQueryError, PgErrorFields, isUniqueViolation, pgErrorFields, translateConstraintViolation, translateDbError, translateDbErrors };
|
|
11
59
|
//# sourceMappingURL=pg-errors.d.ts.map
|