@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.
- package/LICENSE +373 -0
- package/README.md +283 -0
- package/dist/database/schema/auth.d.ts +951 -0
- package/dist/database/schema/auth.js +226 -0
- package/dist/database/schema/common.d.ts +168 -0
- package/dist/database/schema/common.js +130 -0
- package/dist/database/schema/index.d.ts +3419 -0
- package/dist/database/schema/index.js +895 -0
- package/dist/database/schema/schema-pins.test.node.d.ts +52 -0
- package/dist/database/schema/schema-pins.test.node.js +398 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +151 -0
- package/dist/lib/boot-check.d.ts +22 -0
- package/dist/lib/boot-check.js +42 -0
- package/dist/lib/boot-check.test.node.d.ts +8 -0
- package/dist/lib/boot-check.test.node.js +46 -0
- package/dist/lib/db-manager.d.ts +54 -0
- package/dist/lib/db-manager.js +49 -0
- package/dist/lib/test-db.d.ts +37 -0
- package/dist/lib/test-db.js +111 -0
- package/dist/lib/test-helper.d.ts +33 -0
- package/dist/lib/test-helper.js +68 -0
- package/dist/modules/admin/admin-permissions-repository.d.ts +35 -0
- package/dist/modules/admin/admin-permissions-repository.js +101 -0
- package/dist/modules/admin/admin-preferences-repository.d.ts +62 -0
- package/dist/modules/admin/admin-preferences-repository.js +156 -0
- package/dist/modules/admin/admin-roles-repository.d.ts +11 -0
- package/dist/modules/admin/admin-roles-repository.js +209 -0
- package/dist/modules/admin/admin-store.d.ts +20 -0
- package/dist/modules/admin/admin-store.js +30 -0
- package/dist/modules/admin/admin-users-repository.d.ts +11 -0
- package/dist/modules/admin/admin-users-repository.js +303 -0
- package/dist/modules/admin/index.d.ts +27 -0
- package/dist/modules/admin/index.js +27 -0
- package/dist/modules/admin/refresh-tokens-repository.d.ts +34 -0
- package/dist/modules/admin/refresh-tokens-repository.js +160 -0
- package/dist/modules/audit/audit-commands.d.ts +29 -0
- package/dist/modules/audit/audit-commands.js +40 -0
- package/dist/modules/audit/audit-queries.d.ts +41 -0
- package/dist/modules/audit/audit-queries.js +169 -0
- package/dist/modules/counters/counters-commands.d.ts +54 -0
- package/dist/modules/counters/counters-commands.js +121 -0
- package/dist/modules/counters/tests/counters-concurrency.test.d.ts +8 -0
- package/dist/modules/counters/tests/counters-concurrency.test.js +111 -0
- package/dist/modules/storage/classify-error.d.ts +34 -0
- package/dist/modules/storage/classify-error.js +50 -0
- package/dist/modules/storage/classify-error.test.node.d.ts +8 -0
- package/dist/modules/storage/classify-error.test.node.js +89 -0
- package/dist/modules/storage/normalize-row.d.ts +55 -0
- package/dist/modules/storage/normalize-row.js +135 -0
- package/dist/modules/storage/normalize-row.test.node.d.ts +8 -0
- package/dist/modules/storage/normalize-row.test.node.js +89 -0
- package/dist/modules/storage/storage-commands.d.ts +443 -0
- package/dist/modules/storage/storage-commands.js +1263 -0
- package/dist/modules/storage/storage-insert.d.ts +21 -0
- package/dist/modules/storage/storage-insert.js +152 -0
- package/dist/modules/storage/storage-queries.d.ts +805 -0
- package/dist/modules/storage/storage-queries.js +1815 -0
- package/dist/modules/storage/storage-store-manifest.d.ts +77 -0
- package/dist/modules/storage/storage-store-manifest.js +168 -0
- package/dist/modules/storage/storage-utils.d.ts +49 -0
- package/dist/modules/storage/storage-utils.js +76 -0
- package/dist/modules/storage/tests/dialect-pins.integration.test.d.ts +8 -0
- package/dist/modules/storage/tests/dialect-pins.integration.test.js +266 -0
- package/dist/modules/storage/tests/storage-commands.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-commands.test.js +324 -0
- package/dist/modules/storage/tests/storage-document-paths.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-paths.test.js +214 -0
- package/dist/modules/storage/tests/storage-document-tree.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-tree.test.js +361 -0
- package/dist/modules/storage/tests/storage-queries.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-queries.test.js +685 -0
- package/dist/modules/storage/tests/storage-status-and-lifecycle.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-status-and-lifecycle.test.js +268 -0
- 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
|
+
};
|