@byline/db-mysql 4.8.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,15 @@
1
1
  # @byline/db-mysql
2
2
 
3
- > **Status: preliminary.** This adapter is published so it can be evaluated and
4
- > exercised, not because MySQL is a fully supported Byline backend yet. The
5
- > storage layer is complete and proven — it passes the entire shared
6
- > `@byline/db-conformance` behavioural suite, the same suite `@byline/db-postgres`
7
- > passes, with identical results. What is missing is the surrounding ecosystem:
3
+ > **Status: generally available.** The storage adapter passes the same
4
+ > `@byline/db-conformance` suite as `@byline/db-postgres`,
5
+ > `@byline/search-mysql` supplies the matching search provider, and
6
+ > `@byline/cli` can provision and scaffold a MySQL installation.
8
7
  >
9
- > - **No search provider.** `@byline/search-postgres` has no MySQL counterpart, and
10
- > this is not merely a missing feature `initBylineCore()` **throws at boot** if
11
- > any collection declares `search` and no provider is registered. See
12
- > [Search on MySQL](#search-on-mysql) below for the one-step workaround you will
13
- > need. Tracked in
14
- > [#52](https://github.com/Byline-CMS/bylinecms.dev/issues/52).
15
- > - **No CLI support.** `byline init` scaffolds a Postgres installation; wiring
16
- > MySQL is a manual edit to your `server.config.ts`.
17
- > - **Narrower CI coverage than Postgres.** Continuous integration pins MySQL 8.0
18
- > — the engine floor — so nothing yet exercises 9.x automatically, and no leg
19
- > runs under a non-UTC timezone. Tracked in
20
- > [#55](https://github.com/Byline-CMS/bylinecms.dev/issues/55).
21
- >
22
- > Treat it as suitable for evaluation, prototypes, and installations that do not
23
- > need search. The criteria for general availability are tracked in
24
- > [#58](https://github.com/Byline-CMS/bylinecms.dev/issues/58).
8
+ > Continuous integration pins MySQL 8.0 to exercise the supported engine floor.
9
+ > Both adapter conformance suites also run under a non-UTC timezone
10
+ > (`Asia/Kathmandu`) so calendar-date handling cannot silently depend on the
11
+ > host timezone. MySQL 9.x remains covered by local development with
12
+ > `mysql/docker-compose.yml`, which runs `mysql:latest`.
25
13
 
26
14
  MySQL adapter for Byline CMS — Drizzle schema, migrations, and the storage /
27
15
  queries / commands implementation behind `IDbAdapter`. It is Byline's second
@@ -42,6 +30,20 @@ workflow, and content translation as first-class concerns.
42
30
 
43
31
  ## Install
44
32
 
33
+ For a new Byline installation, let the CLI select the package, environment
34
+ variable, baseline, and server configuration together:
35
+
36
+ ```sh
37
+ npx @byline/cli@latest init --database mysql
38
+ ```
39
+
40
+ The generated application uses `@byline/db-mysql`,
41
+ `@byline/db-mysql/admin`, and
42
+ `BYLINE_DB_MYSQL_CONNECTION_STRING`. If you include the example collections,
43
+ it also installs and registers `@byline/search-mysql`.
44
+
45
+ For a hand-wired application:
46
+
45
47
  ```sh
46
48
  pnpm add @byline/db-mysql
47
49
  ```
@@ -82,10 +84,9 @@ const core = await initBylineCore({
82
84
  })
83
85
  ```
84
86
 
85
- This mirrors how `apps/webapp/byline/server.config.ts` wires `@byline/db-postgres` today
86
- swap `pgAdapter` / `@byline/db-postgres/admin` for `mysqlAdapter` /
87
- `@byline/db-mysql/admin` and the rest of an existing Byline server config is unchanged,
88
- because both adapters implement the same `IDbAdapter` and `AdminStore` contracts.
87
+ This is the wiring generated by `byline init --database mysql`. Both database
88
+ adapters implement the same `IDbAdapter` and `AdminStore` contracts, so the
89
+ rest of the server configuration is adapter-independent.
89
90
 
90
91
  `connectionString` is the **only** connection input `mysqlAdapter` takes, and
91
92
  it must be a `mysql://` URL — mysql2 parses connection URLs natively (`uri`
@@ -198,12 +199,10 @@ databases, and a few divergences are real rather than papered over.
198
199
  are meaning-bearing in some scripts (a Thai tone mark, a Devanagari
199
200
  anusvara, Hebrew niqqud) are never silently folded together the way
200
201
  `ai_ci` would fold them.
201
- - **No search provider yet.** `@byline/search-postgres` has no MySQL
202
- counterpart today a collection's `search` config
203
- (`CollectionDefinition.search`) requires a registered `SearchProvider`,
204
- and none ships for MySQL yet. Track
205
- [the `@byline/search-mysql` follow-up issue](https://github.com/Byline-CMS/bylinecms.dev/issues/52)
206
- for status.
202
+ - **Search uses a dialect-specific provider.** Register
203
+ `@byline/search-mysql` with this adapter. It implements the same portable
204
+ lexical and weighting contract as `@byline/search-postgres`, while owning its
205
+ own MySQL `FULLTEXT` schema and migrations.
207
206
  - **The connection string is the only connection input.** See "Usage"
208
207
  above — this is called out here too because it is the most common way
209
208
  this adapter is misconfigured by a reader coming from the Postgres
@@ -218,60 +217,32 @@ databases, and a few divergences are real rather than papered over.
218
217
 
219
218
  ## Search on MySQL
220
219
 
221
- There is no MySQL search provider yet, and the absence is load-bearing rather
222
- than cosmetic: `validateSearchConfig` runs inside `initBylineCore()` and
223
- **throws** when any collection declares a `search` block but no provider is
224
- registered. Byline's own reference application opts five collections into
225
- search, so a MySQL installation that copies it fails at boot with an error that
226
- does not mention MySQL at all.
227
-
228
- Until `@byline/search-mysql` exists, register a no-op provider. It satisfies
229
- validation, and indexing and querying become silent no-ops rather than errors —
230
- the admin Reindex action and any search page return nothing, which is the honest
231
- answer on this adapter today:
220
+ Register `@byline/search-mysql` with the adapter's existing promise pool. Apply
221
+ the provider's independent migration stream before serving traffic:
232
222
 
233
223
  ```ts
234
- const noopSearch = {
235
- capabilities: {
236
- facets: false,
237
- typoTolerance: false,
238
- semantic: false,
239
- bm25: false,
240
- weighting: false,
241
- highlights: false,
242
- },
243
- async upsert() {},
244
- async remove() {},
245
- async search() {
246
- return { hits: [], total: 0 }
247
- },
248
- }
224
+ import { migrate, mysqlSearch } from '@byline/search-mysql'
225
+
226
+ await migrate(db.pool)
249
227
 
250
228
  await initBylineCore({
251
229
  // …
252
230
  db,
253
- search: noopSearch,
231
+ search: mysqlSearch({ pool: db.pool, defaultLocale }),
254
232
  })
255
233
  ```
256
234
 
257
- The alternative removing the `search` block from every collection definition —
258
- also works, but it discards configuration you will want back when a provider
259
- lands. The no-op keeps your collections declarative and confines the workaround
260
- to one place.
261
-
262
- ## Not yet shipped
263
-
264
- - **`@byline/search-mysql`** see [Search on MySQL](#search-on-mysql) above.
265
- Tracked in [issue #52](https://github.com/Byline-CMS/bylinecms.dev/issues/52).
266
- - **CLI support.** `byline init` scaffolds a Postgres installation. Adding MySQL
267
- to an existing project is a manual edit to `byline/server.config.ts` swap
268
- `pgAdapter` for `mysqlAdapter`, swap the `@byline/db-postgres/admin` import for
269
- `@byline/db-mysql/admin`, drop the `@byline/search-postgres` migrate call, and
270
- register the no-op search provider above.
271
- - **A MySQL storage-benchmark target.** The design spec flags view
272
- materialisation (the `ROW_NUMBER()` window inside a derived table, used
273
- by both current-version views) as a question to answer before this
274
- adapter's GA — Postgres's own benchmark sweep
235
+ Search rows are a disposable projection of published versions. The provider
236
+ uses parser-safe portable terms, weighted MySQL `FULLTEXT` indexes, and analyzer
237
+ fingerprints. Its package README documents migrations, capabilities, and the
238
+ clear-and-rebuild procedure.
239
+
240
+ ## Current boundaries
241
+
242
+ - **A MySQL storage-benchmark target (non-blocking).** The design spec flags
243
+ view materialisation (the `ROW_NUMBER()` window inside a derived table,
244
+ used by both current-version views) as useful performance-characterisation
245
+ work. It is not a release gate. PostgreSQL's own benchmark sweep
275
246
  ([`docs/03-architecture/01-document-storage.md`](https://github.com/Byline-CMS/bylinecms.dev/blob/develop/docs/03-architecture/01-document-storage.md#indicative-benchmarks))
276
247
  has no MySQL counterpart yet. Tracked in
277
248
  [issue #53](https://github.com/Byline-CMS/bylinecms.dev/issues/53).
@@ -20,7 +20,7 @@
20
20
  * to 1; bumped by write paths when needed).
21
21
  * - snake_case column names matching the rest of the Byline schema.
22
22
  *
23
- * See docs/06-auth-and-security/01-authn-authz.md for the full data model and present-state
23
+ * See docs/07-auth-and-security/01-authn-authz.md for the full data model and present-state
24
24
  * reference.
25
25
  */
26
26
  import { relations, sql } from 'drizzle-orm';
@@ -86,7 +86,7 @@ export const varcharCaseSensitive = customType({
86
86
  * same tick and receive an *identical* `CURRENT_TIMESTAMP(3)` value, which
87
87
  * is a real correctness gap for anything that orders or windows by these
88
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
89
+ * fixture — see docs/10-testing.md and the Task 11 report for the
90
90
  * live-server evidence: four rapid statements produced 2 distinct values at
91
91
  * fsp 3 versus 4 distinct values at fsp 6). Matching pg's precision closes
92
92
  * that gap at the source instead of asking every fixture/consumer to work
@@ -168,7 +168,7 @@ export const documentPaths = mysqlTable('byline_document_paths', {
168
168
  // carries forward across edits and survives restore. Surfaced on reads as
169
169
  // `availableLocales`; the public advertised set is the intersection with the
170
170
  // ledger's `_availableVersionLocales`. Replaced wholesale on write (the lifecycle
171
- // deletes then re-inserts the set), never appended. See docs/07-internationalization/index.md.
171
+ // deletes then re-inserts the set), never appended. See docs/08-internationalization/index.md.
172
172
  export const documentAvailableLocales = mysqlTable('byline_document_available_locales', {
173
173
  document_id: uuidChar('document_id').notNull(),
174
174
  locale: varchar('locale', { length: 10 }).notNull(),
@@ -198,7 +198,7 @@ export const documentAvailableLocales = mysqlTable('byline_document_available_lo
198
198
  // identically in any locale). Computed status-blind at write time and frozen
199
199
  // on the immutable version, so restore / point-in-time reads stay consistent.
200
200
  // Drives `localeFallback: 'strict'` reads via an indexed EXISTS gate without
201
- // scanning the store_* tables. See docs/07-internationalization/index.md.
201
+ // scanning the store_* tables. See docs/08-internationalization/index.md.
202
202
  export const documentVersionLocales = mysqlTable('byline_document_version_locales', {
203
203
  document_version_id: uuidChar('document_version_id').notNull(),
204
204
  locale: varchar('locale', { length: 10 }).notNull(),
@@ -300,7 +300,7 @@ export const currentDocumentsView = mysqlView('byline_current_documents').as((qb
300
300
  // read paths (`buildLocaleChain` / `pathProjection` / field-fallback)
301
301
  // re-base onto the per-document source rather than the mutable global
302
302
  // default — a primary-key join, already present for `order_key`.
303
- // See docs/07-internationalization/index.md.
303
+ // See docs/08-internationalization/index.md.
304
304
  source_locale: documents.source_locale,
305
305
  })
306
306
  .from(sq)
@@ -407,7 +407,7 @@ export const textStore = mysqlTable('byline_store_text', {
407
407
  // no query code references this index name — and Byline's actual
408
408
  // full-text search is a separate, pluggable `SearchProvider` seam
409
409
  // (`@byline/core`), not built on this table's indexes. See
410
- // docs/05-reading-and-delivery/07-search.md.
410
+ // docs/06-search/index.md.
411
411
  index('idx_text_locale_value').on(table.locale, sql `${table.value}(191)`),
412
412
  index('idx_text_path_value').on(table.field_path, sql `${table.value}(191)`),
413
413
  // Unique constraints for unique fields
@@ -860,7 +860,7 @@ export const jsonStoreRelations = relations(jsonStore, ({ one }) => ({
860
860
  // fit without a second migration. Append-only and deliberately **FK-free**: an
861
861
  // audit row is an immutable historical fact that must outlive the document,
862
862
  // collection, or actor it references — a `document.deleted` row cannot be
863
- // allowed to cascade-delete itself. See docs/06-auth-and-security/02-auditability.md — Workstream 2.
863
+ // allowed to cascade-delete itself. See docs/07-auth-and-security/02-auditability.md — Workstream 2.
864
864
  export const auditLog = mysqlTable('byline_audit_log', {
865
865
  id: uuidChar('id').primaryKey(), // UUIDv7 — time-ordered, so id ordering ≈ time ordering
866
866
  document_id: uuidChar('document_id'), // NULL for admin-realm (non-document) events; no FK
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export declare const mysqlAdapter: ({ connectionString, collections, defaultCont
36
36
  * for documents whose `source_locale` is not yet backfilled. Per-document
37
37
  * reads and writes otherwise re-base onto each document's own `source_locale`
38
38
  * (carried on the current-documents views), so changing this value does not
39
- * re-interpret existing data. See docs/07-internationalization/index.md.
39
+ * re-interpret existing data. See docs/08-internationalization/index.md.
40
40
  */
41
41
  defaultContentLocale: string;
42
42
  /**
@@ -11,6 +11,9 @@
11
11
  * `initBylineCore()` boot rather than surfacing as an obscure SQL error the
12
12
  * first time the storage layer emits a LATERAL join (Task 10+).
13
13
  */
14
+ // Keep this adapter floor synchronized with the pre-install check in
15
+ // `packages/cli/src/lib/database/mysql.ts`; the CLI contract test compares
16
+ // the two constants without adding a runtime dependency on this package.
14
17
  const MIN = { major: 8, minor: 0, patch: 14 };
15
18
  const unsupportedEngineError = (reported) => new Error(`@byline/db-mysql requires MySQL ${MIN.major}.${MIN.minor}.${MIN.patch}+ (LATERAL joins); server reports ${reported}. MariaDB is not supported.`);
16
19
  /**
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  /**
9
- * Append-only audit-log writes (docs/06-auth-and-security/02-auditability.md — Workstream 2). A deliberately
9
+ * Append-only audit-log writes (docs/07-auth-and-security/02-auditability.md — Workstream 2). A deliberately
10
10
  * dumb command: it inserts one row and knows nothing about *which* changes
11
11
  * warrant an audit entry — that policy lives in the lifecycle services, which
12
12
  * wrap the mutation + this append in `withTransaction` so they commit
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  /**
9
- * Audit-log reads (docs/06-auth-and-security/02-auditability.md — Workstreams 3 & 4). Reads run on the pool
9
+ * Audit-log reads (docs/07-auth-and-security/02-auditability.md — Workstreams 3 & 4). Reads run on the pool
10
10
  * directly — they never need to join an audit write's transaction — so this
11
11
  * takes the raw `db` (drizzle over the pool) rather than the `DBManager`.
12
12
  * Access scoping is the caller's responsibility (the document's own read
@@ -205,7 +205,7 @@ export declare class DocumentCommands implements IDocumentCommands {
205
205
  * clause *after* `INSERT INTO <table>` and immediately before the `SELECT`
206
206
  * it modifies (confirmed against a live MySQL 9.7.1 server) — so the two
207
207
  * clauses are transposed relative to the pg original. See
208
- * docs/07-internationalization/index.md.
208
+ * docs/08-internationalization/index.md.
209
209
  */
210
210
  private writeVersionLocaleLedger;
211
211
  /**
@@ -268,7 +268,7 @@ export declare class DocumentCommands implements IDocumentCommands {
268
268
  * **without** minting a new document version or touching workflow status. The
269
269
  * change is immediate and applies across every version of the document; the
270
270
  * public advertised set remains the intersection with the resolved version's
271
- * completeness ledger. See docs/07-internationalization/index.md.
271
+ * completeness ledger. See docs/08-internationalization/index.md.
272
272
  */
273
273
  setDocumentAvailableLocales(params: {
274
274
  documentId: string;
@@ -192,7 +192,7 @@ export class DocumentCommands {
192
192
  // source locale rather than the mutable global default. NULL (a row not
193
193
  // yet touched by `backfillSourceLocales`) falls back to the configured
194
194
  // default — the value it was implicitly authored against.
195
- // See docs/07-internationalization/index.md.
195
+ // See docs/08-internationalization/index.md.
196
196
  let sourceLocale;
197
197
  if (documentId == null) {
198
198
  documentId = uuidv7();
@@ -345,7 +345,7 @@ export class DocumentCommands {
345
345
  // accounts for the per-locale carry-forward in step 5 — not just the
346
346
  // freshly-flattened locale. A version with no localized content at all
347
347
  // records a single `'all'` sentinel (it renders identically in any
348
- // locale). Status-blind by design — see docs/07-internationalization/index.md.
348
+ // locale). Status-blind by design — see docs/08-internationalization/index.md.
349
349
  await this.writeVersionLocaleLedger(tx, documentVersion.id, sourceLocale);
350
350
  return {
351
351
  document: documentVersion,
@@ -446,7 +446,7 @@ export class DocumentCommands {
446
446
  * clause *after* `INSERT INTO <table>` and immediately before the `SELECT`
447
447
  * it modifies (confirmed against a live MySQL 9.7.1 server) — so the two
448
448
  * clauses are transposed relative to the pg original. See
449
- * docs/07-internationalization/index.md.
449
+ * docs/08-internationalization/index.md.
450
450
  */
451
451
  async writeVersionLocaleLedger(tx, versionId, sourceLocale) {
452
452
  await tx.execute(sql `
@@ -558,7 +558,7 @@ export class DocumentCommands {
558
558
  * **without** minting a new document version or touching workflow status. The
559
559
  * change is immediate and applies across every version of the document; the
560
560
  * public advertised set remains the intersection with the resolved version's
561
- * completeness ledger. See docs/07-internationalization/index.md.
561
+ * completeness ledger. See docs/08-internationalization/index.md.
562
562
  */
563
563
  async setDocumentAvailableLocales(params) {
564
564
  await this.db.transaction(async (tx) => {
@@ -128,7 +128,7 @@ export declare class DocumentQueries implements IDocumentQueries {
128
128
  * document, or any document read after the global default is switched, falls
129
129
  * back to the locale it was actually authored in) — otherwise the configured
130
130
  * global default, which is correct for not-yet-anchored rows and for
131
- * row-less lookups (findByPath). See docs/07-internationalization/index.md.
131
+ * row-less lookups (findByPath). See docs/08-internationalization/index.md.
132
132
  */
133
133
  private buildLocaleChain;
134
134
  /**
@@ -161,7 +161,7 @@ export class DocumentQueries {
161
161
  * document, or any document read after the global default is switched, falls
162
162
  * back to the locale it was actually authored in) — otherwise the configured
163
163
  * global default, which is correct for not-yet-anchored rows and for
164
- * row-less lookups (findByPath). See docs/07-internationalization/index.md.
164
+ * row-less lookups (findByPath). See docs/08-internationalization/index.md.
165
165
  */
166
166
  buildLocaleChain(requestedLocale, sourceLocale) {
167
167
  const floor = sourceLocale ?? this.defaultContentLocale;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/db-mysql",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "4.8.0",
5
+ "version": "4.10.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -50,8 +50,8 @@
50
50
  "drizzle-orm": "^0.45.2",
51
51
  "mysql2": "^3.23.1",
52
52
  "uuid": "^14.0.1",
53
- "@byline/admin": "4.8.0",
54
- "@byline/core": "4.8.0"
53
+ "@byline/admin": "4.10.0",
54
+ "@byline/core": "4.10.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@biomejs/biome": "2.5.4",
@@ -65,9 +65,9 @@
65
65
  "tsx": "^4.23.1",
66
66
  "typescript": "^7.0.2",
67
67
  "vitest": "^4.1.10",
68
- "@byline/auth": "4.8.0",
69
- "@byline/db-conformance": "0.0.2",
70
- "@byline/client": "4.8.0"
68
+ "@byline/auth": "4.10.0",
69
+ "@byline/db-conformance": "0.0.4",
70
+ "@byline/client": "4.10.0"
71
71
  },
72
72
  "publishConfig": {
73
73
  "access": "public",