@byline/search-mysql 4.9.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.
@@ -0,0 +1,157 @@
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 { encodeSqlToken, } from '@byline/search-analysis';
9
+ const WEIGHT_CLASSES = ['A', 'B', 'C', 'D'];
10
+ const STREAM_BOUNDARY = 'bylinefulltextboundary';
11
+ /**
12
+ * Encode portable logical terms into text streams consumed by MySQL FULLTEXT.
13
+ * Separate exact, expansion-kind, and Han-gram streams preserve adjacency for
14
+ * phrase matching without letting phrases cross weight classes or token
15
+ * streams.
16
+ */
17
+ export function buildPortableMySqlIndexDocument(row, analyzer) {
18
+ const matchingStreams = [];
19
+ const tokensByWeight = {
20
+ A: [],
21
+ B: [],
22
+ C: [],
23
+ D: [],
24
+ };
25
+ for (const sourceWeight of WEIGHT_CLASSES) {
26
+ const analyzed = analyzer.analyzeText({
27
+ text: row.weighted[sourceWeight],
28
+ locale: row.locale,
29
+ });
30
+ matchingStreams.push(...serializeMatchingStreams(analyzed.tokens));
31
+ for (const token of analyzed.tokens) {
32
+ tokensByWeight[tokenWeight(token, sourceWeight)].push(token);
33
+ }
34
+ }
35
+ const weighted = {
36
+ A: serializeStreams(tokensByWeight.A),
37
+ B: serializeStreams(tokensByWeight.B),
38
+ C: serializeStreams(tokensByWeight.C),
39
+ D: serializeStreams(tokensByWeight.D),
40
+ };
41
+ return {
42
+ searchText: matchingStreams.join(` ${STREAM_BOUNDARY} `),
43
+ weighted,
44
+ analyzerFingerprint: analyzer.fingerprint,
45
+ };
46
+ }
47
+ function tokenWeight(token, sourceWeight) {
48
+ if (token.kind === 'gram')
49
+ return 'D';
50
+ if (token.kind === 'exact' || token.kind === 'identifier')
51
+ return sourceWeight;
52
+ return lowerWeight(sourceWeight);
53
+ }
54
+ function lowerWeight(weight) {
55
+ switch (weight) {
56
+ case 'A':
57
+ return 'B';
58
+ case 'B':
59
+ return 'C';
60
+ case 'C':
61
+ case 'D':
62
+ return 'D';
63
+ }
64
+ }
65
+ function serializeStreams(tokens) {
66
+ const streams = [];
67
+ const source = tokens
68
+ .filter((token) => token.kind === 'exact' || token.kind === 'identifier')
69
+ .toSorted(compareTokens);
70
+ if (source.length > 0)
71
+ streams.push(source);
72
+ for (const kind of ['stem', 'lemma', 'normalized']) {
73
+ const derived = tokens.filter((token) => token.kind === kind).toSorted(compareTokens);
74
+ if (derived.length > 0)
75
+ streams.push(derived);
76
+ }
77
+ const grams = tokens.filter((token) => token.kind === 'gram').toSorted(compareTokens);
78
+ if (grams.length > 0)
79
+ streams.push(grams);
80
+ return streams
81
+ .map((stream) => stream.map((token) => encodeSqlToken(token)).join(' '))
82
+ .join(` ${STREAM_BOUNDARY} `);
83
+ }
84
+ /**
85
+ * Preserve source-token adjacency for matching while making each expansion
86
+ * kind phrase-capable. A derived stream falls back to the exact/identifier
87
+ * token at positions where that kind produced no alternative, so a query such
88
+ * as `"runs restoration"` can match indexed `"running restoration"` through
89
+ * the stem `run` without losing the unchanged neighboring term.
90
+ */
91
+ function serializeMatchingStreams(tokens) {
92
+ const source = tokens
93
+ .filter((token) => token.kind === 'exact' || token.kind === 'identifier')
94
+ .toSorted(compareTokens);
95
+ const sourceGroups = groupTokensByPosition(source);
96
+ const sourceStreams = buildSourceStreams(sourceGroups);
97
+ const streams = [...sourceStreams];
98
+ const primarySource = sourceGroups.map(preferredSourceToken);
99
+ for (const kind of ['stem', 'lemma', 'normalized']) {
100
+ const derived = tokens.filter((token) => token.kind === kind);
101
+ if (derived.length === 0)
102
+ continue;
103
+ const byPosition = new Map();
104
+ for (const token of derived.toSorted(compareTokens)) {
105
+ const positioned = byPosition.get(token.position);
106
+ if (positioned == null)
107
+ byPosition.set(token.position, [token]);
108
+ else
109
+ positioned.push(token);
110
+ }
111
+ streams.push(serializeTokenStream(primarySource.map((fallback) => byPosition.get(fallback.position)?.[0] ?? fallback)));
112
+ }
113
+ const grams = tokens.filter((token) => token.kind === 'gram').toSorted(compareTokens);
114
+ if (grams.length > 0)
115
+ streams.push(serializeTokenStream(grams));
116
+ return [...new Set(streams.filter((stream) => stream.length > 0))];
117
+ }
118
+ function buildSourceStreams(groups) {
119
+ if (groups.length === 0)
120
+ return [];
121
+ const streams = [serializeTokenStream(groups.map(preferredSourceToken))];
122
+ const exactVariantCount = Math.max(0, ...groups.map((group) => group.filter((token) => token.kind === 'exact').length));
123
+ for (let variant = 0; variant < exactVariantCount; variant++) {
124
+ streams.push(serializeTokenStream(groups.map((group) => {
125
+ const exact = group.filter((token) => token.kind === 'exact');
126
+ return exact[variant] ?? exact[0] ?? preferredSourceToken(group);
127
+ })));
128
+ }
129
+ return [...new Set(streams)];
130
+ }
131
+ function preferredSourceToken(group) {
132
+ const token = group.find((candidate) => candidate.kind === 'identifier') ??
133
+ group.find((candidate) => candidate.kind === 'exact');
134
+ if (token == null)
135
+ throw new TypeError('Portable source position has no searchable token');
136
+ return token;
137
+ }
138
+ function groupTokensByPosition(tokens) {
139
+ const groups = new Map();
140
+ for (const token of tokens) {
141
+ const group = groups.get(token.position);
142
+ if (group == null)
143
+ groups.set(token.position, [token]);
144
+ else
145
+ group.push(token);
146
+ }
147
+ return [...groups.values()];
148
+ }
149
+ function serializeTokenStream(tokens) {
150
+ return tokens.map((token) => encodeSqlToken(token)).join(' ');
151
+ }
152
+ function compareTokens(left, right) {
153
+ return (left.position - right.position ||
154
+ left.normalizedStart - right.normalizedStart ||
155
+ left.normalizedEnd - right.normalizedEnd ||
156
+ left.value.localeCompare(right.value));
157
+ }
@@ -0,0 +1,8 @@
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
+ export {};
@@ -0,0 +1,67 @@
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 { createPortableSearchAnalyzer, encodeSqlToken, } from '@byline/search-analysis';
9
+ import { describe, expect, it } from 'vitest';
10
+ import { buildPortableMySqlIndexDocument } from './portable-index-document.js';
11
+ const expander = {
12
+ fingerprint: 'english-test1',
13
+ supports: (locale) => locale.startsWith('en'),
14
+ expand: (token) => (token.value === 'running' ? [{ kind: 'stem', value: 'run' }] : []),
15
+ };
16
+ function row(overrides = {}) {
17
+ return {
18
+ collectionPath: 'reports',
19
+ documentId: 'report-1',
20
+ locale: 'en',
21
+ status: 'published',
22
+ zones: ['library'],
23
+ title: 'Running database',
24
+ path: 'running-database',
25
+ body: 'Running 数据库',
26
+ weighted: { A: 'Running 数据库', B: '', C: '', D: '' },
27
+ facets: {},
28
+ filters: {},
29
+ updatedAt: '2026-07-26T00:00:00.000Z',
30
+ ...overrides,
31
+ };
32
+ }
33
+ describe('buildPortableMySqlIndexDocument', () => {
34
+ it('keeps exact terms at source weight and lowers derived variants', () => {
35
+ const analyzer = createPortableSearchAnalyzer({ expanders: [expander] });
36
+ const document = buildPortableMySqlIndexDocument(row(), analyzer);
37
+ const exact = encodeSqlToken({ kind: 'exact', value: 'running' });
38
+ const stem = encodeSqlToken({ kind: 'stem', value: 'run' });
39
+ expect(document.weighted.A).toContain(exact);
40
+ expect(document.weighted.A).not.toContain(stem);
41
+ expect(document.weighted.B).toContain(stem);
42
+ expect(document.analyzerFingerprint).toBe(analyzer.fingerprint);
43
+ });
44
+ it('always assigns Han grams to the lightest weight class', () => {
45
+ const document = buildPortableMySqlIndexDocument(row(), createPortableSearchAnalyzer());
46
+ expect(document.weighted.D).toContain(encodeSqlToken({ kind: 'gram', value: '数据' }));
47
+ expect(document.weighted.D).toContain(encodeSqlToken({ kind: 'gram', value: '据库' }));
48
+ });
49
+ it('inserts unsearchable boundaries between independent streams', () => {
50
+ const document = buildPortableMySqlIndexDocument(row({ weighted: { A: 'alpha', B: 'beta', C: '', D: '' } }), createPortableSearchAnalyzer());
51
+ expect(document.searchText).toContain('bylinefulltextboundary');
52
+ });
53
+ it('uses exact fallbacks beside derived terms in matching streams', () => {
54
+ const analyzer = createPortableSearchAnalyzer({ expanders: [expander] });
55
+ const document = buildPortableMySqlIndexDocument(row({ weighted: { A: 'running restoration', B: '', C: '', D: '' } }), analyzer);
56
+ const stem = encodeSqlToken({ kind: 'stem', value: 'run' });
57
+ const exactNeighbor = encodeSqlToken({ kind: 'exact', value: 'restoration' });
58
+ expect(document.searchText).toContain(`${stem} ${exactNeighbor}`);
59
+ });
60
+ it('writes identifier and constituent phrase streams at one logical position', () => {
61
+ const document = buildPortableMySqlIndexDocument(row({ weighted: { A: 'COVID-19 cases', B: '', C: '', D: '' } }), createPortableSearchAnalyzer());
62
+ const cases = encodeSqlToken({ kind: 'exact', value: 'cases' });
63
+ expect(document.searchText).toContain(`${encodeSqlToken({ kind: 'identifier', value: 'covid-19' })} ${cases}`);
64
+ expect(document.searchText).toContain(`${encodeSqlToken({ kind: 'exact', value: 'covid' })} ${cases}`);
65
+ expect(document.searchText).toContain(`${encodeSqlToken({ kind: 'exact', value: '19' })} ${cases}`);
66
+ });
67
+ });
@@ -0,0 +1,22 @@
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 PortableQueryPlan } from '@byline/search-analysis';
9
+ export interface PortableMySqlQuery {
10
+ /** Boolean-mode query per source concept; terms inside one query are ORs. */
11
+ conceptQueries: string[];
12
+ /** Alternative quoted Boolean-mode queries for each required phrase. */
13
+ phraseQueries: string[][];
14
+ /** Ordered Han-gram fallback phrases. */
15
+ gramQueries: string[];
16
+ /** Flat optional-term query used only for weighted relevance scoring. */
17
+ rankingQuery: string;
18
+ operator: 'all' | 'any';
19
+ minimumShouldMatch?: number;
20
+ }
21
+ /** Translate a portable plan into parser-safe MySQL Boolean-mode inputs. */
22
+ export declare function buildPortableMySqlQuery(plan: PortableQueryPlan): PortableMySqlQuery;
@@ -0,0 +1,87 @@
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 { encodeSqlToken, } from '@byline/search-analysis';
9
+ /** Translate a portable plan into parser-safe MySQL Boolean-mode inputs. */
10
+ export function buildPortableMySqlQuery(plan) {
11
+ const conceptTerms = plan.concepts.map(conceptAlternatives);
12
+ if (conceptTerms.some((terms) => terms.length === 0)) {
13
+ throw new TypeError('Portable query plan contains a concept with no searchable terms');
14
+ }
15
+ const phraseQueries = plan.phrases.map((phrase) => {
16
+ const concepts = phrase.conceptIndexes.map((index) => plan.concepts[index]);
17
+ if (concepts.some((concept) => concept == null)) {
18
+ throw new RangeError('Portable query phrase references an unknown concept');
19
+ }
20
+ return phraseStreamVariants(concepts);
21
+ });
22
+ const gramQueries = plan.gramSequences
23
+ .map((sequence) => quotedPhrase(orderedTerms(sequence)))
24
+ .filter((query) => query.length > 0);
25
+ const rankingQuery = [
26
+ ...conceptTerms.flat(),
27
+ ...plan.gramSequences.flatMap((sequence) => orderedTerms(sequence)),
28
+ ]
29
+ .filter((term, index, terms) => terms.indexOf(term) === index)
30
+ .join(' ');
31
+ return {
32
+ conceptQueries: conceptTerms.map((terms) => terms.join(' ')),
33
+ phraseQueries,
34
+ gramQueries,
35
+ rankingQuery,
36
+ operator: plan.matching.operator,
37
+ ...(plan.matching.minimumShouldMatch != null
38
+ ? { minimumShouldMatch: plan.matching.minimumShouldMatch }
39
+ : {}),
40
+ };
41
+ }
42
+ function conceptAlternatives(concept) {
43
+ const terms = [
44
+ ...concept.exactTokens,
45
+ ...concept.stemTokens,
46
+ ...concept.lemmaTokens,
47
+ ...concept.normalizedTokens,
48
+ ...concept.identifierTokens,
49
+ ].map((token) => encodeSqlToken(token));
50
+ const gramPhrase = quotedPhrase(orderedTerms(concept.gramTokens));
51
+ if (gramPhrase.length > 0)
52
+ terms.push(gramPhrase);
53
+ return [...new Set(terms)];
54
+ }
55
+ /**
56
+ * Mirror the index's phrase-capable source and expansion-kind streams. Mixed
57
+ * expansion kinds never coexist in one indexed stream, so their Cartesian
58
+ * product would only produce impossible phrases.
59
+ */
60
+ function phraseStreamVariants(concepts) {
61
+ const source = concepts.map(sourceToken);
62
+ const variants = [source];
63
+ for (const kind of ['stemTokens', 'lemmaTokens', 'normalizedTokens']) {
64
+ if (!concepts.some((concept) => concept[kind].length > 0))
65
+ continue;
66
+ variants.push(concepts.map((concept, index) => concept[kind][0] ?? source[index]));
67
+ }
68
+ return [
69
+ ...new Set(variants
70
+ .map((tokens) => quotedPhrase(tokens.map((token) => encodeSqlToken(token))))
71
+ .filter((query) => query.length > 0)),
72
+ ];
73
+ }
74
+ function sourceToken(concept) {
75
+ const token = concept.identifierTokens[0] ?? concept.exactTokens[0];
76
+ if (token == null)
77
+ throw new TypeError('Portable query concept has no source token');
78
+ return token;
79
+ }
80
+ function orderedTerms(tokens) {
81
+ return tokens
82
+ .toSorted((left, right) => left.normalizedStart - right.normalizedStart || left.normalizedEnd - right.normalizedEnd)
83
+ .map((token) => encodeSqlToken(token));
84
+ }
85
+ function quotedPhrase(terms) {
86
+ return terms.length > 0 ? `"${terms.join(' ')}"` : '';
87
+ }
@@ -0,0 +1,8 @@
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
+ export {};
@@ -0,0 +1,68 @@
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 { createPortableSearchAnalyzer, encodeSqlToken, } from '@byline/search-analysis';
9
+ import { describe, expect, it } from 'vitest';
10
+ import { buildPortableMySqlQuery } from './portable-query.js';
11
+ const expander = {
12
+ fingerprint: 'english-test1',
13
+ supports: (locale) => locale.startsWith('en'),
14
+ expand: (token) => (token.value === 'running' ? [{ kind: 'stem', value: 'run' }] : []),
15
+ };
16
+ describe('buildPortableMySqlQuery', () => {
17
+ it('keeps alternatives grouped by source concept', () => {
18
+ const analyzer = createPortableSearchAnalyzer({ expanders: [expander] });
19
+ const translated = buildPortableMySqlQuery(analyzer.analyzeQuery({ query: 'running restoration', locale: 'en' }));
20
+ expect(translated.conceptQueries).toHaveLength(2);
21
+ expect(translated.conceptQueries[0]).toContain(encodeSqlToken({ kind: 'exact', value: 'running' }));
22
+ expect(translated.conceptQueries[0]).toContain(encodeSqlToken({ kind: 'stem', value: 'run' }));
23
+ expect(translated.operator).toBe('all');
24
+ });
25
+ it('preserves phrase order and minimum-should-match intent', () => {
26
+ const translated = buildPortableMySqlQuery(createPortableSearchAnalyzer().analyzeQuery({
27
+ query: '"forest restoration" database',
28
+ matching: { operator: 'any', minimumShouldMatch: 2 },
29
+ }));
30
+ expect(translated.conceptQueries).toHaveLength(3);
31
+ expect(translated.phraseQueries[0]?.[0]).toMatch(/^"[a-z0-9]+ [a-z0-9]+"$/);
32
+ expect(translated.minimumShouldMatch).toBe(2);
33
+ });
34
+ it('emits only phrase variants that mirror physical index streams', () => {
35
+ const phraseExpander = {
36
+ fingerprint: 'phrase-test1',
37
+ supports: (locale) => locale.startsWith('en'),
38
+ expand: (token) => token.value === 'running'
39
+ ? [{ kind: 'stem', value: 'run' }]
40
+ : token.value === 'restoration'
41
+ ? [{ kind: 'stem', value: 'restore' }]
42
+ : [],
43
+ };
44
+ const translated = buildPortableMySqlQuery(createPortableSearchAnalyzer({ expanders: [phraseExpander] }).analyzeQuery({
45
+ query: '"running restoration"',
46
+ locale: 'en',
47
+ }));
48
+ expect(translated.phraseQueries[0]).toEqual([
49
+ `"${encodeSqlToken({ kind: 'exact', value: 'running' })} ${encodeSqlToken({
50
+ kind: 'exact',
51
+ value: 'restoration',
52
+ })}"`,
53
+ `"${encodeSqlToken({ kind: 'stem', value: 'run' })} ${encodeSqlToken({
54
+ kind: 'stem',
55
+ value: 'restore',
56
+ })}"`,
57
+ ]);
58
+ });
59
+ it('returns an empty ranking query for punctuation-only input', () => {
60
+ const translated = buildPortableMySqlQuery(createPortableSearchAnalyzer().analyzeQuery({ query: '— -- !!!' }));
61
+ expect(translated).toMatchObject({
62
+ conceptQueries: [],
63
+ phraseQueries: [],
64
+ gramQueries: [],
65
+ rankingQuery: '',
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,41 @@
1
+ -- @byline/search-mysql — 0001_init
2
+ --
3
+ -- Disposable portable full-text index. One row per
4
+ -- (collection_path, document_id, locale). Every searchable logical token is
5
+ -- encoded by @byline/search-analysis before MySQL's parser sees it, avoiding
6
+ -- language-specific stopword and minimum-token divergence.
7
+
8
+ CREATE TABLE IF NOT EXISTS byline_search_documents (
9
+ collection_path varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
10
+ document_id varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
11
+ locale varchar(35) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
12
+ status varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
13
+ zones json NOT NULL,
14
+ title text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
15
+ path text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
16
+ body longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
17
+ search_text longtext CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
18
+ search_a longtext CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
19
+ search_b longtext CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
20
+ search_c longtext CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
21
+ search_d longtext CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
22
+ analyzer_fingerprint varchar(512) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
23
+ facets json NOT NULL,
24
+ filters json NOT NULL,
25
+ updated_at datetime(6) NOT NULL,
26
+ PRIMARY KEY (collection_path, document_id, locale),
27
+ KEY byline_search_documents_collection_idx (collection_path, status),
28
+ FULLTEXT KEY byline_search_documents_text_idx (search_text),
29
+ FULLTEXT KEY byline_search_documents_a_idx (search_a),
30
+ FULLTEXT KEY byline_search_documents_b_idx (search_b),
31
+ FULLTEXT KEY byline_search_documents_c_idx (search_c),
32
+ FULLTEXT KEY byline_search_documents_d_idx (search_d)
33
+ ) ENGINE=InnoDB;
34
+
35
+ CREATE TABLE IF NOT EXISTS byline_search_index_metadata (
36
+ collection_path varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
37
+ analyzer_fingerprint varchar(512) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
38
+ zones json NOT NULL,
39
+ updated_at datetime(6) NOT NULL,
40
+ PRIMARY KEY (collection_path)
41
+ ) ENGINE=InnoDB;
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@byline/search-mysql",
3
+ "private": false,
4
+ "license": "MPL-2.0",
5
+ "version": "4.9.0",
6
+ "engines": {
7
+ "node": ">=20.9.0"
8
+ },
9
+ "description": "Byline CMS built-in MySQL full-text search provider",
10
+ "keywords": [
11
+ "cms",
12
+ "headless cms",
13
+ "content management",
14
+ "search",
15
+ "mysql",
16
+ "full-text search"
17
+ ],
18
+ "homepage": "https://github.com/Byline-CMS/bylinecms.dev",
19
+ "bugs": {
20
+ "url": "https://github.com/Byline-CMS/bylinecms.dev/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Byline-CMS/bylinecms.dev.git",
25
+ "directory": "packages/search-mysql"
26
+ },
27
+ "type": "module",
28
+ "main": "dist/index.js",
29
+ "index": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "migrations"
42
+ ],
43
+ "dependencies": {
44
+ "@byline/core": "4.9.0",
45
+ "@byline/search-analysis": "4.9.0"
46
+ },
47
+ "peerDependencies": {
48
+ "mysql2": "^3.23.1"
49
+ },
50
+ "devDependencies": {
51
+ "@biomejs/biome": "2.5.4",
52
+ "@types/node": "^26.1.1",
53
+ "chokidar": "^5.0.0",
54
+ "chokidar-cli": "^3.0.0",
55
+ "dotenv": "^17.4.2",
56
+ "mysql2": "^3.23.1",
57
+ "npm-run-all": "^4.1.5",
58
+ "tsc-alias": "^1.9.1",
59
+ "tsx": "^4.23.1",
60
+ "typescript": "^7.0.2",
61
+ "vitest": "^4.1.10",
62
+ "@byline/search-conformance": "0.0.2"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "scripts": {
68
+ "dev": "chokidar 'src/**/*' -c 'npm-run-all build'",
69
+ "build": "tsc -p tsconfig.json && tsc-alias",
70
+ "clean": "node scripts/clean.js node_modules dist build .turbo",
71
+ "lint": "biome check --write --unsafe --diagnostic-level=error",
72
+ "test": "vitest run --mode=node",
73
+ "test:integration": "vitest run --mode=integration",
74
+ "test:watch": "vitest --mode=node",
75
+ "typecheck": "tsc --noEmit"
76
+ }
77
+ }