@byline/db-mysql 4.8.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.
Files changed (75) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +283 -0
  3. package/dist/database/schema/auth.d.ts +951 -0
  4. package/dist/database/schema/auth.js +226 -0
  5. package/dist/database/schema/common.d.ts +168 -0
  6. package/dist/database/schema/common.js +130 -0
  7. package/dist/database/schema/index.d.ts +3419 -0
  8. package/dist/database/schema/index.js +895 -0
  9. package/dist/database/schema/schema-pins.test.node.d.ts +52 -0
  10. package/dist/database/schema/schema-pins.test.node.js +398 -0
  11. package/dist/index.d.ts +79 -0
  12. package/dist/index.js +151 -0
  13. package/dist/lib/boot-check.d.ts +22 -0
  14. package/dist/lib/boot-check.js +42 -0
  15. package/dist/lib/boot-check.test.node.d.ts +8 -0
  16. package/dist/lib/boot-check.test.node.js +46 -0
  17. package/dist/lib/db-manager.d.ts +54 -0
  18. package/dist/lib/db-manager.js +49 -0
  19. package/dist/lib/test-db.d.ts +37 -0
  20. package/dist/lib/test-db.js +111 -0
  21. package/dist/lib/test-helper.d.ts +33 -0
  22. package/dist/lib/test-helper.js +68 -0
  23. package/dist/modules/admin/admin-permissions-repository.d.ts +35 -0
  24. package/dist/modules/admin/admin-permissions-repository.js +101 -0
  25. package/dist/modules/admin/admin-preferences-repository.d.ts +62 -0
  26. package/dist/modules/admin/admin-preferences-repository.js +156 -0
  27. package/dist/modules/admin/admin-roles-repository.d.ts +11 -0
  28. package/dist/modules/admin/admin-roles-repository.js +209 -0
  29. package/dist/modules/admin/admin-store.d.ts +20 -0
  30. package/dist/modules/admin/admin-store.js +30 -0
  31. package/dist/modules/admin/admin-users-repository.d.ts +11 -0
  32. package/dist/modules/admin/admin-users-repository.js +303 -0
  33. package/dist/modules/admin/index.d.ts +27 -0
  34. package/dist/modules/admin/index.js +27 -0
  35. package/dist/modules/admin/refresh-tokens-repository.d.ts +34 -0
  36. package/dist/modules/admin/refresh-tokens-repository.js +160 -0
  37. package/dist/modules/audit/audit-commands.d.ts +29 -0
  38. package/dist/modules/audit/audit-commands.js +40 -0
  39. package/dist/modules/audit/audit-queries.d.ts +41 -0
  40. package/dist/modules/audit/audit-queries.js +169 -0
  41. package/dist/modules/counters/counters-commands.d.ts +54 -0
  42. package/dist/modules/counters/counters-commands.js +121 -0
  43. package/dist/modules/counters/tests/counters-concurrency.test.d.ts +8 -0
  44. package/dist/modules/counters/tests/counters-concurrency.test.js +111 -0
  45. package/dist/modules/storage/classify-error.d.ts +34 -0
  46. package/dist/modules/storage/classify-error.js +50 -0
  47. package/dist/modules/storage/classify-error.test.node.d.ts +8 -0
  48. package/dist/modules/storage/classify-error.test.node.js +89 -0
  49. package/dist/modules/storage/normalize-row.d.ts +55 -0
  50. package/dist/modules/storage/normalize-row.js +135 -0
  51. package/dist/modules/storage/normalize-row.test.node.d.ts +8 -0
  52. package/dist/modules/storage/normalize-row.test.node.js +89 -0
  53. package/dist/modules/storage/storage-commands.d.ts +443 -0
  54. package/dist/modules/storage/storage-commands.js +1263 -0
  55. package/dist/modules/storage/storage-insert.d.ts +21 -0
  56. package/dist/modules/storage/storage-insert.js +152 -0
  57. package/dist/modules/storage/storage-queries.d.ts +805 -0
  58. package/dist/modules/storage/storage-queries.js +1815 -0
  59. package/dist/modules/storage/storage-store-manifest.d.ts +77 -0
  60. package/dist/modules/storage/storage-store-manifest.js +168 -0
  61. package/dist/modules/storage/storage-utils.d.ts +49 -0
  62. package/dist/modules/storage/storage-utils.js +76 -0
  63. package/dist/modules/storage/tests/dialect-pins.integration.test.d.ts +8 -0
  64. package/dist/modules/storage/tests/dialect-pins.integration.test.js +266 -0
  65. package/dist/modules/storage/tests/storage-commands.test.d.ts +8 -0
  66. package/dist/modules/storage/tests/storage-commands.test.js +324 -0
  67. package/dist/modules/storage/tests/storage-document-paths.test.d.ts +8 -0
  68. package/dist/modules/storage/tests/storage-document-paths.test.js +214 -0
  69. package/dist/modules/storage/tests/storage-document-tree.test.d.ts +8 -0
  70. package/dist/modules/storage/tests/storage-document-tree.test.js +361 -0
  71. package/dist/modules/storage/tests/storage-queries.test.d.ts +8 -0
  72. package/dist/modules/storage/tests/storage-queries.test.js +685 -0
  73. package/dist/modules/storage/tests/storage-status-and-lifecycle.test.d.ts +8 -0
  74. package/dist/modules/storage/tests/storage-status-and-lifecycle.test.js +268 -0
  75. package/package.json +91 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AdminPermissionsRepository } from '@byline/admin/admin-permissions';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ /**
12
+ * MySQL implementation of `AdminPermissionsRepository` — per-role ability
13
+ * grants and the distinct-abilities-for-user join that drives
14
+ * `resolveActor()`. Ported from
15
+ * `packages/db-postgres/src/modules/admin/admin-permissions-repository.ts`.
16
+ *
17
+ * `grantAbility` — pg's `onConflictDoNothing({ target: [admin_role_id,
18
+ * ability] })` targets the named `uq_byline_admin_permissions_role_ability`
19
+ * constraint specifically. `byline_admin_permissions` carries **two** unique
20
+ * keys — the `id` primary key and that named constraint — so, per the
21
+ * per-table rule this port follows throughout (see
22
+ * `admin-roles-repository.ts`'s `assignToUser` for the one-unique-key
23
+ * counter-case), a plain `.onDuplicateKeyUpdate()` is not safe here: MySQL
24
+ * has no per-constraint targeting, so it would fire — and silently rewrite
25
+ * an existing row — on *either* unique key, when only a collision on the
26
+ * role+ability pair should be swallowed. This uses the insert-then-catch
27
+ * pattern instead (mirrors `writeDocumentPath` in
28
+ * `storage-commands.ts`): try the insert; if it fails with `ER_DUP_ENTRY`
29
+ * on `uq_byline_admin_permissions_role_ability` specifically (detected via
30
+ * the shared `classifyError`), treat it as the idempotent no-op pg's
31
+ * `onConflictDoNothing` gives; any other error (in particular a duplicate
32
+ * on `id`, which — being a fresh UUIDv7 — should never happen in practice,
33
+ * but is structurally a different unique key) rethrows unchanged.
34
+ */
35
+ export declare function createAdminPermissionsRepository(db: MySql2Database<typeof schema>): AdminPermissionsRepository;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { DbErrorCodes } from '@byline/core';
9
+ import { and, eq } from 'drizzle-orm';
10
+ import { v7 as uuidv7 } from 'uuid';
11
+ import { adminPermissions, adminRoleAdminUser } from '../../database/schema/auth.js';
12
+ import { classifyError } from '../storage/classify-error.js';
13
+ /**
14
+ * MySQL implementation of `AdminPermissionsRepository` — per-role ability
15
+ * grants and the distinct-abilities-for-user join that drives
16
+ * `resolveActor()`. Ported from
17
+ * `packages/db-postgres/src/modules/admin/admin-permissions-repository.ts`.
18
+ *
19
+ * `grantAbility` — pg's `onConflictDoNothing({ target: [admin_role_id,
20
+ * ability] })` targets the named `uq_byline_admin_permissions_role_ability`
21
+ * constraint specifically. `byline_admin_permissions` carries **two** unique
22
+ * keys — the `id` primary key and that named constraint — so, per the
23
+ * per-table rule this port follows throughout (see
24
+ * `admin-roles-repository.ts`'s `assignToUser` for the one-unique-key
25
+ * counter-case), a plain `.onDuplicateKeyUpdate()` is not safe here: MySQL
26
+ * has no per-constraint targeting, so it would fire — and silently rewrite
27
+ * an existing row — on *either* unique key, when only a collision on the
28
+ * role+ability pair should be swallowed. This uses the insert-then-catch
29
+ * pattern instead (mirrors `writeDocumentPath` in
30
+ * `storage-commands.ts`): try the insert; if it fails with `ER_DUP_ENTRY`
31
+ * on `uq_byline_admin_permissions_role_ability` specifically (detected via
32
+ * the shared `classifyError`), treat it as the idempotent no-op pg's
33
+ * `onConflictDoNothing` gives; any other error (in particular a duplicate
34
+ * on `id`, which — being a fresh UUIDv7 — should never happen in practice,
35
+ * but is structurally a different unique key) rethrows unchanged.
36
+ */
37
+ export function createAdminPermissionsRepository(db) {
38
+ return {
39
+ async grantAbility(roleId, ability) {
40
+ try {
41
+ await db.insert(adminPermissions).values({ id: uuidv7(), admin_role_id: roleId, ability });
42
+ }
43
+ catch (err) {
44
+ const classification = classifyError(err);
45
+ const isRoleAbilityConflict = classification.code === DbErrorCodes.UNIQUE_VIOLATION &&
46
+ classification.constraint === 'uq_byline_admin_permissions_role_ability';
47
+ if (!isRoleAbilityConflict)
48
+ throw err;
49
+ // Already granted — idempotent no-op, matching pg's onConflictDoNothing.
50
+ }
51
+ },
52
+ async revokeAbility(roleId, ability) {
53
+ await db
54
+ .delete(adminPermissions)
55
+ .where(and(eq(adminPermissions.admin_role_id, roleId), eq(adminPermissions.ability, ability)));
56
+ },
57
+ async listAbilities(roleId) {
58
+ const rows = await db
59
+ .select({ ability: adminPermissions.ability })
60
+ .from(adminPermissions)
61
+ .where(eq(adminPermissions.admin_role_id, roleId));
62
+ return rows.map((r) => r.ability);
63
+ },
64
+ async setAbilities(roleId, abilities) {
65
+ await db.transaction(async (tx) => {
66
+ await tx.delete(adminPermissions).where(eq(adminPermissions.admin_role_id, roleId));
67
+ if (abilities.length === 0)
68
+ return;
69
+ const rows = abilities.map((ability) => ({
70
+ id: uuidv7(),
71
+ admin_role_id: roleId,
72
+ ability,
73
+ }));
74
+ await tx.insert(adminPermissions).values(rows);
75
+ });
76
+ },
77
+ async listAbilitiesForUser(userId) {
78
+ const rows = await db
79
+ .selectDistinct({ ability: adminPermissions.ability })
80
+ .from(adminPermissions)
81
+ .innerJoin(adminRoleAdminUser, eq(adminRoleAdminUser.admin_role_id, adminPermissions.admin_role_id))
82
+ .where(eq(adminRoleAdminUser.admin_user_id, userId));
83
+ return rows.map((r) => r.ability);
84
+ },
85
+ async listRolesForAbility(ability) {
86
+ const rows = await db
87
+ .select({ admin_role_id: adminPermissions.admin_role_id })
88
+ .from(adminPermissions)
89
+ .where(eq(adminPermissions.ability, ability));
90
+ return rows.map((r) => r.admin_role_id);
91
+ },
92
+ async listUsersForAbility(ability) {
93
+ const rows = await db
94
+ .selectDistinct({ admin_user_id: adminRoleAdminUser.admin_user_id })
95
+ .from(adminRoleAdminUser)
96
+ .innerJoin(adminPermissions, eq(adminPermissions.admin_role_id, adminRoleAdminUser.admin_role_id))
97
+ .where(eq(adminPermissions.ability, ability));
98
+ return rows.map((r) => r.admin_user_id);
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AdminPreferencesRepository } from '@byline/admin/admin-preferences';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ /**
12
+ * MySQL implementation of `AdminPreferencesRepository`. Ported from
13
+ * `packages/db-postgres/src/modules/admin/admin-preferences-repository.ts`.
14
+ *
15
+ * `jsonb` → `json`. pg's upsert merges the JSONB patch per key inside a
16
+ * single `ON CONFLICT DO UPDATE value = value || patch` statement, computed
17
+ * atomically at the database. MySQL's nearest built-in, `JSON_MERGE_PATCH`,
18
+ * implements RFC 7396 merge-patch semantics — which recurse into matching
19
+ * nested *object* values instead of replacing them wholesale. Confirmed
20
+ * live, not assumed: `JSON_MERGE_PATCH('{"filters":{"status":"draft",
21
+ * "author":"ada"}}', '{"filters":{"status":"published"}}')` returns
22
+ * `{"filters":{"author":"ada","status":"published"}}` — it keeps `author`.
23
+ * Postgres's `||` on the same two documents returns `{"filters":
24
+ * {"status":"published"}}` — it replaces the nested `filters` object
25
+ * wholesale, discarding `author`. `AdminUserPreferenceRow.value` is a
26
+ * `Record<string, unknown>` a caller could one day populate with a nested
27
+ * object, so this doesn't reach for `JSON_MERGE_PATCH` — it computes the
28
+ * merge in JS instead (`{ ...current, ...patch }`), an exact shallow-merge
29
+ * equivalent of `||` that matches pg's actual (not RFC 7396) semantics.
30
+ *
31
+ * Race safety: rather than a `SELECT` (locked or not) followed by a
32
+ * conditional `INSERT`/`UPDATE` — which leaves a window on the
33
+ * *first-ever* write for a (user, scope) pair, since two concurrent
34
+ * transactions can each observe "no row yet" before either commits — this
35
+ * tries the `INSERT` first. `byline_admin_user_preferences` has exactly one
36
+ * unique key, the composite `(user_id, scope)` primary key, so the SQL
37
+ * engine itself is the single arbiter of "does this row already exist":
38
+ * the loser of a genuine race gets `ER_DUP_ENTRY` back atomically and falls
39
+ * through to a `SELECT … FOR UPDATE` + merge + `UPDATE`, run inside a
40
+ * transaction so the lock is held across the read-modify-write. A
41
+ * duplicate-key error is only treated as "row exists, merge" when
42
+ * `classifyError` confirms it's the `PRIMARY` key specifically (verified
43
+ * live: a composite-PK collision on this table reports `for key
44
+ * '<table>.PRIMARY'`) — any other error (e.g. the `fk_admin_user_
45
+ * preferences_user_id` foreign key rejecting an unknown `userId`) rethrows
46
+ * unchanged rather than being misread as a pre-existing row.
47
+ *
48
+ * The conflict-fallback branch handles the row vanishing out from under it
49
+ * too: if a concurrent delete (e.g. cascading from the owning admin user)
50
+ * removes the row between the failed `INSERT` and the locked `SELECT`,
51
+ * `existing` comes back empty and this inserts fresh rather than merging
52
+ * onto — and reporting success for — a snapshot of a row that no longer
53
+ * exists. The subsequent `UPDATE`'s affected-row count is checked too,
54
+ * defensively, even though the `FOR UPDATE` lock held across the
55
+ * read-modify-write should make a mid-transaction disappearance there
56
+ * unreachable.
57
+ *
58
+ * `.returning()` has no MySQL equivalent; `created_at`/`updated_at` are
59
+ * captured once as `now` and reused for both the write and the returned
60
+ * row — no re-`SELECT` needed.
61
+ */
62
+ export declare function createAdminPreferencesRepository(db: MySql2Database<typeof schema>): AdminPreferencesRepository;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { DbErrorCodes, ERR_DATABASE, getLogger } from '@byline/core';
9
+ import { and, eq } from 'drizzle-orm';
10
+ import { adminUserPreferences } from '../../database/schema/auth.js';
11
+ import { classifyError } from '../storage/classify-error.js';
12
+ import { affectedRowCount } from '../storage/storage-utils.js';
13
+ /**
14
+ * MySQL implementation of `AdminPreferencesRepository`. Ported from
15
+ * `packages/db-postgres/src/modules/admin/admin-preferences-repository.ts`.
16
+ *
17
+ * `jsonb` → `json`. pg's upsert merges the JSONB patch per key inside a
18
+ * single `ON CONFLICT DO UPDATE value = value || patch` statement, computed
19
+ * atomically at the database. MySQL's nearest built-in, `JSON_MERGE_PATCH`,
20
+ * implements RFC 7396 merge-patch semantics — which recurse into matching
21
+ * nested *object* values instead of replacing them wholesale. Confirmed
22
+ * live, not assumed: `JSON_MERGE_PATCH('{"filters":{"status":"draft",
23
+ * "author":"ada"}}', '{"filters":{"status":"published"}}')` returns
24
+ * `{"filters":{"author":"ada","status":"published"}}` — it keeps `author`.
25
+ * Postgres's `||` on the same two documents returns `{"filters":
26
+ * {"status":"published"}}` — it replaces the nested `filters` object
27
+ * wholesale, discarding `author`. `AdminUserPreferenceRow.value` is a
28
+ * `Record<string, unknown>` a caller could one day populate with a nested
29
+ * object, so this doesn't reach for `JSON_MERGE_PATCH` — it computes the
30
+ * merge in JS instead (`{ ...current, ...patch }`), an exact shallow-merge
31
+ * equivalent of `||` that matches pg's actual (not RFC 7396) semantics.
32
+ *
33
+ * Race safety: rather than a `SELECT` (locked or not) followed by a
34
+ * conditional `INSERT`/`UPDATE` — which leaves a window on the
35
+ * *first-ever* write for a (user, scope) pair, since two concurrent
36
+ * transactions can each observe "no row yet" before either commits — this
37
+ * tries the `INSERT` first. `byline_admin_user_preferences` has exactly one
38
+ * unique key, the composite `(user_id, scope)` primary key, so the SQL
39
+ * engine itself is the single arbiter of "does this row already exist":
40
+ * the loser of a genuine race gets `ER_DUP_ENTRY` back atomically and falls
41
+ * through to a `SELECT … FOR UPDATE` + merge + `UPDATE`, run inside a
42
+ * transaction so the lock is held across the read-modify-write. A
43
+ * duplicate-key error is only treated as "row exists, merge" when
44
+ * `classifyError` confirms it's the `PRIMARY` key specifically (verified
45
+ * live: a composite-PK collision on this table reports `for key
46
+ * '<table>.PRIMARY'`) — any other error (e.g. the `fk_admin_user_
47
+ * preferences_user_id` foreign key rejecting an unknown `userId`) rethrows
48
+ * unchanged rather than being misread as a pre-existing row.
49
+ *
50
+ * The conflict-fallback branch handles the row vanishing out from under it
51
+ * too: if a concurrent delete (e.g. cascading from the owning admin user)
52
+ * removes the row between the failed `INSERT` and the locked `SELECT`,
53
+ * `existing` comes back empty and this inserts fresh rather than merging
54
+ * onto — and reporting success for — a snapshot of a row that no longer
55
+ * exists. The subsequent `UPDATE`'s affected-row count is checked too,
56
+ * defensively, even though the `FOR UPDATE` lock held across the
57
+ * read-modify-write should make a mid-transaction disappearance there
58
+ * unreachable.
59
+ *
60
+ * `.returning()` has no MySQL equivalent; `created_at`/`updated_at` are
61
+ * captured once as `now` and reused for both the write and the returned
62
+ * row — no re-`SELECT` needed.
63
+ */
64
+ export function createAdminPreferencesRepository(db) {
65
+ return {
66
+ async get(userId, scope) {
67
+ const [row] = await db
68
+ .select()
69
+ .from(adminUserPreferences)
70
+ .where(and(eq(adminUserPreferences.user_id, userId), eq(adminUserPreferences.scope, scope)));
71
+ return row ?? null;
72
+ },
73
+ async upsert(userId, scope, patch) {
74
+ const insertNow = new Date();
75
+ try {
76
+ await db.insert(adminUserPreferences).values({
77
+ user_id: userId,
78
+ scope,
79
+ value: patch,
80
+ created_at: insertNow,
81
+ updated_at: insertNow,
82
+ });
83
+ return {
84
+ user_id: userId,
85
+ scope,
86
+ value: { ...patch },
87
+ created_at: insertNow,
88
+ updated_at: insertNow,
89
+ };
90
+ }
91
+ catch (err) {
92
+ const classification = classifyError(err);
93
+ const isExistingRow = classification.code === DbErrorCodes.UNIQUE_VIOLATION &&
94
+ classification.constraint === 'PRIMARY';
95
+ if (!isExistingRow)
96
+ throw err;
97
+ }
98
+ // A row already exists for (userId, scope) — lock it, merge in JS, and
99
+ // write the merge back. Locked inside a transaction so a second
100
+ // concurrent upsert blocks on this row rather than racing it.
101
+ return db.transaction(async (tx) => {
102
+ const [existing] = await tx
103
+ .select()
104
+ .from(adminUserPreferences)
105
+ .where(and(eq(adminUserPreferences.user_id, userId), eq(adminUserPreferences.scope, scope)))
106
+ .for('update');
107
+ const now = new Date();
108
+ if (!existing) {
109
+ // The row that made our INSERT fail is gone by the time we got
110
+ // here — a concurrent delete (e.g. cascading from the owning
111
+ // admin user) raced us between the failed INSERT and this locked
112
+ // read. There is truly no row now, so insert fresh rather than
113
+ // merging onto a snapshot that no longer exists.
114
+ await tx.insert(adminUserPreferences).values({
115
+ user_id: userId,
116
+ scope,
117
+ value: patch,
118
+ created_at: now,
119
+ updated_at: now,
120
+ });
121
+ return {
122
+ user_id: userId,
123
+ scope,
124
+ value: { ...patch },
125
+ created_at: now,
126
+ updated_at: now,
127
+ };
128
+ }
129
+ const merged = {
130
+ ...existing.value,
131
+ ...patch,
132
+ };
133
+ const result = await tx
134
+ .update(adminUserPreferences)
135
+ .set({ value: merged, updated_at: now })
136
+ .where(and(eq(adminUserPreferences.user_id, userId), eq(adminUserPreferences.scope, scope)));
137
+ if (affectedRowCount(result) === 0) {
138
+ // Unreachable in practice — `existing` was read via `FOR UPDATE`
139
+ // inside this same transaction, so the row is locked and cannot
140
+ // vanish before the `UPDATE` above. Guarded anyway so a write
141
+ // that didn't happen is never reported as a success.
142
+ throw ERR_DATABASE({
143
+ message: 'admin preferences upsert: locked row vanished before UPDATE',
144
+ }).log(getLogger());
145
+ }
146
+ return {
147
+ user_id: userId,
148
+ scope,
149
+ value: merged,
150
+ created_at: existing.created_at,
151
+ updated_at: now,
152
+ };
153
+ });
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { type AdminRolesRepository } from '@byline/admin/admin-roles';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ export declare function createAdminRolesRepository(db: MySql2Database<typeof schema>): AdminRolesRepository;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { ERR_ADMIN_ROLE_VERSION_CONFLICT, } from '@byline/admin/admin-roles';
9
+ import { and, asc, eq, sql } from 'drizzle-orm';
10
+ import { v7 as uuidv7 } from 'uuid';
11
+ import { adminRoleAdminUser, adminRoles } from '../../database/schema/auth.js';
12
+ import { affectedRowCount } from '../storage/storage-utils.js';
13
+ /**
14
+ * MySQL implementation of `AdminRolesRepository` — role CRUD, reorder, and
15
+ * role ↔ user assignments. Ability grants live on `AdminPermissionsRepository`
16
+ * (see `admin-permissions-repository.ts`). Ported from
17
+ * `packages/db-postgres/src/modules/admin/admin-roles-repository.ts`.
18
+ *
19
+ * `create`/`update` follow the same `.returning()` → construct-in-JS
20
+ * approach as `admin-users-repository.ts` (see that file's docblock for the
21
+ * full rationale): `create`'s `created_at`/`updated_at` are captured once as
22
+ * `now` and used for both the `INSERT` and the returned row; `update` reads
23
+ * the current row first (values the patch didn't touch aren't otherwise
24
+ * knowable) and merges the patch onto it in JS, with the vid-guarded
25
+ * `UPDATE`'s affected-row count as the sole accept/reject authority.
26
+ *
27
+ * Unlike `admin-users-repository.ts`'s `update`/`setPasswordHash` (which
28
+ * re-`SELECT` post-`UPDATE` — see that file's docblock for why), `update`
29
+ * here safely returns the pre-read-merged row: every mutator of
30
+ * `byline_admin_roles` — `update` itself and `reorder` — bumps `vid`, so
31
+ * there is no vid-less mutator on this table analogous to admin-users'
32
+ * `recordLoginSuccess`/`recordLoginFailure` that could leave a pre-read
33
+ * snapshot stale while the guarded `UPDATE` still succeeds. If a future
34
+ * change adds a vid-less mutator to this table, revisit this.
35
+ *
36
+ * `assignToUser`'s upsert uses a plain `.onDuplicateKeyUpdate()` no-op
37
+ * idiom (`admin_role_id = admin_role_id`) rather than the insert-then-catch
38
+ * pattern `admin-permissions-repository.ts` needs: `byline_admin_role_
39
+ * admin_user` carries exactly one unique key — the composite primary key
40
+ * `(admin_role_id, admin_user_id)` targeted here — so a duplicate-key
41
+ * collision on this table can only be that one key, and MySQL's lack of
42
+ * per-constraint targeting is a non-issue.
43
+ */
44
+ const PUBLIC_ROLE_COLUMNS = {
45
+ id: adminRoles.id,
46
+ vid: adminRoles.vid,
47
+ name: adminRoles.name,
48
+ machine_name: adminRoles.machine_name,
49
+ description: adminRoles.description,
50
+ order: adminRoles.order,
51
+ created_at: adminRoles.created_at,
52
+ updated_at: adminRoles.updated_at,
53
+ };
54
+ export function createAdminRolesRepository(db) {
55
+ return {
56
+ // -----------------------------------------------------------------
57
+ // Role CRUD
58
+ // -----------------------------------------------------------------
59
+ async create(input) {
60
+ const id = uuidv7();
61
+ const now = new Date();
62
+ const row = {
63
+ id,
64
+ vid: 1,
65
+ name: input.name,
66
+ machine_name: input.machine_name,
67
+ description: input.description ?? null,
68
+ order: input.order ?? 0,
69
+ created_at: now,
70
+ updated_at: now,
71
+ };
72
+ await db.insert(adminRoles).values({
73
+ id: row.id,
74
+ name: row.name,
75
+ machine_name: row.machine_name,
76
+ description: row.description,
77
+ order: row.order,
78
+ created_at: now,
79
+ updated_at: now,
80
+ });
81
+ return row;
82
+ },
83
+ async getById(id) {
84
+ const [row] = await db
85
+ .select(PUBLIC_ROLE_COLUMNS)
86
+ .from(adminRoles)
87
+ .where(eq(adminRoles.id, id));
88
+ return row ?? null;
89
+ },
90
+ async getByMachineName(machineName) {
91
+ const [row] = await db
92
+ .select(PUBLIC_ROLE_COLUMNS)
93
+ .from(adminRoles)
94
+ .where(eq(adminRoles.machine_name, machineName));
95
+ return row ?? null;
96
+ },
97
+ async list() {
98
+ return db
99
+ .select(PUBLIC_ROLE_COLUMNS)
100
+ .from(adminRoles)
101
+ .orderBy(asc(adminRoles.order), asc(adminRoles.created_at));
102
+ },
103
+ async update(id, expectedVid, patch) {
104
+ const [current] = await db
105
+ .select(PUBLIC_ROLE_COLUMNS)
106
+ .from(adminRoles)
107
+ .where(eq(adminRoles.id, id));
108
+ const now = new Date();
109
+ const updateSet = {
110
+ updated_at: now,
111
+ vid: sql `${adminRoles.vid} + 1`,
112
+ };
113
+ if (patch.name !== undefined)
114
+ updateSet.name = patch.name;
115
+ if (patch.description !== undefined)
116
+ updateSet.description = patch.description;
117
+ if (patch.order !== undefined)
118
+ updateSet.order = patch.order;
119
+ const result = await db
120
+ .update(adminRoles)
121
+ .set(updateSet)
122
+ .where(and(eq(adminRoles.id, id), eq(adminRoles.vid, expectedVid)));
123
+ if (affectedRowCount(result) === 0 || !current)
124
+ throw ERR_ADMIN_ROLE_VERSION_CONFLICT();
125
+ return {
126
+ ...current,
127
+ name: patch.name !== undefined ? patch.name : current.name,
128
+ description: patch.description !== undefined ? patch.description : current.description,
129
+ order: patch.order !== undefined ? patch.order : current.order,
130
+ vid: expectedVid + 1,
131
+ updated_at: now,
132
+ };
133
+ },
134
+ async delete(id, expectedVid) {
135
+ // Cascades remove role ↔ user assignments and per-role permissions
136
+ // (`ON DELETE CASCADE` on both FKs — see `database/schema/auth.ts`).
137
+ const result = await db
138
+ .delete(adminRoles)
139
+ .where(and(eq(adminRoles.id, id), eq(adminRoles.vid, expectedVid)));
140
+ if (affectedRowCount(result) === 0)
141
+ throw ERR_ADMIN_ROLE_VERSION_CONFLICT();
142
+ },
143
+ async reorder(ids) {
144
+ if (ids.length === 0)
145
+ return;
146
+ // Single transaction so the list is never observed half-reordered.
147
+ // No vid gate: see `AdminRolesRepository.reorder` contract docs.
148
+ await db.transaction(async (tx) => {
149
+ const now = new Date();
150
+ for (let i = 0; i < ids.length; i++) {
151
+ await tx
152
+ .update(adminRoles)
153
+ .set({
154
+ order: i,
155
+ updated_at: now,
156
+ vid: sql `${adminRoles.vid} + 1`,
157
+ })
158
+ .where(eq(adminRoles.id, ids[i]));
159
+ }
160
+ });
161
+ },
162
+ // -----------------------------------------------------------------
163
+ // Role ↔ user assignments
164
+ // -----------------------------------------------------------------
165
+ async assignToUser(roleId, userId) {
166
+ await db
167
+ .insert(adminRoleAdminUser)
168
+ .values({ admin_role_id: roleId, admin_user_id: userId })
169
+ .onDuplicateKeyUpdate({
170
+ set: { admin_role_id: sql `admin_role_id` },
171
+ });
172
+ },
173
+ async unassignFromUser(roleId, userId) {
174
+ await db
175
+ .delete(adminRoleAdminUser)
176
+ .where(and(eq(adminRoleAdminUser.admin_role_id, roleId), eq(adminRoleAdminUser.admin_user_id, userId)));
177
+ },
178
+ async listRolesForUser(userId) {
179
+ const rows = await db
180
+ .select(PUBLIC_ROLE_COLUMNS)
181
+ .from(adminRoles)
182
+ .innerJoin(adminRoleAdminUser, eq(adminRoleAdminUser.admin_role_id, adminRoles.id))
183
+ .where(eq(adminRoleAdminUser.admin_user_id, userId))
184
+ .orderBy(asc(adminRoles.order));
185
+ return rows;
186
+ },
187
+ async listUsersForRole(roleId) {
188
+ const rows = await db
189
+ .select({ admin_user_id: adminRoleAdminUser.admin_user_id })
190
+ .from(adminRoleAdminUser)
191
+ .where(eq(adminRoleAdminUser.admin_role_id, roleId));
192
+ return rows.map((r) => r.admin_user_id);
193
+ },
194
+ async setRolesForUser(userId, roleIds) {
195
+ // Single transaction — user assignments are never observed
196
+ // half-edited. Symmetric to AdminPermissionsRepository.setAbilities.
197
+ await db.transaction(async (tx) => {
198
+ await tx.delete(adminRoleAdminUser).where(eq(adminRoleAdminUser.admin_user_id, userId));
199
+ if (roleIds.length === 0)
200
+ return;
201
+ const rows = roleIds.map((admin_role_id) => ({
202
+ admin_role_id,
203
+ admin_user_id: userId,
204
+ }));
205
+ await tx.insert(adminRoleAdminUser).values(rows);
206
+ });
207
+ },
208
+ };
209
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AdminStore } from '@byline/admin';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ /**
12
+ * Wire the five admin repositories against a Drizzle handle and return the
13
+ * `AdminStore` bundle expected by `@byline/admin` — specifically by the
14
+ * built-in `JwtSessionProvider`, by `seedSuperAdmin`, and by the admin-user /
15
+ * admin-role commands. Mirrors
16
+ * `packages/db-postgres/src/modules/admin/admin-store.ts`.
17
+ *
18
+ * Construct once per process, alongside the `mysqlAdapter` call.
19
+ */
20
+ export declare function createAdminStore(db: MySql2Database<typeof schema>): AdminStore;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { createAdminPermissionsRepository } from './admin-permissions-repository.js';
9
+ import { createAdminPreferencesRepository } from './admin-preferences-repository.js';
10
+ import { createAdminRolesRepository } from './admin-roles-repository.js';
11
+ import { createAdminUsersRepository } from './admin-users-repository.js';
12
+ import { createRefreshTokensRepository } from './refresh-tokens-repository.js';
13
+ /**
14
+ * Wire the five admin repositories against a Drizzle handle and return the
15
+ * `AdminStore` bundle expected by `@byline/admin` — specifically by the
16
+ * built-in `JwtSessionProvider`, by `seedSuperAdmin`, and by the admin-user /
17
+ * admin-role commands. Mirrors
18
+ * `packages/db-postgres/src/modules/admin/admin-store.ts`.
19
+ *
20
+ * Construct once per process, alongside the `mysqlAdapter` call.
21
+ */
22
+ export function createAdminStore(db) {
23
+ return {
24
+ adminUsers: createAdminUsersRepository(db),
25
+ adminRoles: createAdminRolesRepository(db),
26
+ adminPermissions: createAdminPermissionsRepository(db),
27
+ refreshTokens: createRefreshTokensRepository(db),
28
+ adminPreferences: createAdminPreferencesRepository(db),
29
+ };
30
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { type AdminUsersRepository } from '@byline/admin/admin-users';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ export declare function createAdminUsersRepository(db: MySql2Database<typeof schema>): AdminUsersRepository;