@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
|
@@ -1,15 +1,106 @@
|
|
|
1
|
+
import { ConflictError } from "@headless-lms/core/shared/errors";
|
|
1
2
|
//#region src/repositories/pg-errors.ts
|
|
3
|
+
const PG_CODE = /^[0-9A-Z]{5}$/;
|
|
4
|
+
function pgErrorFields(err) {
|
|
5
|
+
for (const level of [err, err?.cause]) {
|
|
6
|
+
const e = level;
|
|
7
|
+
if (typeof e?.code === "string" && PG_CODE.test(e.code)) return {
|
|
8
|
+
code: e.code,
|
|
9
|
+
...typeof e.constraint === "string" && { constraint: e.constraint },
|
|
10
|
+
...typeof e.table === "string" && { table: e.table },
|
|
11
|
+
...typeof e.column === "string" && { column: e.column },
|
|
12
|
+
...typeof e.detail === "string" && { detail: e.detail }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Postgres unique_violation (23505). */
|
|
17
|
+
function isUniqueViolation(err) {
|
|
18
|
+
return pgErrorFields(err)?.code === "23505";
|
|
19
|
+
}
|
|
2
20
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `cause`), so both levels are checked.
|
|
21
|
+
* A database failure sanitized for logging: the driver's message, the SQL
|
|
22
|
+
* text, and the structured pg fields — never the bind params, which a
|
|
23
|
+
* DrizzleQueryError embeds in its message and can carry PII.
|
|
7
24
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
25
|
+
var DbQueryError = class extends Error {
|
|
26
|
+
pg;
|
|
27
|
+
query;
|
|
28
|
+
constructor(message, opts) {
|
|
29
|
+
super(message, opts.cause && { cause: opts.cause });
|
|
30
|
+
this.name = "DbQueryError";
|
|
31
|
+
if (opts.pg) this.pg = opts.pg;
|
|
32
|
+
if (opts.query) this.query = opts.query;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What the unit of work rethrows: constraint violations become domain
|
|
37
|
+
* ConflictErrors, other query failures become sanitized DbQueryErrors, and
|
|
38
|
+
* anything non-database passes through untouched.
|
|
39
|
+
*/
|
|
40
|
+
function translateDbError(err) {
|
|
41
|
+
if (err instanceof ConflictError || err instanceof DbQueryError) return err;
|
|
42
|
+
const conflict = translateConstraintViolation(err);
|
|
43
|
+
if (conflict) return conflict;
|
|
44
|
+
const e = err;
|
|
45
|
+
if (typeof e?.query === "string" && Array.isArray(e.params)) {
|
|
46
|
+
const cause = e.cause instanceof Error ? e.cause : void 0;
|
|
47
|
+
const pg = pgErrorFields(err);
|
|
48
|
+
return new DbQueryError(cause?.message ?? "database query failed", {
|
|
49
|
+
...cause && { cause },
|
|
50
|
+
query: e.query,
|
|
51
|
+
...pg && { pg }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return err;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Route every method of a repository class through translateDbError, so no
|
|
58
|
+
* drizzle or pg error ever leaves this package — regardless of whether the
|
|
59
|
+
* call runs inside a unit of work. Applied once per class, right below its
|
|
60
|
+
* declaration.
|
|
61
|
+
*/
|
|
62
|
+
function translateDbErrors(cls) {
|
|
63
|
+
const proto = cls.prototype;
|
|
64
|
+
for (const name of Object.getOwnPropertyNames(proto)) {
|
|
65
|
+
if (name === "constructor") continue;
|
|
66
|
+
const desc = Object.getOwnPropertyDescriptor(proto, name);
|
|
67
|
+
if (!desc || typeof desc.value !== "function") continue;
|
|
68
|
+
const original = desc.value;
|
|
69
|
+
proto[name] = function(...args) {
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = original.apply(this, args);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw translateDbError(err);
|
|
75
|
+
}
|
|
76
|
+
if (result instanceof Promise) return result.catch((err) => {
|
|
77
|
+
throw translateDbError(err);
|
|
78
|
+
});
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** `Key (slug)=(orgie) already exists.` → ['slug', 'orgie'] */
|
|
84
|
+
const KEY_DETAIL = /^Key \((.+?)\)=\((.+?)\)/;
|
|
85
|
+
/**
|
|
86
|
+
* Only unique violations (23505) become ConflictErrors: a duplicate is the
|
|
87
|
+
* one constraint failure the caller can act on, so it earns a 409 with a
|
|
88
|
+
* client-facing message — the offending column and the caller's own value,
|
|
89
|
+
* never pg's `detail`. Every other constraint failure (foreign keys
|
|
90
|
+
* included) is a server-side integrity problem: it stays an internal error,
|
|
91
|
+
* sanitized by translateDbError and logged, and the client sees a generic
|
|
92
|
+
* 500. The raw error stays on `cause` for server-side logs.
|
|
93
|
+
*/
|
|
94
|
+
function translateConstraintViolation(err) {
|
|
95
|
+
const pg = pgErrorFields(err);
|
|
96
|
+
if (pg?.code !== "23505") return;
|
|
97
|
+
const key = pg.detail?.match(KEY_DETAIL);
|
|
98
|
+
const column = key?.[1];
|
|
99
|
+
const value = key?.[2];
|
|
100
|
+
const conflict = new ConflictError(column && value && !column.includes(",") ? `${column} "${value}" is already in use` : "This already exists");
|
|
101
|
+
return Object.assign(conflict, { cause: err });
|
|
11
102
|
}
|
|
12
103
|
//#endregion
|
|
13
|
-
export { isUniqueViolation };
|
|
104
|
+
export { DbQueryError, isUniqueViolation, pgErrorFields, translateConstraintViolation, translateDbError, translateDbErrors };
|
|
14
105
|
|
|
15
106
|
//# sourceMappingURL=pg-errors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-errors.js","names":[],"sources":["../../src/repositories/pg-errors.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"pg-errors.js","names":[],"sources":["../../src/repositories/pg-errors.ts"],"sourcesContent":["import { ConflictError } from \"@headless-lms/core/shared/errors\";\n\n/**\n * Structured fields off a node-postgres DatabaseError. The driver (and\n * drizzle's DrizzleQueryError) sometimes wraps the original error behind\n * `cause`, so both levels are checked.\n */\nexport type PgErrorFields = {\n code: string;\n constraint?: string;\n table?: string;\n column?: string;\n detail?: string;\n};\n\nconst PG_CODE = /^[0-9A-Z]{5}$/;\n\nexport function pgErrorFields(err: unknown): PgErrorFields | undefined {\n for (const level of [err, (err as { cause?: unknown } | undefined)?.cause]) {\n const e = level as Partial<PgErrorFields> | undefined;\n if (typeof e?.code === \"string\" && PG_CODE.test(e.code)) {\n return {\n code: e.code,\n ...(typeof e.constraint === \"string\" && { constraint: e.constraint }),\n ...(typeof e.table === \"string\" && { table: e.table }),\n ...(typeof e.column === \"string\" && { column: e.column }),\n ...(typeof e.detail === \"string\" && { detail: e.detail }),\n };\n }\n }\n return undefined;\n}\n\n/** Postgres unique_violation (23505). */\nexport function isUniqueViolation(err: unknown): boolean {\n return pgErrorFields(err)?.code === \"23505\";\n}\n\n/**\n * A database failure sanitized for logging: the driver's message, the SQL\n * text, and the structured pg fields — never the bind params, which a\n * DrizzleQueryError embeds in its message and can carry PII.\n */\nexport class DbQueryError extends Error {\n readonly pg?: PgErrorFields;\n readonly query?: string;\n\n constructor(message: string, opts: { pg?: PgErrorFields; query?: string; cause?: Error }) {\n super(message, opts.cause && { cause: opts.cause });\n this.name = \"DbQueryError\";\n if (opts.pg) {\n this.pg = opts.pg;\n }\n if (opts.query) {\n this.query = opts.query;\n }\n }\n}\n\n/**\n * What the unit of work rethrows: constraint violations become domain\n * ConflictErrors, other query failures become sanitized DbQueryErrors, and\n * anything non-database passes through untouched.\n */\nexport function translateDbError(err: unknown): unknown {\n if (err instanceof ConflictError || err instanceof DbQueryError) {\n return err;\n }\n const conflict = translateConstraintViolation(err);\n if (conflict) {\n return conflict;\n }\n const e = err as { query?: unknown; params?: unknown; cause?: unknown; message?: unknown };\n if (typeof e?.query === \"string\" && Array.isArray(e.params)) {\n const cause = e.cause instanceof Error ? e.cause : undefined;\n const pg = pgErrorFields(err);\n return new DbQueryError(cause?.message ?? \"database query failed\", {\n ...(cause && { cause }),\n query: e.query,\n ...(pg && { pg }),\n });\n }\n return err;\n}\n\n/**\n * Route every method of a repository class through translateDbError, so no\n * drizzle or pg error ever leaves this package — regardless of whether the\n * call runs inside a unit of work. Applied once per class, right below its\n * declaration.\n */\nexport function translateDbErrors(cls: { prototype: object }): void {\n const proto = cls.prototype as Record<string, unknown>;\n for (const name of Object.getOwnPropertyNames(proto)) {\n if (name === \"constructor\") {\n continue;\n }\n const desc = Object.getOwnPropertyDescriptor(proto, name);\n if (!desc || typeof desc.value !== \"function\") {\n continue;\n }\n const original = desc.value as (...args: unknown[]) => unknown;\n proto[name] = function (this: unknown, ...args: unknown[]) {\n let result: unknown;\n try {\n result = original.apply(this, args);\n } catch (err) {\n throw translateDbError(err);\n }\n if (result instanceof Promise) {\n return result.catch((err: unknown) => {\n throw translateDbError(err);\n });\n }\n return result;\n };\n }\n}\n\n/** `Key (slug)=(orgie) already exists.` → ['slug', 'orgie'] */\nconst KEY_DETAIL = /^Key \\((.+?)\\)=\\((.+?)\\)/;\n\n/**\n * Only unique violations (23505) become ConflictErrors: a duplicate is the\n * one constraint failure the caller can act on, so it earns a 409 with a\n * client-facing message — the offending column and the caller's own value,\n * never pg's `detail`. Every other constraint failure (foreign keys\n * included) is a server-side integrity problem: it stays an internal error,\n * sanitized by translateDbError and logged, and the client sees a generic\n * 500. The raw error stays on `cause` for server-side logs.\n */\nexport function translateConstraintViolation(err: unknown): ConflictError | undefined {\n const pg = pgErrorFields(err);\n if (pg?.code !== \"23505\") {\n return undefined;\n }\n const key = pg.detail?.match(KEY_DETAIL);\n const column = key?.[1];\n const value = key?.[2];\n const conflict = new ConflictError(\n column && value && !column.includes(\",\")\n ? `${column} \"${value}\" is already in use`\n : \"This already exists\",\n );\n return Object.assign(conflict, { cause: err });\n}\n"],"mappings":";;AAeA,MAAM,UAAU;AAEhB,SAAgB,cAAc,KAAyC;CACrE,KAAK,MAAM,SAAS,CAAC,KAAM,KAAyC,KAAK,GAAG;EAC1E,MAAM,IAAI;EACV,IAAI,OAAO,GAAG,SAAS,YAAY,QAAQ,KAAK,EAAE,IAAI,GACpD,OAAO;GACL,MAAM,EAAE;GACR,GAAI,OAAO,EAAE,eAAe,YAAY,EAAE,YAAY,EAAE,WAAW;GACnE,GAAI,OAAO,EAAE,UAAU,YAAY,EAAE,OAAO,EAAE,MAAM;GACpD,GAAI,OAAO,EAAE,WAAW,YAAY,EAAE,QAAQ,EAAE,OAAO;GACvD,GAAI,OAAO,EAAE,WAAW,YAAY,EAAE,QAAQ,EAAE,OAAO;EACzD;CAEJ;AAEF;;AAGA,SAAgB,kBAAkB,KAAuB;CACvD,OAAO,cAAc,GAAG,CAAC,EAAE,SAAS;AACtC;;;;;;AAOA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,SAAiB,MAA6D;EACxF,MAAM,SAAS,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM,CAAC;EAClD,KAAK,OAAO;EACZ,IAAI,KAAK,IACP,KAAK,KAAK,KAAK;EAEjB,IAAI,KAAK,OACP,KAAK,QAAQ,KAAK;CAEtB;AACF;;;;;;AAOA,SAAgB,iBAAiB,KAAuB;CACtD,IAAI,eAAe,iBAAiB,eAAe,cACjD,OAAO;CAET,MAAM,WAAW,6BAA6B,GAAG;CACjD,IAAI,UACF,OAAO;CAET,MAAM,IAAI;CACV,IAAI,OAAO,GAAG,UAAU,YAAY,MAAM,QAAQ,EAAE,MAAM,GAAG;EAC3D,MAAM,QAAQ,EAAE,iBAAiB,QAAQ,EAAE,QAAQ,KAAA;EACnD,MAAM,KAAK,cAAc,GAAG;EAC5B,OAAO,IAAI,aAAa,OAAO,WAAW,yBAAyB;GACjE,GAAI,SAAS,EAAE,MAAM;GACrB,OAAO,EAAE;GACT,GAAI,MAAM,EAAE,GAAG;EACjB,CAAC;CACH;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAAkB,KAAkC;CAClE,MAAM,QAAQ,IAAI;CAClB,KAAK,MAAM,QAAQ,OAAO,oBAAoB,KAAK,GAAG;EACpD,IAAI,SAAS,eACX;EAEF,MAAM,OAAO,OAAO,yBAAyB,OAAO,IAAI;EACxD,IAAI,CAAC,QAAQ,OAAO,KAAK,UAAU,YACjC;EAEF,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,SAAyB,GAAG,MAAiB;GACzD,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,MAAM,MAAM,IAAI;GACpC,SAAS,KAAK;IACZ,MAAM,iBAAiB,GAAG;GAC5B;GACA,IAAI,kBAAkB,SACpB,OAAO,OAAO,OAAO,QAAiB;IACpC,MAAM,iBAAiB,GAAG;GAC5B,CAAC;GAEH,OAAO;EACT;CACF;AACF;;AAGA,MAAM,aAAa;;;;;;;;;;AAWnB,SAAgB,6BAA6B,KAAyC;CACpF,MAAM,KAAK,cAAc,GAAG;CAC5B,IAAI,IAAI,SAAS,SACf;CAEF,MAAM,MAAM,GAAG,QAAQ,MAAM,UAAU;CACvC,MAAM,SAAS,MAAM;CACrB,MAAM,QAAQ,MAAM;CACpB,MAAM,WAAW,IAAI,cACnB,UAAU,SAAS,CAAC,OAAO,SAAS,GAAG,IACnC,GAAG,OAAO,IAAI,MAAM,uBACpB,qBACN;CACA,OAAO,OAAO,OAAO,UAAU,EAAE,OAAO,IAAI,CAAC;AAC/C"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Logger, Page, Post, PostFlag, PostFlagResolution, ReactionEmoji } from "@headless-lms/core/types";
|
|
3
|
+
import { BookmarkQuery, FeedQuery, PageQuery, PostPatch, PostReactions, PostRepository } from "@headless-lms/core/discussion";
|
|
4
|
+
//#region src/repositories/posts.d.ts
|
|
5
|
+
declare class DrizzlePostRepository implements PostRepository {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
|
+
insert(orgId: string, post: Post): Promise<Post>;
|
|
10
|
+
update(orgId: string, id: string, patch: PostPatch): Promise<Post | null>;
|
|
11
|
+
find(orgId: string, id: string): Promise<Post | null>;
|
|
12
|
+
findMany(orgId: string, ids: string[]): Promise<Post[]>;
|
|
13
|
+
listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>>;
|
|
14
|
+
listByAuthor(orgId: string, orgUserId: string, spaceIds: string[], query: PageQuery): Promise<Page<Post>>;
|
|
15
|
+
listBookmarked(orgId: string, orgUserId: string, spaceIds: string[], query: BookmarkQuery): Promise<Page<Post>>;
|
|
16
|
+
listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]>;
|
|
17
|
+
listDue(orgId: string, now: Date, limit: number, afterId?: string): Promise<Post[]>;
|
|
18
|
+
hasDue(orgId: string, now: Date): Promise<boolean>;
|
|
19
|
+
reactionsOf(orgId: string, postIds: string[], viewerId: string): Promise<Record<string, PostReactions>>;
|
|
20
|
+
setReaction(orgId: string, postId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<void>;
|
|
21
|
+
follow(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
22
|
+
followerIds(orgId: string, postId: string): Promise<string[]>;
|
|
23
|
+
followedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
|
|
24
|
+
unfollow(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
25
|
+
bookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
26
|
+
unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
27
|
+
bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
|
|
28
|
+
insertFlag(orgId: string, flag: PostFlag): Promise<PostFlag | null>;
|
|
29
|
+
findFlag(orgId: string, postId: string, orgUserId: string): Promise<PostFlag | null>;
|
|
30
|
+
listOpenFlags(orgId: string, spaceId: string, limit: number): Promise<PostFlag[]>;
|
|
31
|
+
resolveFlags(orgId: string, postId: string, resolved: {
|
|
32
|
+
resolvedAt: Date;
|
|
33
|
+
resolvedBy: string;
|
|
34
|
+
resolution: PostFlagResolution;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { DrizzlePostRepository };
|
|
39
|
+
//# sourceMappingURL=posts.d.ts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { postBookmarks, postFlags, postFollows, postReactions, posts } from "../schema/discussion.js";
|
|
2
|
+
import "../schema/index.js";
|
|
3
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
4
|
+
import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm";
|
|
5
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
6
|
+
//#region src/repositories/posts.ts
|
|
7
|
+
function toPost(row) {
|
|
8
|
+
return {
|
|
9
|
+
orgId: row.orgId,
|
|
10
|
+
id: row.id,
|
|
11
|
+
communityId: row.communityId,
|
|
12
|
+
spaceId: row.spaceId,
|
|
13
|
+
orgUserId: row.orgUserId,
|
|
14
|
+
topicId: row.topicId,
|
|
15
|
+
title: row.title,
|
|
16
|
+
body: row.body,
|
|
17
|
+
extra: row.extra,
|
|
18
|
+
coverAssetId: row.coverAssetId,
|
|
19
|
+
status: row.status,
|
|
20
|
+
scheduledAt: row.scheduledAt,
|
|
21
|
+
publishedAt: row.publishedAt,
|
|
22
|
+
removedBy: row.removedBy,
|
|
23
|
+
pinned: row.pinned,
|
|
24
|
+
commentsClosed: row.commentsClosed,
|
|
25
|
+
reactionsEnabled: row.reactionsEnabled,
|
|
26
|
+
createdAt: row.createdAt,
|
|
27
|
+
updatedAt: row.updatedAt
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function toFlag(row) {
|
|
31
|
+
return {
|
|
32
|
+
orgId: row.orgId,
|
|
33
|
+
id: row.id,
|
|
34
|
+
postId: row.postId,
|
|
35
|
+
orgUserId: row.orgUserId,
|
|
36
|
+
reason: row.reason,
|
|
37
|
+
resolvedAt: row.resolvedAt,
|
|
38
|
+
resolution: row.resolution,
|
|
39
|
+
resolvedBy: row.resolvedBy,
|
|
40
|
+
createdAt: row.createdAt,
|
|
41
|
+
updatedAt: row.updatedAt
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var DrizzlePostRepository = class {
|
|
45
|
+
db;
|
|
46
|
+
logger;
|
|
47
|
+
constructor(db, logger = noopLogger) {
|
|
48
|
+
this.db = db;
|
|
49
|
+
this.logger = logger;
|
|
50
|
+
}
|
|
51
|
+
async insert(orgId, post) {
|
|
52
|
+
const [row] = await this.db.insert(posts).values({
|
|
53
|
+
orgId,
|
|
54
|
+
id: post.id,
|
|
55
|
+
communityId: post.communityId,
|
|
56
|
+
spaceId: post.spaceId,
|
|
57
|
+
orgUserId: post.orgUserId,
|
|
58
|
+
topicId: post.topicId,
|
|
59
|
+
title: post.title,
|
|
60
|
+
body: post.body,
|
|
61
|
+
extra: post.extra,
|
|
62
|
+
coverAssetId: post.coverAssetId,
|
|
63
|
+
status: post.status,
|
|
64
|
+
scheduledAt: post.scheduledAt,
|
|
65
|
+
publishedAt: post.publishedAt,
|
|
66
|
+
removedBy: post.removedBy,
|
|
67
|
+
pinned: post.pinned,
|
|
68
|
+
commentsClosed: post.commentsClosed,
|
|
69
|
+
reactionsEnabled: post.reactionsEnabled,
|
|
70
|
+
createdAt: post.createdAt,
|
|
71
|
+
updatedAt: post.updatedAt
|
|
72
|
+
}).returning();
|
|
73
|
+
this.logger.debug("post inserted", {
|
|
74
|
+
orgId,
|
|
75
|
+
postId: post.id
|
|
76
|
+
});
|
|
77
|
+
return toPost(row);
|
|
78
|
+
}
|
|
79
|
+
async update(orgId, id, patch) {
|
|
80
|
+
const [row] = await this.db.update(posts).set({
|
|
81
|
+
...patch,
|
|
82
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
83
|
+
}).where(and(eq(posts.orgId, orgId), eq(posts.id, id))).returning();
|
|
84
|
+
return row ? toPost(row) : null;
|
|
85
|
+
}
|
|
86
|
+
async find(orgId, id) {
|
|
87
|
+
const [row] = await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.id, id))).limit(1);
|
|
88
|
+
return row ? toPost(row) : null;
|
|
89
|
+
}
|
|
90
|
+
async findMany(orgId, ids) {
|
|
91
|
+
if (ids.length === 0) return [];
|
|
92
|
+
return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), inArray(posts.id, ids)))).map(toPost);
|
|
93
|
+
}
|
|
94
|
+
async listPublished(orgId, spaceIds, query) {
|
|
95
|
+
if (spaceIds.length === 0) return {
|
|
96
|
+
rows: [],
|
|
97
|
+
total: 0,
|
|
98
|
+
page: query.page,
|
|
99
|
+
pageSize: query.pageSize
|
|
100
|
+
};
|
|
101
|
+
const where = and(eq(posts.orgId, orgId), inArray(posts.spaceId, spaceIds), eq(posts.status, "published"), query.topicId ? eq(posts.topicId, query.topicId) : void 0);
|
|
102
|
+
const rows = await this.db.select().from(posts).where(where).orderBy(desc(posts.pinned), desc(posts.publishedAt), desc(posts.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
103
|
+
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(posts).where(where);
|
|
104
|
+
return {
|
|
105
|
+
rows: rows.map(toPost),
|
|
106
|
+
total,
|
|
107
|
+
page: query.page,
|
|
108
|
+
pageSize: query.pageSize
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async listByAuthor(orgId, orgUserId, spaceIds, query) {
|
|
112
|
+
if (spaceIds.length === 0) return {
|
|
113
|
+
rows: [],
|
|
114
|
+
total: 0,
|
|
115
|
+
page: query.page,
|
|
116
|
+
pageSize: query.pageSize
|
|
117
|
+
};
|
|
118
|
+
const where = and(eq(posts.orgId, orgId), eq(posts.orgUserId, orgUserId), inArray(posts.spaceId, spaceIds));
|
|
119
|
+
const rows = await this.db.select().from(posts).where(where).orderBy(desc(posts.createdAt), desc(posts.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
120
|
+
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(posts).where(where);
|
|
121
|
+
return {
|
|
122
|
+
rows: rows.map(toPost),
|
|
123
|
+
total,
|
|
124
|
+
page: query.page,
|
|
125
|
+
pageSize: query.pageSize
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async listBookmarked(orgId, orgUserId, spaceIds, query) {
|
|
129
|
+
if (spaceIds.length === 0) return {
|
|
130
|
+
rows: [],
|
|
131
|
+
total: 0,
|
|
132
|
+
page: query.page,
|
|
133
|
+
pageSize: query.pageSize
|
|
134
|
+
};
|
|
135
|
+
const where = and(eq(postBookmarks.orgId, orgId), eq(postBookmarks.orgUserId, orgUserId), inArray(posts.spaceId, spaceIds), or(eq(posts.orgUserId, orgUserId), inArray(posts.status, query.visibleStatuses)));
|
|
136
|
+
const rows = await this.db.select({ post: posts }).from(postBookmarks).innerJoin(posts, and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId))).where(where).orderBy(desc(postBookmarks.createdAt), desc(posts.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
137
|
+
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(postBookmarks).innerJoin(posts, and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId))).where(where);
|
|
138
|
+
return {
|
|
139
|
+
rows: rows.map((row) => toPost(row.post)),
|
|
140
|
+
total,
|
|
141
|
+
page: query.page,
|
|
142
|
+
pageSize: query.pageSize
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async listPending(orgId, spaceId, limit) {
|
|
146
|
+
return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), eq(posts.status, "pending"))).orderBy(asc(posts.createdAt), asc(posts.id)).limit(limit)).map(toPost);
|
|
147
|
+
}
|
|
148
|
+
async listDue(orgId, now, limit, afterId) {
|
|
149
|
+
return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.status, "scheduled"), lte(posts.scheduledAt, now), afterId === void 0 ? void 0 : gt(posts.id, afterId))).orderBy(asc(posts.id)).limit(limit)).map(toPost);
|
|
150
|
+
}
|
|
151
|
+
async hasDue(orgId, now) {
|
|
152
|
+
const due = this.db.select({ id: posts.id }).from(posts).where(and(eq(posts.orgId, orgId), eq(posts.status, "scheduled"), lte(posts.scheduledAt, now))).limit(1);
|
|
153
|
+
return (await this.db.execute(sql`select exists (${due}) as due`)).rows[0]?.due === true;
|
|
154
|
+
}
|
|
155
|
+
async reactionsOf(orgId, postIds, viewerId) {
|
|
156
|
+
if (postIds.length === 0) return {};
|
|
157
|
+
const rows = await this.db.select({
|
|
158
|
+
postId: postReactions.postId,
|
|
159
|
+
emoji: postReactions.emoji,
|
|
160
|
+
count: sql`cast(count(*) as int)`,
|
|
161
|
+
mine: sql`bool_or(${postReactions.orgUserId} = ${viewerId})`
|
|
162
|
+
}).from(postReactions).where(and(eq(postReactions.orgId, orgId), inArray(postReactions.postId, postIds))).groupBy(postReactions.postId, postReactions.emoji);
|
|
163
|
+
const byPost = {};
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
const entry = byPost[row.postId] ??= { reactions: {} };
|
|
166
|
+
entry.reactions[row.emoji] = row.count;
|
|
167
|
+
if (row.mine) entry.viewerReaction = row.emoji;
|
|
168
|
+
}
|
|
169
|
+
return byPost;
|
|
170
|
+
}
|
|
171
|
+
async setReaction(orgId, postId, orgUserId, emoji) {
|
|
172
|
+
if (emoji === null) {
|
|
173
|
+
await this.db.delete(postReactions).where(and(eq(postReactions.orgId, orgId), eq(postReactions.postId, postId), eq(postReactions.orgUserId, orgUserId)));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
await this.db.insert(postReactions).values({
|
|
177
|
+
orgId,
|
|
178
|
+
postId,
|
|
179
|
+
orgUserId,
|
|
180
|
+
emoji
|
|
181
|
+
}).onConflictDoUpdate({
|
|
182
|
+
target: [
|
|
183
|
+
postReactions.orgId,
|
|
184
|
+
postReactions.postId,
|
|
185
|
+
postReactions.orgUserId
|
|
186
|
+
],
|
|
187
|
+
set: { emoji }
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async follow(orgId, postId, orgUserId) {
|
|
191
|
+
await this.db.insert(postFollows).values({
|
|
192
|
+
orgId,
|
|
193
|
+
postId,
|
|
194
|
+
orgUserId
|
|
195
|
+
}).onConflictDoNothing();
|
|
196
|
+
}
|
|
197
|
+
async followerIds(orgId, postId) {
|
|
198
|
+
return (await this.db.select({ orgUserId: postFollows.orgUserId }).from(postFollows).where(and(eq(postFollows.orgId, orgId), eq(postFollows.postId, postId)))).map((row) => row.orgUserId);
|
|
199
|
+
}
|
|
200
|
+
async followedAmong(orgId, postIds, orgUserId) {
|
|
201
|
+
if (postIds.length === 0) return [];
|
|
202
|
+
return (await this.db.select({ postId: postFollows.postId }).from(postFollows).where(and(eq(postFollows.orgId, orgId), eq(postFollows.orgUserId, orgUserId), inArray(postFollows.postId, postIds)))).map((row) => row.postId);
|
|
203
|
+
}
|
|
204
|
+
async unfollow(orgId, postId, orgUserId) {
|
|
205
|
+
await this.db.delete(postFollows).where(and(eq(postFollows.orgId, orgId), eq(postFollows.postId, postId), eq(postFollows.orgUserId, orgUserId)));
|
|
206
|
+
}
|
|
207
|
+
async bookmark(orgId, postId, orgUserId) {
|
|
208
|
+
await this.db.insert(postBookmarks).values({
|
|
209
|
+
orgId,
|
|
210
|
+
postId,
|
|
211
|
+
orgUserId
|
|
212
|
+
}).onConflictDoNothing();
|
|
213
|
+
}
|
|
214
|
+
async unbookmark(orgId, postId, orgUserId) {
|
|
215
|
+
await this.db.delete(postBookmarks).where(and(eq(postBookmarks.orgId, orgId), eq(postBookmarks.postId, postId), eq(postBookmarks.orgUserId, orgUserId)));
|
|
216
|
+
}
|
|
217
|
+
async bookmarkedAmong(orgId, postIds, orgUserId) {
|
|
218
|
+
if (postIds.length === 0) return [];
|
|
219
|
+
return (await this.db.select({ postId: postBookmarks.postId }).from(postBookmarks).where(and(eq(postBookmarks.orgId, orgId), eq(postBookmarks.orgUserId, orgUserId), inArray(postBookmarks.postId, postIds)))).map((row) => row.postId);
|
|
220
|
+
}
|
|
221
|
+
async insertFlag(orgId, flag) {
|
|
222
|
+
const [row] = await this.db.insert(postFlags).values({
|
|
223
|
+
orgId,
|
|
224
|
+
id: flag.id,
|
|
225
|
+
postId: flag.postId,
|
|
226
|
+
orgUserId: flag.orgUserId,
|
|
227
|
+
reason: flag.reason,
|
|
228
|
+
resolvedAt: flag.resolvedAt,
|
|
229
|
+
resolution: flag.resolution,
|
|
230
|
+
resolvedBy: flag.resolvedBy,
|
|
231
|
+
createdAt: flag.createdAt,
|
|
232
|
+
updatedAt: flag.updatedAt
|
|
233
|
+
}).onConflictDoNothing().returning();
|
|
234
|
+
return row ? toFlag(row) : null;
|
|
235
|
+
}
|
|
236
|
+
async findFlag(orgId, postId, orgUserId) {
|
|
237
|
+
const [row] = await this.db.select().from(postFlags).where(and(eq(postFlags.orgId, orgId), eq(postFlags.postId, postId), eq(postFlags.orgUserId, orgUserId))).limit(1);
|
|
238
|
+
return row ? toFlag(row) : null;
|
|
239
|
+
}
|
|
240
|
+
async listOpenFlags(orgId, spaceId, limit) {
|
|
241
|
+
return (await this.db.select({ flag: postFlags }).from(postFlags).innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId))).where(and(eq(postFlags.orgId, orgId), eq(posts.spaceId, spaceId), isNull(postFlags.resolvedAt))).orderBy(asc(postFlags.createdAt), asc(postFlags.id)).limit(limit)).map((row) => toFlag(row.flag));
|
|
242
|
+
}
|
|
243
|
+
async resolveFlags(orgId, postId, resolved) {
|
|
244
|
+
await this.db.update(postFlags).set({
|
|
245
|
+
resolvedAt: resolved.resolvedAt,
|
|
246
|
+
resolvedBy: resolved.resolvedBy,
|
|
247
|
+
resolution: resolved.resolution
|
|
248
|
+
}).where(and(eq(postFlags.orgId, orgId), eq(postFlags.postId, postId), isNull(postFlags.resolvedAt)));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
translateDbErrors(DrizzlePostRepository);
|
|
252
|
+
//#endregion
|
|
253
|
+
export { DrizzlePostRepository };
|
|
254
|
+
|
|
255
|
+
//# sourceMappingURL=posts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posts.js","names":[],"sources":["../../src/repositories/posts.ts"],"sourcesContent":["import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n BookmarkQuery,\n FeedQuery,\n PageQuery,\n PostReactions,\n PostRepository,\n PostPatch,\n} from \"@headless-lms/core/discussion\";\nimport type {\n Logger,\n Page,\n Post,\n PostFlag,\n PostFlagResolution,\n ReactionEmoji,\n} from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { postFlags, postFollows, postBookmarks, postReactions, posts } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype PostRow = typeof posts.$inferSelect;\ntype FlagRow = typeof postFlags.$inferSelect;\n\nfunction toPost(row: PostRow): Post {\n return {\n orgId: row.orgId,\n id: row.id,\n communityId: row.communityId,\n spaceId: row.spaceId,\n orgUserId: row.orgUserId,\n topicId: row.topicId,\n title: row.title,\n body: row.body,\n extra: row.extra,\n coverAssetId: row.coverAssetId,\n status: row.status,\n scheduledAt: row.scheduledAt,\n publishedAt: row.publishedAt,\n removedBy: row.removedBy,\n pinned: row.pinned,\n commentsClosed: row.commentsClosed,\n reactionsEnabled: row.reactionsEnabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction toFlag(row: FlagRow): PostFlag {\n return {\n orgId: row.orgId,\n id: row.id,\n postId: row.postId,\n orgUserId: row.orgUserId,\n reason: row.reason,\n resolvedAt: row.resolvedAt,\n resolution: row.resolution,\n resolvedBy: row.resolvedBy,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzlePostRepository implements PostRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, post: Post): Promise<Post> {\n const [row] = await this.db\n .insert(posts)\n .values({\n orgId,\n id: post.id,\n communityId: post.communityId,\n spaceId: post.spaceId,\n orgUserId: post.orgUserId,\n topicId: post.topicId,\n title: post.title,\n body: post.body,\n extra: post.extra,\n coverAssetId: post.coverAssetId,\n status: post.status,\n scheduledAt: post.scheduledAt,\n publishedAt: post.publishedAt,\n removedBy: post.removedBy,\n pinned: post.pinned,\n commentsClosed: post.commentsClosed,\n reactionsEnabled: post.reactionsEnabled,\n createdAt: post.createdAt,\n updatedAt: post.updatedAt,\n })\n .returning();\n this.logger.debug(\"post inserted\", { orgId, postId: post.id });\n return toPost(row!);\n }\n\n async update(orgId: string, id: string, patch: PostPatch): Promise<Post | null> {\n const [row] = await this.db\n .update(posts)\n .set({ ...patch, updatedAt: new Date() })\n .where(and(eq(posts.orgId, orgId), eq(posts.id, id)))\n .returning();\n return row ? toPost(row) : null;\n }\n\n async find(orgId: string, id: string): Promise<Post | null> {\n const [row] = await this.db\n .select()\n .from(posts)\n .where(and(eq(posts.orgId, orgId), eq(posts.id, id)))\n .limit(1);\n return row ? toPost(row) : null;\n }\n\n async findMany(orgId: string, ids: string[]): Promise<Post[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(posts)\n .where(and(eq(posts.orgId, orgId), inArray(posts.id, ids)));\n return rows.map(toPost);\n }\n\n async listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>> {\n if (spaceIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const where = and(\n eq(posts.orgId, orgId),\n inArray(posts.spaceId, spaceIds),\n eq(posts.status, \"published\"),\n query.topicId ? eq(posts.topicId, query.topicId) : undefined,\n );\n const rows = await this.db\n .select()\n .from(posts)\n .where(where)\n .orderBy(desc(posts.pinned), desc(posts.publishedAt), desc(posts.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(posts)\n .where(where);\n return { rows: rows.map(toPost), total, page: query.page, pageSize: query.pageSize };\n }\n\n async listByAuthor(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: PageQuery,\n ): Promise<Page<Post>> {\n if (spaceIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const where = and(\n eq(posts.orgId, orgId),\n eq(posts.orgUserId, orgUserId),\n inArray(posts.spaceId, spaceIds),\n );\n const rows = await this.db\n .select()\n .from(posts)\n .where(where)\n .orderBy(desc(posts.createdAt), desc(posts.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(posts)\n .where(where);\n return { rows: rows.map(toPost), total, page: query.page, pageSize: query.pageSize };\n }\n\n async listBookmarked(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: BookmarkQuery,\n ): Promise<Page<Post>> {\n if (spaceIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const where = and(\n eq(postBookmarks.orgId, orgId),\n eq(postBookmarks.orgUserId, orgUserId),\n inArray(posts.spaceId, spaceIds),\n or(eq(posts.orgUserId, orgUserId), inArray(posts.status, query.visibleStatuses)),\n );\n const rows = await this.db\n .select({ post: posts })\n .from(postBookmarks)\n .innerJoin(\n posts,\n and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId)),\n )\n .where(where)\n .orderBy(desc(postBookmarks.createdAt), desc(posts.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(postBookmarks)\n .innerJoin(\n posts,\n and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId)),\n )\n .where(where);\n return {\n rows: rows.map((row) => toPost(row.post)),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]> {\n const rows = await this.db\n .select()\n .from(posts)\n .where(and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), eq(posts.status, \"pending\")))\n .orderBy(asc(posts.createdAt), asc(posts.id))\n .limit(limit);\n return rows.map(toPost);\n }\n\n async listDue(orgId: string, now: Date, limit: number, afterId?: string): Promise<Post[]> {\n const rows = await this.db\n .select()\n .from(posts)\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.status, \"scheduled\"),\n lte(posts.scheduledAt, now),\n afterId === undefined ? undefined : gt(posts.id, afterId),\n ),\n )\n .orderBy(asc(posts.id))\n .limit(limit);\n return rows.map(toPost);\n }\n\n async hasDue(orgId: string, now: Date): Promise<boolean> {\n const due = this.db\n .select({ id: posts.id })\n .from(posts)\n .where(\n and(eq(posts.orgId, orgId), eq(posts.status, \"scheduled\"), lte(posts.scheduledAt, now)),\n )\n .limit(1);\n const result = await this.db.execute<{ due: boolean }>(sql`select exists (${due}) as due`);\n return result.rows[0]?.due === true;\n }\n\n async reactionsOf(\n orgId: string,\n postIds: string[],\n viewerId: string,\n ): Promise<Record<string, PostReactions>> {\n if (postIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({\n postId: postReactions.postId,\n emoji: postReactions.emoji,\n count: sql<number>`cast(count(*) as int)`,\n mine: sql<boolean>`bool_or(${postReactions.orgUserId} = ${viewerId})`,\n })\n .from(postReactions)\n .where(and(eq(postReactions.orgId, orgId), inArray(postReactions.postId, postIds)))\n .groupBy(postReactions.postId, postReactions.emoji);\n const byPost: Record<string, PostReactions> = {};\n for (const row of rows) {\n const entry = (byPost[row.postId] ??= { reactions: {} });\n entry.reactions[row.emoji] = row.count;\n if (row.mine) {\n entry.viewerReaction = row.emoji;\n }\n }\n return byPost;\n }\n\n async setReaction(\n orgId: string,\n postId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<void> {\n if (emoji === null) {\n await this.db\n .delete(postReactions)\n .where(\n and(\n eq(postReactions.orgId, orgId),\n eq(postReactions.postId, postId),\n eq(postReactions.orgUserId, orgUserId),\n ),\n );\n return;\n }\n await this.db\n .insert(postReactions)\n .values({ orgId, postId, orgUserId, emoji })\n .onConflictDoUpdate({\n target: [postReactions.orgId, postReactions.postId, postReactions.orgUserId],\n set: { emoji },\n });\n }\n\n async follow(orgId: string, postId: string, orgUserId: string): Promise<void> {\n await this.db.insert(postFollows).values({ orgId, postId, orgUserId }).onConflictDoNothing();\n }\n\n async followerIds(orgId: string, postId: string): Promise<string[]> {\n const rows = await this.db\n .select({ orgUserId: postFollows.orgUserId })\n .from(postFollows)\n .where(and(eq(postFollows.orgId, orgId), eq(postFollows.postId, postId)));\n return rows.map((row) => row.orgUserId);\n }\n\n async followedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]> {\n if (postIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ postId: postFollows.postId })\n .from(postFollows)\n .where(\n and(\n eq(postFollows.orgId, orgId),\n eq(postFollows.orgUserId, orgUserId),\n inArray(postFollows.postId, postIds),\n ),\n );\n return rows.map((row) => row.postId);\n }\n\n async unfollow(orgId: string, postId: string, orgUserId: string): Promise<void> {\n await this.db\n .delete(postFollows)\n .where(\n and(\n eq(postFollows.orgId, orgId),\n eq(postFollows.postId, postId),\n eq(postFollows.orgUserId, orgUserId),\n ),\n );\n }\n\n async bookmark(orgId: string, postId: string, orgUserId: string): Promise<void> {\n await this.db.insert(postBookmarks).values({ orgId, postId, orgUserId }).onConflictDoNothing();\n }\n\n async unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void> {\n await this.db\n .delete(postBookmarks)\n .where(\n and(\n eq(postBookmarks.orgId, orgId),\n eq(postBookmarks.postId, postId),\n eq(postBookmarks.orgUserId, orgUserId),\n ),\n );\n }\n\n async bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]> {\n if (postIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ postId: postBookmarks.postId })\n .from(postBookmarks)\n .where(\n and(\n eq(postBookmarks.orgId, orgId),\n eq(postBookmarks.orgUserId, orgUserId),\n inArray(postBookmarks.postId, postIds),\n ),\n );\n return rows.map((row) => row.postId);\n }\n\n async insertFlag(orgId: string, flag: PostFlag): Promise<PostFlag | null> {\n const [row] = await this.db\n .insert(postFlags)\n .values({\n orgId,\n id: flag.id,\n postId: flag.postId,\n orgUserId: flag.orgUserId,\n reason: flag.reason,\n resolvedAt: flag.resolvedAt,\n resolution: flag.resolution,\n resolvedBy: flag.resolvedBy,\n createdAt: flag.createdAt,\n updatedAt: flag.updatedAt,\n })\n .onConflictDoNothing()\n .returning();\n return row ? toFlag(row) : null;\n }\n\n async findFlag(orgId: string, postId: string, orgUserId: string): Promise<PostFlag | null> {\n const [row] = await this.db\n .select()\n .from(postFlags)\n .where(\n and(\n eq(postFlags.orgId, orgId),\n eq(postFlags.postId, postId),\n eq(postFlags.orgUserId, orgUserId),\n ),\n )\n .limit(1);\n return row ? toFlag(row) : null;\n }\n\n async listOpenFlags(orgId: string, spaceId: string, limit: number): Promise<PostFlag[]> {\n const rows = await this.db\n .select({ flag: postFlags })\n .from(postFlags)\n .innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId)))\n .where(\n and(eq(postFlags.orgId, orgId), eq(posts.spaceId, spaceId), isNull(postFlags.resolvedAt)),\n )\n .orderBy(asc(postFlags.createdAt), asc(postFlags.id))\n .limit(limit);\n return rows.map((row) => toFlag(row.flag));\n }\n\n async resolveFlags(\n orgId: string,\n postId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: PostFlagResolution },\n ): Promise<void> {\n await this.db\n .update(postFlags)\n .set({\n resolvedAt: resolved.resolvedAt,\n resolvedBy: resolved.resolvedBy,\n resolution: resolved.resolution,\n })\n .where(\n and(eq(postFlags.orgId, orgId), eq(postFlags.postId, postId), isNull(postFlags.resolvedAt)),\n );\n }\n}\n\ntranslateDbErrors(DrizzlePostRepository);\n"],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ declare class DrizzleProgressRepository implements ProgressRepository {
|
|
|
11
11
|
findByTargets(orgId: string, orgUserId: string, targetIds: string[], opts?: {
|
|
12
12
|
forUpdate?: boolean;
|
|
13
13
|
}): Promise<ProgressRecord[]>;
|
|
14
|
-
update(orgId: string, id: string, patch: Partial<Pick<ProgressRecord,
|
|
14
|
+
update(orgId: string, id: string, patch: Partial<Pick<ProgressRecord, "position" | "completedAt">>): Promise<ProgressRecord | null>;
|
|
15
15
|
}
|
|
16
16
|
//#endregion
|
|
17
17
|
export { DrizzleProgressRepository };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { progressRecords } from "../schema/progress.js";
|
|
2
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
2
3
|
import { and, eq, inArray } from "drizzle-orm";
|
|
3
4
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
5
|
//#region src/repositories/progress.ts
|
|
@@ -12,6 +13,7 @@ function toRecord(row) {
|
|
|
12
13
|
startedAt: row.startedAt,
|
|
13
14
|
position: row.position ?? null,
|
|
14
15
|
completedAt: row.completedAt,
|
|
16
|
+
createdAt: row.createdAt,
|
|
15
17
|
updatedAt: row.updatedAt
|
|
16
18
|
};
|
|
17
19
|
}
|
|
@@ -53,6 +55,7 @@ var DrizzleProgressRepository = class {
|
|
|
53
55
|
return row ? toRecord(row) : null;
|
|
54
56
|
}
|
|
55
57
|
};
|
|
58
|
+
translateDbErrors(DrizzleProgressRepository);
|
|
56
59
|
//#endregion
|
|
57
60
|
export { DrizzleProgressRepository };
|
|
58
61
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"progress.js","names":[],"sources":["../../src/repositories/progress.ts"],"sourcesContent":["// progress — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray } from
|
|
1
|
+
{"version":3,"file":"progress.js","names":[],"sources":["../../src/repositories/progress.ts"],"sourcesContent":["// progress — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n ProgressRecord,\n ProgressRepository,\n ProgressTarget,\n ProgressTargetType,\n} from \"@headless-lms/core/progress\";\nimport { progressRecords } from \"../schema/progress.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof progressRecords.$inferSelect;\n\nfunction toRecord(row: Row): ProgressRecord {\n return {\n id: row.id,\n orgId: row.orgId,\n orgUserId: row.orgUserId,\n targetType: row.targetType as ProgressTargetType,\n targetId: row.targetId,\n startedAt: row.startedAt,\n position: row.position ?? null,\n completedAt: row.completedAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleProgressRepository implements ProgressRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, record: ProgressRecord): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .insert(progressRecords)\n .values({\n id: record.id,\n orgId,\n orgUserId: record.orgUserId,\n targetType: record.targetType,\n targetId: record.targetId,\n startedAt: new Date(record.startedAt),\n position: record.position ?? null,\n completedAt: record.completedAt ? new Date(record.completedAt) : null,\n updatedAt: new Date(record.updatedAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ? toRecord(row) : null;\n }\n\n async findByTarget(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, target.orgUserId),\n eq(progressRecords.targetType, target.targetType),\n eq(progressRecords.targetId, target.targetId),\n ),\n )\n .limit(1);\n return row ? toRecord(row) : null;\n }\n\n async findByTargets(\n orgId: string,\n orgUserId: string,\n targetIds: string[],\n opts?: { forUpdate?: boolean },\n ): Promise<ProgressRecord[]> {\n if (targetIds.length === 0) {\n return [];\n }\n const base = this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, orgUserId),\n inArray(progressRecords.targetId, targetIds),\n ),\n );\n // Deterministic lock order prevents deadlock between overlapping reports.\n const rows = opts?.forUpdate\n ? await base.orderBy(progressRecords.id).for(\"update\")\n : await base;\n return rows.map(toRecord);\n }\n\n async update(\n orgId: string,\n id: string,\n patch: Partial<Pick<ProgressRecord, \"position\" | \"completedAt\">>,\n ): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .update(progressRecords)\n .set({\n ...(\"position\" in patch ? { position: patch.position ?? null } : {}),\n ...(\"completedAt\" in patch\n ? { completedAt: patch.completedAt ? new Date(patch.completedAt) : null }\n : {}),\n })\n .where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.id, id)))\n .returning();\n return row ? toRecord(row) : null;\n }\n}\ntranslateDbErrors(DrizzleProgressRepository);\n"],"mappings":";;;;;AAgBA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,WAAW,IAAI;EACf,YAAY,IAAI;EAChB,UAAU,IAAI;EACd,WAAW,IAAI;EACf,UAAU,IAAI,YAAY;EAC1B,aAAa,IAAI;EACjB,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,QAAwD;EAClF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,OAAO;GACN,IAAI,OAAO;GACX;GACA,WAAW,OAAO;GAClB,YAAY,OAAO;GACnB,UAAU,OAAO;GACjB,WAAW,IAAI,KAAK,OAAO,SAAS;GACpC,UAAU,OAAO,YAAY;GAC7B,aAAa,OAAO,cAAc,IAAI,KAAK,OAAO,WAAW,IAAI;GACjE,WAAW,IAAI,KAAK,OAAO,SAAS;EACtC,CAAC,CAAC,CACD,oBAAoB,CAAC,CACrB,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,aAAa,OAAe,QAAwD;EACxF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,OAAO,SAAS,GAC9C,GAAG,gBAAgB,YAAY,OAAO,UAAU,GAChD,GAAG,gBAAgB,UAAU,OAAO,QAAQ,CAC9C,CACF,CAAC,CACA,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,cACJ,OACA,WACA,WACA,MAC2B;EAC3B,IAAI,UAAU,WAAW,GACvB,OAAO,CAAC;EAEV,MAAM,OAAO,KAAK,GACf,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,SAAS,GACvC,QAAQ,gBAAgB,UAAU,SAAS,CAC7C,CACF;EAKF,QAHa,MAAM,YACf,MAAM,KAAK,QAAQ,gBAAgB,EAAE,CAAC,CAAC,IAAI,QAAQ,IACnD,MAAM,KAAA,CACE,IAAI,QAAQ;CAC1B;CAEA,MAAM,OACJ,OACA,IACA,OACgC;EAChC,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,IAAI;GACH,GAAI,cAAc,QAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,CAAC;GAClE,GAAI,iBAAiB,QACjB,EAAE,aAAa,MAAM,cAAc,IAAI,KAAK,MAAM,WAAW,IAAI,KAAK,IACtE,CAAC;EACP,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,gBAAgB,OAAO,KAAK,GAAG,GAAG,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CACxE,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;AACF;AACA,kBAAkB,yBAAyB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { settings } from "../schema/settings.js";
|
|
2
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
2
3
|
import { and, eq } from "drizzle-orm";
|
|
3
4
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
5
|
//#region src/repositories/settings.ts
|
|
@@ -67,6 +68,7 @@ var DrizzleSettingsRepository = class {
|
|
|
67
68
|
return toRecord(row);
|
|
68
69
|
}
|
|
69
70
|
};
|
|
71
|
+
translateDbErrors(DrizzleSettingsRepository);
|
|
70
72
|
//#endregion
|
|
71
73
|
export { DrizzleSettingsRepository, deepMerge };
|
|
72
74
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.js","names":[],"sources":["../../src/repositories/settings.ts"],"sourcesContent":["import { and, eq } from
|
|
1
|
+
{"version":3,"file":"settings.js","names":[],"sources":["../../src/repositories/settings.ts"],"sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport type { Setting } from \"@headless-lms/core/schemas\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { SettingsRecord, SettingsRepository } from \"@headless-lms/core/shared/settings\";\nimport { settings } from \"../schema/settings.js\";\nimport type { Logger } from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof settings.$inferSelect;\n\nfunction toRecord(row: Row): SettingsRecord {\n return {\n namespace: row.namespace,\n scopeId: row.scopeId,\n value: row.value,\n };\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursive merge of a partial patch into a stored value: a patch cannot\n * clobber sibling keys at any depth, only the leaves it names. Arrays and\n * scalars are replaced outright — merging arrays element-wise has no meaning\n * for settings, and null is a value a caller may legitimately store.\n * Exported for tests.\n */\nexport function deepMerge(base: unknown, patch: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(patch)) {\n return patch;\n }\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(patch)) {\n merged[key] = key in base ? deepMerge(base[key], value) : value;\n }\n return merged;\n}\n\nexport class DrizzleSettingsRepository implements SettingsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async find(orgId: string, scopeId: string, namespace?: string): Promise<SettingsRecord[]> {\n const baseConditions = [eq(settings.orgId, orgId), eq(settings.scopeId, scopeId)];\n const where = namespace\n ? and(...baseConditions, eq(settings.namespace, namespace))\n : and(...baseConditions);\n\n const rows = await this.db.select().from(settings).where(where);\n return rows.map(toRecord);\n }\n\n async patch(\n orgId: string,\n namespace: string,\n scopeId: string,\n patch: Record<string, unknown>,\n ): Promise<SettingsRecord> {\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, namespace),\n eq(settings.scopeId, scopeId),\n );\n\n // The merge happens in JS, so the read and the write must be one atomic\n // unit or concurrent patches would clobber each other's sibling keys:\n // SELECT ... FOR UPDATE holds the row for the rest of the transaction.\n const row = await this.db.transaction(async (tx) => {\n let [existing] = await tx.select().from(settings).where(key).for(\"update\");\n\n if (!existing) {\n const [inserted] = await tx\n .insert(settings)\n .values({ orgId, namespace, scopeId, value: patch as Setting[\"value\"] })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return inserted;\n }\n // Lost the race: a concurrent transaction inserted the row and has\n // committed by the time onConflictDoNothing returned. Take the lock\n // and merge onto what it wrote.\n [existing] = await tx.select().from(settings).where(key).for(\"update\");\n if (!existing) {\n throw new Error(\"settings patch: row vanished mid-transaction\");\n }\n }\n\n const [updated] = await tx\n .update(settings)\n .set({ value: deepMerge(existing.value, patch) as Setting[\"value\"], updatedAt: new Date() })\n .where(key)\n .returning();\n return updated;\n });\n\n if (!row) {\n throw new Error(\"settings patch returned no row\");\n }\n this.logger.debug(\"settings.patch\", { orgId, namespace, scopeId });\n return toRecord(row);\n }\n}\ntranslateDbErrors(DrizzleSettingsRepository);\n"],"mappings":";;;;;AAWA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,WAAW,IAAI;EACf,SAAS,IAAI;EACb,OAAO,IAAI;CACb;AACF;AAEA,SAAS,cAAc,OAAkD;CACvE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;;;AASA,SAAgB,UAAU,MAAe,OAAyB;CAChE,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,KAAK,GAC9C,OAAO;CAET,MAAM,SAAkC,EAAE,GAAG,KAAK;CAClD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,OAAO,OAAO,OAAO,OAAO,UAAU,KAAK,MAAM,KAAK,IAAI;CAE5D,OAAO;AACT;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,KAAK,OAAe,SAAiB,WAA+C;EACxF,MAAM,iBAAiB,CAAC,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,SAAS,OAAO,CAAC;EAChF,MAAM,QAAQ,YACV,IAAI,GAAG,gBAAgB,GAAG,SAAS,WAAW,SAAS,CAAC,IACxD,IAAI,GAAG,cAAc;EAGzB,QAAO,MADY,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,KAAK,EAAA,CAClD,IAAI,QAAQ;CAC1B;CAEA,MAAM,MACJ,OACA,WACA,SACA,OACyB;EACzB,MAAM,MAAM,IACV,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,WAAW,SAAS,GAChC,GAAG,SAAS,SAAS,OAAO,CAC9B;EAKA,MAAM,MAAM,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;GAClD,IAAI,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;GAEzE,IAAI,CAAC,UAAU;IACb,MAAM,CAAC,YAAY,MAAM,GACtB,OAAO,QAAQ,CAAC,CAChB,OAAO;KAAE;KAAO;KAAW;KAAS,OAAO;IAA0B,CAAC,CAAC,CACvE,oBAAoB,CAAC,CACrB,UAAU;IACb,IAAI,UACF,OAAO;IAKT,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;IACrE,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,8CAA8C;GAElE;GAEA,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,QAAQ,CAAC,CAChB,IAAI;IAAE,OAAO,UAAU,SAAS,OAAO,KAAK;IAAuB,2BAAW,IAAI,KAAK;GAAE,CAAC,CAAC,CAC3F,MAAM,GAAG,CAAC,CACV,UAAU;GACb,OAAO;EACT,CAAC;EAED,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,gCAAgC;EAElD,KAAK,OAAO,MAAM,kBAAkB;GAAE;GAAO;GAAW;EAAQ,CAAC;EACjE,OAAO,SAAS,GAAG;CACrB;AACF;AACA,kBAAkB,yBAAyB"}
|