@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,226 @@
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
+ /**
9
+ * Auth schema — admin identity, roles, role-user assignment, and per-role
10
+ * ability grants.
11
+ *
12
+ * All tables carry the `byline_` prefix so Byline can coexist with other
13
+ * schemas in a shared database. The TypeScript exports are unprefixed —
14
+ * the prefix is a DB-side concern.
15
+ *
16
+ * Shape mirrors the mature Modulus Learning implementation with minor
17
+ * Byline conventions:
18
+ * - UUIDv7 primary keys (generated at insert time in the repository).
19
+ * - `vid` integer version column for optimistic concurrency (defaults
20
+ * to 1; bumped by write paths when needed).
21
+ * - snake_case column names matching the rest of the Byline schema.
22
+ *
23
+ * See docs/06-auth-and-security/01-authn-authz.md for the full data model and present-state
24
+ * reference.
25
+ */
26
+ import { relations, sql } from 'drizzle-orm';
27
+ import { boolean, datetime, foreignKey, index, int, json, mysqlTable, primaryKey, text, unique, varchar, } from 'drizzle-orm/mysql-core';
28
+ import { createdAt, timestamps, uuidChar } from './common.js';
29
+ // Foreign keys below use the table-level `foreignKey()` builder with an
30
+ // explicit, short `fk_<table>_<column>` name rather than the Postgres
31
+ // schema's column-level `.references()` shorthand — MySQL enforces a hard
32
+ // 64-character identifier cap that several of this schema's auto-generated
33
+ // FK names exceed. See the longer explanation in `./index.ts`.
34
+ // ---------------------------------------------------------------------------
35
+ // byline_admin_users
36
+ // ---------------------------------------------------------------------------
37
+ export const adminUsers = mysqlTable('byline_admin_users', {
38
+ id: uuidChar('id').primaryKey(),
39
+ vid: int('vid').notNull().default(1),
40
+ given_name: varchar('given_name', { length: 100 }),
41
+ family_name: varchar('family_name', { length: 100 }),
42
+ /** Optional — email is the primary identifier. Unique when present. */
43
+ username: varchar('username', { length: 64 }).unique(),
44
+ email: varchar('email', { length: 254 }).notNull().unique(),
45
+ /** Full PHC string, e.g. `$argon2id$v=19$m=…$…$…`. */
46
+ password: varchar('password', { length: 255 }).notNull(),
47
+ remember_me: boolean('remember_me').notNull().default(false),
48
+ // fsp 6, matching pg's `timestamp('last_login', { precision: 6, withTimezone: true })` —
49
+ // see `common.ts`'s `auditTimestamp` docblock for why every temporal column
50
+ // in this schema moved from fsp 3 to fsp 6.
51
+ last_login: datetime('last_login', { fsp: 6 }),
52
+ last_login_ip: varchar('last_login_ip', { length: 45 }),
53
+ failed_login_attempts: int('failed_login_attempts').notNull().default(0),
54
+ /**
55
+ * Actor-level super-admin bypass. When true, `AdminAuth.isSuperAdmin`
56
+ * short-circuits every ability check. Set only via the seed script
57
+ * (or manually by a DB admin) — never via the admin UI.
58
+ */
59
+ is_super_admin: boolean('is_super_admin').notNull().default(false),
60
+ /**
61
+ * Account enablement. Defaults to `false` so accounts created through
62
+ * any future admin UI require a deliberate enable step. The super-admin
63
+ * seed sets this to `true`.
64
+ */
65
+ is_enabled: boolean('is_enabled').notNull().default(false),
66
+ is_email_verified: boolean('is_email_verified').notNull().default(false),
67
+ /**
68
+ * Per-user admin interface locale preference. Nullable — `null` means
69
+ * "use the detection cascade" (cookie → Accept-Language → defaultLocale).
70
+ * Stored as a BCP 47 code (`en`, `pt-BR`, `zh-Hans-CN`); validated at
71
+ * write time against the host's `i18n.interface.locales` set.
72
+ */
73
+ preferred_locale: varchar('preferred_locale', { length: 16 }),
74
+ ...timestamps,
75
+ }, (table) => [index('idx_byline_admin_users_email').on(table.email)]);
76
+ // ---------------------------------------------------------------------------
77
+ // byline_admin_user_preferences
78
+ // ---------------------------------------------------------------------------
79
+ /**
80
+ * Scoped per-user key-value preferences (e.g. sticky list-view page
81
+ * size and sort). One row per (user, scope); `scope` is a dot-separated
82
+ * key like `collections.docs.list` and `value` is a JSON object whose
83
+ * shape belongs to the scope's feature. Composite natural PK — writes
84
+ * are `INSERT … ON DUPLICATE KEY UPDATE` with a per-key JSON merge.
85
+ * See `@byline/admin/admin-preferences`.
86
+ */
87
+ export const adminUserPreferences = mysqlTable('byline_admin_user_preferences', {
88
+ user_id: uuidChar('user_id').notNull(),
89
+ scope: varchar('scope', { length: 255 }).notNull(),
90
+ value: json('value').notNull().$type(),
91
+ ...timestamps,
92
+ }, (table) => [
93
+ foreignKey({
94
+ name: 'fk_admin_user_preferences_user_id',
95
+ columns: [table.user_id],
96
+ foreignColumns: [adminUsers.id],
97
+ }).onDelete('cascade'),
98
+ primaryKey({ columns: [table.user_id, table.scope] }),
99
+ ]);
100
+ // ---------------------------------------------------------------------------
101
+ // byline_admin_roles
102
+ // ---------------------------------------------------------------------------
103
+ export const adminRoles = mysqlTable('byline_admin_roles', {
104
+ id: uuidChar('id').primaryKey(),
105
+ vid: int('vid').notNull().default(1),
106
+ /** Human-readable label, e.g. `'Editor'`. */
107
+ name: varchar('name', { length: 128 }).notNull(),
108
+ /** Stable identifier used in code, e.g. `'editor'`, `'super-admin'`. */
109
+ machine_name: varchar('machine_name', { length: 128 }).notNull().unique(),
110
+ description: text('description'),
111
+ /** Display ordering in the role-editor UI. */
112
+ order: int('order').notNull().default(0),
113
+ ...timestamps,
114
+ }, (table) => [index('idx_byline_admin_roles_machine_name').on(table.machine_name)]);
115
+ // ---------------------------------------------------------------------------
116
+ // byline_admin_role_admin_user — many-to-many join
117
+ // ---------------------------------------------------------------------------
118
+ export const adminRoleAdminUser = mysqlTable('byline_admin_role_admin_user', {
119
+ admin_role_id: uuidChar('admin_role_id').notNull(),
120
+ admin_user_id: uuidChar('admin_user_id').notNull(),
121
+ ...createdAt,
122
+ }, (table) => [
123
+ foreignKey({
124
+ name: 'fk_admin_role_admin_user_admin_role_id',
125
+ columns: [table.admin_role_id],
126
+ foreignColumns: [adminRoles.id],
127
+ }).onDelete('cascade'),
128
+ foreignKey({
129
+ name: 'fk_admin_role_admin_user_admin_user_id',
130
+ columns: [table.admin_user_id],
131
+ foreignColumns: [adminUsers.id],
132
+ }).onDelete('cascade'),
133
+ primaryKey({ columns: [table.admin_role_id, table.admin_user_id] }),
134
+ index('idx_byline_admin_role_admin_user_user').on(table.admin_user_id),
135
+ ]);
136
+ // ---------------------------------------------------------------------------
137
+ // byline_admin_permissions — one row per (role, ability) grant
138
+ // ---------------------------------------------------------------------------
139
+ export const adminPermissions = mysqlTable('byline_admin_permissions', {
140
+ id: uuidChar('id').primaryKey(),
141
+ vid: int('vid').notNull().default(1),
142
+ admin_role_id: uuidChar('admin_role_id').notNull(),
143
+ /** Flat dotted ability key — see `@byline/auth` AbilityRegistry. */
144
+ ability: varchar('ability', { length: 128 }).notNull(),
145
+ ...timestamps,
146
+ }, (table) => [
147
+ foreignKey({
148
+ name: 'fk_admin_permissions_admin_role_id',
149
+ columns: [table.admin_role_id],
150
+ foreignColumns: [adminRoles.id],
151
+ }).onDelete('cascade'),
152
+ unique('uq_byline_admin_permissions_role_ability').on(table.admin_role_id, table.ability),
153
+ index('idx_byline_admin_permissions_role').on(table.admin_role_id),
154
+ ]);
155
+ // ---------------------------------------------------------------------------
156
+ // byline_admin_refresh_tokens — JWT session provider's refresh-token store
157
+ // ---------------------------------------------------------------------------
158
+ /**
159
+ * Refresh tokens are opaque random strings minted by `JwtSessionProvider`.
160
+ * We never store the plaintext — only a SHA-256 hash (`token_hash`). When
161
+ * a token is rotated, `revoked_at` is stamped and `rotated_to_id` points
162
+ * at the replacement row; presenting a rotated token is treated as replay
163
+ * and revokes the whole chain.
164
+ */
165
+ export const adminRefreshTokens = mysqlTable('byline_admin_refresh_tokens', {
166
+ id: uuidChar('id').primaryKey(),
167
+ admin_user_id: uuidChar('admin_user_id').notNull(),
168
+ /** SHA-256 hex digest of the raw refresh-token string. 64 chars. */
169
+ token_hash: varchar('token_hash', { length: 64 }).notNull().unique(),
170
+ // fsp 6 throughout this table, matching pg — see `common.ts`'s
171
+ // `auditTimestamp` docblock for why every temporal column in this
172
+ // schema moved from fsp 3 to fsp 6.
173
+ issued_at: datetime('issued_at', { fsp: 6 }).notNull().default(sql `CURRENT_TIMESTAMP(6)`),
174
+ expires_at: datetime('expires_at', { fsp: 6 }).notNull(),
175
+ revoked_at: datetime('revoked_at', { fsp: 6 }),
176
+ /**
177
+ * When this token was rotated, the id of the new token issued in its
178
+ * place. Self-referential; set atomically alongside `revoked_at`.
179
+ */
180
+ rotated_to_id: uuidChar('rotated_to_id'),
181
+ last_used_at: datetime('last_used_at', { fsp: 6 }),
182
+ user_agent: varchar('user_agent', { length: 512 }),
183
+ ip: varchar('ip', { length: 45 }),
184
+ ...timestamps,
185
+ }, (table) => [
186
+ foreignKey({
187
+ name: 'fk_admin_refresh_tokens_admin_user_id',
188
+ columns: [table.admin_user_id],
189
+ foreignColumns: [adminUsers.id],
190
+ }).onDelete('cascade'),
191
+ index('idx_byline_admin_refresh_tokens_user').on(table.admin_user_id),
192
+ index('idx_byline_admin_refresh_tokens_token_hash').on(table.token_hash),
193
+ ]);
194
+ // ---------------------------------------------------------------------------
195
+ // Relations (drizzle query helpers)
196
+ // ---------------------------------------------------------------------------
197
+ export const adminUsersRelations = relations(adminUsers, ({ many }) => ({
198
+ roleAssignments: many(adminRoleAdminUser),
199
+ refreshTokens: many(adminRefreshTokens),
200
+ }));
201
+ export const adminRolesRelations = relations(adminRoles, ({ many }) => ({
202
+ userAssignments: many(adminRoleAdminUser),
203
+ permissions: many(adminPermissions),
204
+ }));
205
+ export const adminRoleAdminUserRelations = relations(adminRoleAdminUser, ({ one }) => ({
206
+ role: one(adminRoles, {
207
+ fields: [adminRoleAdminUser.admin_role_id],
208
+ references: [adminRoles.id],
209
+ }),
210
+ user: one(adminUsers, {
211
+ fields: [adminRoleAdminUser.admin_user_id],
212
+ references: [adminUsers.id],
213
+ }),
214
+ }));
215
+ export const adminPermissionsRelations = relations(adminPermissions, ({ one }) => ({
216
+ role: one(adminRoles, {
217
+ fields: [adminPermissions.admin_role_id],
218
+ references: [adminRoles.id],
219
+ }),
220
+ }));
221
+ export const adminRefreshTokensRelations = relations(adminRefreshTokens, ({ one }) => ({
222
+ user: one(adminUsers, {
223
+ fields: [adminRefreshTokens.admin_user_id],
224
+ references: [adminUsers.id],
225
+ }),
226
+ }));
@@ -0,0 +1,168 @@
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
+ /**
9
+ * `CHAR(36) CHARACTER SET ascii COLLATE ascii_bin` — canonical UUID text.
10
+ *
11
+ * Every id / FK column in the schema uses this type. `ascii_bin` is a
12
+ * byte-wise (case-sensitive) collation, so id equality comparisons and
13
+ * joins compare bytes rather than applying a case- or accent-insensitive
14
+ * collation — the MySQL analogue of comparing `uuid` values in Postgres,
15
+ * which has no notion of collation for that type at all. UUIDs are
16
+ * app-generated UUIDv7 (the `uuid` package's `v7()`), never DB-generated.
17
+ */
18
+ export declare const uuidChar: {
19
+ (): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
20
+ name: "";
21
+ dataType: 'custom';
22
+ columnType: 'MySqlCustomColumn';
23
+ data: string;
24
+ driverParam: string;
25
+ enumValues: undefined;
26
+ }>;
27
+ <TConfig extends Record<string, any> & unknown>(fieldConfig?: TConfig | undefined): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
28
+ name: "";
29
+ dataType: 'custom';
30
+ columnType: 'MySqlCustomColumn';
31
+ data: string;
32
+ driverParam: string;
33
+ enumValues: undefined;
34
+ }>;
35
+ <TName extends string>(dbName: TName, fieldConfig?: unknown): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
36
+ name: TName;
37
+ dataType: 'custom';
38
+ columnType: 'MySqlCustomColumn';
39
+ data: string;
40
+ driverParam: string;
41
+ enumValues: undefined;
42
+ }>;
43
+ };
44
+ /**
45
+ * `varchar(...)` with explicit byte-wise (`ascii_bin`) collation.
46
+ *
47
+ * Used for `byline_documents.order_key` (and `byline_document_relationships
48
+ * .order_key`) so the column sorts the same way JavaScript string
49
+ * comparison does. The fractional-index algorithm in `@byline/core`
50
+ * (`generateKeyBetween`, `generateNKeysBetween`) is designed against
51
+ * byte-wise ordering; MySQL's default collation (`utf8mb4_0900_ai_ci` on
52
+ * this database) is accent- and case-insensitive and disagrees with JS on
53
+ * cases like `'Zz' vs 'a0'` — which causes a refetch after a drag-reorder
54
+ * to "snap" the moved row back to its original position. Mirrors the
55
+ * Postgres adapter's `varcharByteSorted` (`COLLATE "C"`); see
56
+ * `packages/db-postgres/src/database/schema/index.ts`.
57
+ */
58
+ export declare const varcharByteSorted: {
59
+ (): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
60
+ name: "";
61
+ dataType: 'custom';
62
+ columnType: 'MySqlCustomColumn';
63
+ data: string;
64
+ driverParam: string;
65
+ enumValues: undefined;
66
+ }>;
67
+ <TConfig extends Record<string, any> & {
68
+ length: number;
69
+ }>(fieldConfig?: TConfig | undefined): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
70
+ name: "";
71
+ dataType: 'custom';
72
+ columnType: 'MySqlCustomColumn';
73
+ data: string;
74
+ driverParam: string;
75
+ enumValues: undefined;
76
+ }>;
77
+ <TName extends string>(dbName: TName, fieldConfig?: {
78
+ length: number;
79
+ } | undefined): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
80
+ name: TName;
81
+ dataType: 'custom';
82
+ columnType: 'MySqlCustomColumn';
83
+ data: string;
84
+ driverParam: string;
85
+ enumValues: undefined;
86
+ }>;
87
+ };
88
+ /**
89
+ * `varchar(...)` with the `utf8mb4_bin` collation — full `utf8mb4` charset
90
+ * (unlike `uuidChar/varcharByteSorted`'s ascii-only columns, this one must
91
+ * carry arbitrary Unicode), but byte-wise, case- and accent-sensitive
92
+ * comparison instead of the database's default `utf8mb4_0900_ai_ci`.
93
+ *
94
+ * Used for `byline_document_paths.path`. MySQL's default collation is
95
+ * accent- *and* case-insensitive, so `/About` and `/about` collide as the
96
+ * same path on MySQL while remaining two distinct paths on Postgres (whose
97
+ * default collation carries no such folding). The project owner's initial
98
+ * instinct was that MySQL's default might be the *better* semantic for
99
+ * URLs — until testing against a live server showed `ai_ci` also collapses
100
+ * combining marks Byline's slugifier deliberately preserves for non-Latin
101
+ * scripts: `กา` = `ก่า` (a Thai tone mark), `कान` = `कानं` (a Devanagari
102
+ * anusvara), `שלום` = `שָׁלוֹם` (Hebrew niqqud) — see
103
+ * `packages/core/src/utils/slugify.ts`, which folds Latin diacritics but
104
+ * explicitly keeps non-Latin combining marks, because those marks are
105
+ * meaning-bearing in a way a Latin accent mark on a URL slug typically
106
+ * isn't. Two Thai (or Devanagari, or Hebrew) slugs Byline intended as
107
+ * distinct documents would silently collide as one path on MySQL only.
108
+ * Ruling: pin `utf8mb4_bin` so the two adapters agree exactly. This is
109
+ * `path` only — the store *value* columns keep the database's default
110
+ * `ai_ci` collation; the resulting accent-insensitive `LIKE` divergence
111
+ * there was elected deliberately by the plan and stands.
112
+ */
113
+ export declare const varcharCaseSensitive: {
114
+ (): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
115
+ name: "";
116
+ dataType: 'custom';
117
+ columnType: 'MySqlCustomColumn';
118
+ data: string;
119
+ driverParam: string;
120
+ enumValues: undefined;
121
+ }>;
122
+ <TConfig extends Record<string, any> & {
123
+ length: number;
124
+ }>(fieldConfig?: TConfig | undefined): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
125
+ name: "";
126
+ dataType: 'custom';
127
+ columnType: 'MySqlCustomColumn';
128
+ data: string;
129
+ driverParam: string;
130
+ enumValues: undefined;
131
+ }>;
132
+ <TName extends string>(dbName: TName, fieldConfig?: {
133
+ length: number;
134
+ } | undefined): import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
135
+ name: TName;
136
+ dataType: 'custom';
137
+ columnType: 'MySqlCustomColumn';
138
+ data: string;
139
+ driverParam: string;
140
+ enumValues: undefined;
141
+ }>;
142
+ };
143
+ /**
144
+ * Both `created_at` and `updated_at` for tables whose rows are
145
+ * mutated in place (most application tables — users, roles, documents,
146
+ * store rows, paths).
147
+ *
148
+ * Spread into a `mysqlTable` definition:
149
+ *
150
+ * mysqlTable('byline_admin_users', {
151
+ * id: uuidChar('id').primaryKey(),
152
+ * ...timestamps,
153
+ * })
154
+ */
155
+ export declare const timestamps: {
156
+ created_at: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/mysql-core").MySqlDateTimeBuilderInitial<string>>>;
157
+ updated_at: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/mysql-core").MySqlDateTimeBuilderInitial<string>>>;
158
+ };
159
+ /**
160
+ * `created_at` only — for tables whose rows are immutable once
161
+ * inserted (junction tables like `byline_admin_role_admin_user`,
162
+ * registry rows like `byline_counter_groups`).
163
+ *
164
+ * Spread into a `mysqlTable` definition the same way as `timestamps`.
165
+ */
166
+ export declare const createdAt: {
167
+ created_at: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/mysql-core").MySqlDateTimeBuilderInitial<string>>>;
168
+ };
@@ -0,0 +1,130 @@
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 { sql } from 'drizzle-orm';
9
+ import { customType, datetime } from 'drizzle-orm/mysql-core';
10
+ /**
11
+ * `CHAR(36) CHARACTER SET ascii COLLATE ascii_bin` — canonical UUID text.
12
+ *
13
+ * Every id / FK column in the schema uses this type. `ascii_bin` is a
14
+ * byte-wise (case-sensitive) collation, so id equality comparisons and
15
+ * joins compare bytes rather than applying a case- or accent-insensitive
16
+ * collation — the MySQL analogue of comparing `uuid` values in Postgres,
17
+ * which has no notion of collation for that type at all. UUIDs are
18
+ * app-generated UUIDv7 (the `uuid` package's `v7()`), never DB-generated.
19
+ */
20
+ export const uuidChar = customType({
21
+ dataType: () => 'char(36) CHARACTER SET ascii COLLATE ascii_bin',
22
+ });
23
+ /**
24
+ * `varchar(...)` with explicit byte-wise (`ascii_bin`) collation.
25
+ *
26
+ * Used for `byline_documents.order_key` (and `byline_document_relationships
27
+ * .order_key`) so the column sorts the same way JavaScript string
28
+ * comparison does. The fractional-index algorithm in `@byline/core`
29
+ * (`generateKeyBetween`, `generateNKeysBetween`) is designed against
30
+ * byte-wise ordering; MySQL's default collation (`utf8mb4_0900_ai_ci` on
31
+ * this database) is accent- and case-insensitive and disagrees with JS on
32
+ * cases like `'Zz' vs 'a0'` — which causes a refetch after a drag-reorder
33
+ * to "snap" the moved row back to its original position. Mirrors the
34
+ * Postgres adapter's `varcharByteSorted` (`COLLATE "C"`); see
35
+ * `packages/db-postgres/src/database/schema/index.ts`.
36
+ */
37
+ export const varcharByteSorted = customType({
38
+ dataType: (config) => {
39
+ const len = config?.length ?? 255;
40
+ return `varchar(${len}) CHARACTER SET ascii COLLATE ascii_bin`;
41
+ },
42
+ });
43
+ /**
44
+ * `varchar(...)` with the `utf8mb4_bin` collation — full `utf8mb4` charset
45
+ * (unlike `uuidChar/varcharByteSorted`'s ascii-only columns, this one must
46
+ * carry arbitrary Unicode), but byte-wise, case- and accent-sensitive
47
+ * comparison instead of the database's default `utf8mb4_0900_ai_ci`.
48
+ *
49
+ * Used for `byline_document_paths.path`. MySQL's default collation is
50
+ * accent- *and* case-insensitive, so `/About` and `/about` collide as the
51
+ * same path on MySQL while remaining two distinct paths on Postgres (whose
52
+ * default collation carries no such folding). The project owner's initial
53
+ * instinct was that MySQL's default might be the *better* semantic for
54
+ * URLs — until testing against a live server showed `ai_ci` also collapses
55
+ * combining marks Byline's slugifier deliberately preserves for non-Latin
56
+ * scripts: `กา` = `ก่า` (a Thai tone mark), `कान` = `कानं` (a Devanagari
57
+ * anusvara), `שלום` = `שָׁלוֹם` (Hebrew niqqud) — see
58
+ * `packages/core/src/utils/slugify.ts`, which folds Latin diacritics but
59
+ * explicitly keeps non-Latin combining marks, because those marks are
60
+ * meaning-bearing in a way a Latin accent mark on a URL slug typically
61
+ * isn't. Two Thai (or Devanagari, or Hebrew) slugs Byline intended as
62
+ * distinct documents would silently collide as one path on MySQL only.
63
+ * Ruling: pin `utf8mb4_bin` so the two adapters agree exactly. This is
64
+ * `path` only — the store *value* columns keep the database's default
65
+ * `ai_ci` collation; the resulting accent-insensitive `LIKE` divergence
66
+ * there was elected deliberately by the plan and stands.
67
+ */
68
+ export const varcharCaseSensitive = customType({
69
+ dataType: (config) => {
70
+ const len = config?.length ?? 255;
71
+ return `varchar(${len}) COLLATE utf8mb4_bin`;
72
+ },
73
+ });
74
+ /**
75
+ * Audit-timestamp column shape used across every Byline table.
76
+ * `DATETIME(6)` — microsecond precision, matching the Postgres adapter's
77
+ * `timestamp(name, { precision: 6, withTimezone: true })` exactly (see
78
+ * `packages/db-postgres/src/database/schema/common.ts`'s `auditTimestamp`).
79
+ * MySQL supports fractional seconds 0–6; picking 6 gives this adapter the
80
+ * same clock resolution as pg rather than a coarser one.
81
+ *
82
+ * This was `fsp: 3` (millisecond) originally, rejected only for
83
+ * `TIMESTAMP`'s 2038 range limit — with no stated rationale for choosing 3
84
+ * over 6. That turned out to matter: at millisecond resolution, two
85
+ * statements issued back-to-back on a fast local connection can land in the
86
+ * same tick and receive an *identical* `CURRENT_TIMESTAMP(3)` value, which
87
+ * is a real correctness gap for anything that orders or windows by these
88
+ * columns (found via `packages/db-conformance`'s audit activity-feed
89
+ * fixture — see docs/09-testing.md and the Task 11 report for the
90
+ * live-server evidence: four rapid statements produced 2 distinct values at
91
+ * fsp 3 versus 4 distinct values at fsp 6). Matching pg's precision closes
92
+ * that gap at the source instead of asking every fixture/consumer to work
93
+ * around a coarser clock.
94
+ *
95
+ * Stored and read as UTC by convention (the mysql2 pool is opened with
96
+ * `timezone: 'Z'` so values round-trip without local-timezone
97
+ * reinterpretation). MySQL's `DATETIME` has no timezone-aware storage the
98
+ * way Postgres's `TIMESTAMPTZ` does, so the UTC discipline is enforced
99
+ * entirely at the connection layer rather than the column type.
100
+ *
101
+ * Defined once here so adding a new column to every table — or changing
102
+ * the precision across the schema — is a one-line edit.
103
+ */
104
+ const auditTimestamp = (name) => datetime(name, { fsp: 6 }).notNull().default(sql `CURRENT_TIMESTAMP(6)`);
105
+ /**
106
+ * Both `created_at` and `updated_at` for tables whose rows are
107
+ * mutated in place (most application tables — users, roles, documents,
108
+ * store rows, paths).
109
+ *
110
+ * Spread into a `mysqlTable` definition:
111
+ *
112
+ * mysqlTable('byline_admin_users', {
113
+ * id: uuidChar('id').primaryKey(),
114
+ * ...timestamps,
115
+ * })
116
+ */
117
+ export const timestamps = {
118
+ created_at: auditTimestamp('created_at'),
119
+ updated_at: auditTimestamp('updated_at'),
120
+ };
121
+ /**
122
+ * `created_at` only — for tables whose rows are immutable once
123
+ * inserted (junction tables like `byline_admin_role_admin_user`,
124
+ * registry rows like `byline_counter_groups`).
125
+ *
126
+ * Spread into a `mysqlTable` definition the same way as `timestamps`.
127
+ */
128
+ export const createdAt = {
129
+ created_at: auditTimestamp('created_at'),
130
+ };