@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,77 @@
|
|
|
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
|
+
* Generated UNION ALL column projections for the EAV store tables.
|
|
9
|
+
*
|
|
10
|
+
* Instead of maintaining 7 hand-synchronized SQL fragments with 41
|
|
11
|
+
* positional columns each, we generate the SELECT list for each store
|
|
12
|
+
* table from a single column manifest. Adding a column or a new store
|
|
13
|
+
* table is a one-line change in the manifest.
|
|
14
|
+
*
|
|
15
|
+
* The manifest data (`storeColumnManifest`, `storeTableNames`) is dialect
|
|
16
|
+
* independent and lives in `@byline/core` — the same manifest
|
|
17
|
+
* `@byline/db-postgres` consumes. This module owns everything
|
|
18
|
+
* MySQL-specific: the generated `SQL` fragments, and `mysqlNullCast()`,
|
|
19
|
+
* which renders the manifest's abstract `nullCast` type names (`'uuid'`,
|
|
20
|
+
* `'boolean'`, …) as MySQL `CAST(NULL AS <type>)` expressions — MySQL has
|
|
21
|
+
* no `NULL::<type>` shorthand.
|
|
22
|
+
*
|
|
23
|
+
* The adapter-agnostic pieces (`StoreType`, `ALL_STORE_TYPES`,
|
|
24
|
+
* `fieldTypeToStore`, `fieldTypeToStoreType`) also live in `@byline/core`
|
|
25
|
+
* so `@byline/client` can consume the same mapping without taking a
|
|
26
|
+
* dependency on either adapter. Mirrors
|
|
27
|
+
* `packages/db-postgres/src/modules/storage/storage-store-manifest.ts`.
|
|
28
|
+
*/
|
|
29
|
+
import { type StoreType } from '@byline/core';
|
|
30
|
+
import { type SQL } from 'drizzle-orm';
|
|
31
|
+
export { ALL_STORE_TYPES, fieldTypeToStore, fieldTypeToStoreType, type StoreType, storeTableNames, } from '@byline/core';
|
|
32
|
+
/** Short-form alias used by adapter-internal call sites. */
|
|
33
|
+
export declare const allStoreTypes: readonly StoreType[];
|
|
34
|
+
/** The field_type literal emitted for each store table in the UNION ALL. */
|
|
35
|
+
declare const fieldTypeLiterals: Record<StoreType, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Map the manifest's abstract `nullCast` type name to its MySQL `CAST(NULL
|
|
38
|
+
* AS <type>)` expression. MySQL has no `NULL::<type>` shorthand (that's
|
|
39
|
+
* Postgres-only syntax), so every branch of the UNION ALL that doesn't
|
|
40
|
+
* provide a given column emits an explicit `CAST`.
|
|
41
|
+
*
|
|
42
|
+
* Each mapping was verified against the live MySQL 9.7.1 container (not
|
|
43
|
+
* just read from the docs) by UNIONing a `CAST(NULL AS …)` branch against a
|
|
44
|
+
* branch selecting a real, populated column of the corresponding store type
|
|
45
|
+
* and confirming the driver returns the real value unmodified (no
|
|
46
|
+
* truncation, no precision loss) — see the Task 10A report for the
|
|
47
|
+
* transcript. Precision-bearing casts (`TIME(3)`, `DATETIME(6)`,
|
|
48
|
+
* `DECIMAL(10,2)`) mirror the schema's own column precision
|
|
49
|
+
* (`packages/db-mysql/src/database/schema/index.ts`) exactly, rather than
|
|
50
|
+
* relying on MySQL's default (whole-second time, `DECIMAL(10,0)`), so a
|
|
51
|
+
* NULL-cast branch can never be the one that silently narrows the unified
|
|
52
|
+
* column's type. `DATETIME(6)` here specifically must track
|
|
53
|
+
* `value_timestamp_tz`'s own `fsp` (Task 11 moved it from 3 to 6, matching
|
|
54
|
+
* pg's microsecond precision — see `common.ts`'s `auditTimestamp` docblock)
|
|
55
|
+
* — re-verified live post-move via `storage-queries.test.ts`'s
|
|
56
|
+
* `datetime`-field round-trip test, which exercises exactly this branch
|
|
57
|
+
* (a text-typed sibling row's UNION leg lands on this `CAST`) and confirmed
|
|
58
|
+
* full microsecond fidelity survives.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mysqlNullCast(nullCast: string): string;
|
|
61
|
+
/** Number of columns in the unified output (base + field_type + type-specific). */
|
|
62
|
+
export declare const UNIFIED_COLUMN_COUNT: number;
|
|
63
|
+
/**
|
|
64
|
+
* Build the SELECT column list for a given store table type.
|
|
65
|
+
*
|
|
66
|
+
* Base columns (id, document_version_id, etc.) are passed through directly.
|
|
67
|
+
* The `field_type` column is emitted as a string literal.
|
|
68
|
+
* Type-specific columns are either mapped to the source expression or
|
|
69
|
+
* emitted as a typed NULL.
|
|
70
|
+
*/
|
|
71
|
+
declare function buildSelectList(storeType: StoreType): string;
|
|
72
|
+
/**
|
|
73
|
+
* Get the Drizzle SQL fragment for a store type's SELECT list.
|
|
74
|
+
* Results are cached — the generation runs once at module load.
|
|
75
|
+
*/
|
|
76
|
+
export declare function storeSelectList(storeType: StoreType): SQL;
|
|
77
|
+
export { buildSelectList, fieldTypeLiterals };
|
|
@@ -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
|
+
* Generated UNION ALL column projections for the EAV store tables.
|
|
9
|
+
*
|
|
10
|
+
* Instead of maintaining 7 hand-synchronized SQL fragments with 41
|
|
11
|
+
* positional columns each, we generate the SELECT list for each store
|
|
12
|
+
* table from a single column manifest. Adding a column or a new store
|
|
13
|
+
* table is a one-line change in the manifest.
|
|
14
|
+
*
|
|
15
|
+
* The manifest data (`storeColumnManifest`, `storeTableNames`) is dialect
|
|
16
|
+
* independent and lives in `@byline/core` — the same manifest
|
|
17
|
+
* `@byline/db-postgres` consumes. This module owns everything
|
|
18
|
+
* MySQL-specific: the generated `SQL` fragments, and `mysqlNullCast()`,
|
|
19
|
+
* which renders the manifest's abstract `nullCast` type names (`'uuid'`,
|
|
20
|
+
* `'boolean'`, …) as MySQL `CAST(NULL AS <type>)` expressions — MySQL has
|
|
21
|
+
* no `NULL::<type>` shorthand.
|
|
22
|
+
*
|
|
23
|
+
* The adapter-agnostic pieces (`StoreType`, `ALL_STORE_TYPES`,
|
|
24
|
+
* `fieldTypeToStore`, `fieldTypeToStoreType`) also live in `@byline/core`
|
|
25
|
+
* so `@byline/client` can consume the same mapping without taking a
|
|
26
|
+
* dependency on either adapter. Mirrors
|
|
27
|
+
* `packages/db-postgres/src/modules/storage/storage-store-manifest.ts`.
|
|
28
|
+
*/
|
|
29
|
+
import { ALL_STORE_TYPES, storeColumnManifest } from '@byline/core';
|
|
30
|
+
import { sql } from 'drizzle-orm';
|
|
31
|
+
// Re-export for adapter-internal consumers.
|
|
32
|
+
export { ALL_STORE_TYPES, fieldTypeToStore, fieldTypeToStoreType, storeTableNames, } from '@byline/core';
|
|
33
|
+
/** Short-form alias used by adapter-internal call sites. */
|
|
34
|
+
export const allStoreTypes = ALL_STORE_TYPES;
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// SQL generation
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/** The field_type literal emitted for each store table in the UNION ALL. */
|
|
39
|
+
const fieldTypeLiterals = {
|
|
40
|
+
text: 'text',
|
|
41
|
+
numeric: 'numeric',
|
|
42
|
+
boolean: 'boolean',
|
|
43
|
+
datetime: 'datetime',
|
|
44
|
+
json: 'richText',
|
|
45
|
+
relation: 'relation',
|
|
46
|
+
file: 'file',
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Map the manifest's abstract `nullCast` type name to its MySQL `CAST(NULL
|
|
50
|
+
* AS <type>)` expression. MySQL has no `NULL::<type>` shorthand (that's
|
|
51
|
+
* Postgres-only syntax), so every branch of the UNION ALL that doesn't
|
|
52
|
+
* provide a given column emits an explicit `CAST`.
|
|
53
|
+
*
|
|
54
|
+
* Each mapping was verified against the live MySQL 9.7.1 container (not
|
|
55
|
+
* just read from the docs) by UNIONing a `CAST(NULL AS …)` branch against a
|
|
56
|
+
* branch selecting a real, populated column of the corresponding store type
|
|
57
|
+
* and confirming the driver returns the real value unmodified (no
|
|
58
|
+
* truncation, no precision loss) — see the Task 10A report for the
|
|
59
|
+
* transcript. Precision-bearing casts (`TIME(3)`, `DATETIME(6)`,
|
|
60
|
+
* `DECIMAL(10,2)`) mirror the schema's own column precision
|
|
61
|
+
* (`packages/db-mysql/src/database/schema/index.ts`) exactly, rather than
|
|
62
|
+
* relying on MySQL's default (whole-second time, `DECIMAL(10,0)`), so a
|
|
63
|
+
* NULL-cast branch can never be the one that silently narrows the unified
|
|
64
|
+
* column's type. `DATETIME(6)` here specifically must track
|
|
65
|
+
* `value_timestamp_tz`'s own `fsp` (Task 11 moved it from 3 to 6, matching
|
|
66
|
+
* pg's microsecond precision — see `common.ts`'s `auditTimestamp` docblock)
|
|
67
|
+
* — re-verified live post-move via `storage-queries.test.ts`'s
|
|
68
|
+
* `datetime`-field round-trip test, which exercises exactly this branch
|
|
69
|
+
* (a text-typed sibling row's UNION leg lands on this `CAST`) and confirmed
|
|
70
|
+
* full microsecond fidelity survives.
|
|
71
|
+
*/
|
|
72
|
+
export function mysqlNullCast(nullCast) {
|
|
73
|
+
switch (nullCast) {
|
|
74
|
+
case 'uuid':
|
|
75
|
+
return 'CAST(NULL AS CHAR(36))';
|
|
76
|
+
case 'boolean':
|
|
77
|
+
return 'CAST(NULL AS SIGNED)';
|
|
78
|
+
case 'timestamp':
|
|
79
|
+
// The manifest's abstract name for the `timestamptz`-shaped column
|
|
80
|
+
// (`value_timestamp_tz`); MySQL's storage-side type is `DATETIME(6)`
|
|
81
|
+
// (matching pg's microsecond precision — see `common.ts`'s
|
|
82
|
+
// `auditTimestamp` docblock), not a MySQL `TIMESTAMP` column.
|
|
83
|
+
return 'CAST(NULL AS DATETIME(6))';
|
|
84
|
+
case 'date':
|
|
85
|
+
return 'CAST(NULL AS DATE)';
|
|
86
|
+
case 'time':
|
|
87
|
+
return 'CAST(NULL AS TIME(3))';
|
|
88
|
+
case 'jsonb':
|
|
89
|
+
return 'CAST(NULL AS JSON)';
|
|
90
|
+
case 'text':
|
|
91
|
+
case 'varchar':
|
|
92
|
+
return 'CAST(NULL AS CHAR)';
|
|
93
|
+
case 'bigint':
|
|
94
|
+
case 'integer':
|
|
95
|
+
return 'CAST(NULL AS SIGNED)';
|
|
96
|
+
case 'decimal':
|
|
97
|
+
return 'CAST(NULL AS DECIMAL(10,2))';
|
|
98
|
+
case 'real':
|
|
99
|
+
return 'CAST(NULL AS FLOAT)';
|
|
100
|
+
case 'text[]':
|
|
101
|
+
// No MySQL array type — `object_keys` is stored as a JSON array of
|
|
102
|
+
// strings on this adapter (see the manifest's own comment at its
|
|
103
|
+
// `object_keys` entry), so the null cast matches that column's real
|
|
104
|
+
// MySQL type.
|
|
105
|
+
return 'CAST(NULL AS JSON)';
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`mysqlNullCast: unrecognised nullCast type '${nullCast}'`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Number of columns in the unified output (base + field_type + type-specific). */
|
|
111
|
+
export const UNIFIED_COLUMN_COUNT = storeColumnManifest.length + 1; // +1 for field_type
|
|
112
|
+
/**
|
|
113
|
+
* Build the SELECT column list for a given store table type.
|
|
114
|
+
*
|
|
115
|
+
* Base columns (id, document_version_id, etc.) are passed through directly.
|
|
116
|
+
* The `field_type` column is emitted as a string literal.
|
|
117
|
+
* Type-specific columns are either mapped to the source expression or
|
|
118
|
+
* emitted as a typed NULL.
|
|
119
|
+
*/
|
|
120
|
+
function buildSelectList(storeType) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
for (const col of storeColumnManifest) {
|
|
123
|
+
// Base columns (no sources) are always passed through from the table.
|
|
124
|
+
if (!col.sources) {
|
|
125
|
+
parts.push(col.name);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const sourceExpr = col.sources[storeType];
|
|
129
|
+
if (sourceExpr) {
|
|
130
|
+
// This store provides this column — use the source expression.
|
|
131
|
+
// If the source column name differs from the output alias, add AS.
|
|
132
|
+
if (sourceExpr === col.name) {
|
|
133
|
+
parts.push(col.name);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
parts.push(`${sourceExpr} as \`${col.name}\``);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// This store doesn't provide this column — emit typed NULL.
|
|
141
|
+
parts.push(`${mysqlNullCast(col.nullCast)} as \`${col.name}\``);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Insert field_type after the base columns. The base columns are the first
|
|
145
|
+
// 7 entries (id through parent_path). field_type goes at position 3
|
|
146
|
+
// (after collection_id, before field_path) to match the original layout.
|
|
147
|
+
const fieldTypeLiteral = `'${fieldTypeLiterals[storeType]}' as \`field_type\``;
|
|
148
|
+
parts.splice(3, 0, fieldTypeLiteral);
|
|
149
|
+
return parts.join(',\n ');
|
|
150
|
+
}
|
|
151
|
+
// Pre-generate SQL fragments for each store type.
|
|
152
|
+
const selectListCache = new Map();
|
|
153
|
+
/**
|
|
154
|
+
* Get the Drizzle SQL fragment for a store type's SELECT list.
|
|
155
|
+
* Results are cached — the generation runs once at module load.
|
|
156
|
+
*/
|
|
157
|
+
export function storeSelectList(storeType) {
|
|
158
|
+
let cached = selectListCache.get(storeType);
|
|
159
|
+
if (!cached) {
|
|
160
|
+
cached = sql.raw(buildSelectList(storeType));
|
|
161
|
+
selectListCache.set(storeType, cached);
|
|
162
|
+
}
|
|
163
|
+
return cached;
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Exported for testing
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
export { buildSelectList, fieldTypeLiterals };
|
|
@@ -0,0 +1,49 @@
|
|
|
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 declare const getFirstOrThrow: <T>(message: string) => (values: T[]) => T;
|
|
9
|
+
/**
|
|
10
|
+
* Coerce a raw driver `DATETIME`/`TIMESTAMP` value from a `db.execute(sql\`...\`)`
|
|
11
|
+
* call to a real `Date`. `drizzle-orm`'s mysql2 driver installs its own
|
|
12
|
+
* `typeCast` on every raw-execute call that has no schema-typed `fields`
|
|
13
|
+
* mapper — which every hand-written SQL read in this adapter is — that
|
|
14
|
+
* unconditionally returns those column types as **strings**
|
|
15
|
+
* (`'YYYY-MM-DD HH:MM:SS.ffffff'`, space-separated, no timezone marker),
|
|
16
|
+
* regardless of the pool's own `timezone` option. Confirmed live across
|
|
17
|
+
* three independent call sites in this adapter (Task 11's audit-log UNION,
|
|
18
|
+
* `normalizeRow`'s `value_timestamp_tz`, and `findDocuments`' main query —
|
|
19
|
+
* see the Task 11 report). Every `DATETIME`/`TIMESTAMP` column in this
|
|
20
|
+
* schema is UTC by convention, so appending `Z` after normalising the
|
|
21
|
+
* separator is the correct interpretation, not an assumed one. Tolerant of
|
|
22
|
+
* `null`/`undefined` and of the value already being a `Date` (defensive —
|
|
23
|
+
* not expected on this path, but cheap to allow and keeps callers simple).
|
|
24
|
+
*
|
|
25
|
+
* This is the sole gate between the raw driver row and three different
|
|
26
|
+
* public contract shapes (`normalizeRow`'s `value_timestamp_tz`,
|
|
27
|
+
* `findDocuments`'s `created_at`/`updated_at`, and the audit log's
|
|
28
|
+
* `occurredAt`), so a malformed value must not fall through as a silent
|
|
29
|
+
* `Invalid Date` — it throws instead (task 13b §M2; see
|
|
30
|
+
* `packages/db-postgres/src/modules/storage/normalize-row.ts`'s `toDate`
|
|
31
|
+
* for the pg-side equivalent of this guard). `context` is optional and
|
|
32
|
+
* purely cosmetic — it names which column the caller is coercing in the
|
|
33
|
+
* thrown message; omitted call sites still get a safe, if less specific,
|
|
34
|
+
* error instead of a silently wrong `Date`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toDate(value: string | Date | null | undefined, context?: string): Date | null;
|
|
37
|
+
/**
|
|
38
|
+
* Read the affected-row count off a drizzle mysql2 `update()`/`delete()`
|
|
39
|
+
* result. MySQL has no `RETURNING`, so every guarded write in this adapter
|
|
40
|
+
* (optimistic-concurrency `UPDATE`/`DELETE` gates, bulk-mutation counts)
|
|
41
|
+
* uses this as its accept/reject or count signal instead. mysql2 resolves
|
|
42
|
+
* these to a `[ResultSetHeader, FieldPacket[]]` tuple — `affectedRows`
|
|
43
|
+
* lives on the first element, not `.rowCount` the way pg's driver shapes
|
|
44
|
+
* it (confirmed live against the test database; see
|
|
45
|
+
* `storage-commands.ts`'s `archivePublishedVersions` docblock for the
|
|
46
|
+
* original finding). Centralised here so the admin repositories (`../admin/
|
|
47
|
+
* *.ts`) share one cast instead of repeating it inline at every call site.
|
|
48
|
+
*/
|
|
49
|
+
export declare function affectedRowCount(result: unknown): number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { ERR_DATABASE, getLogger } from '@byline/core';
|
|
9
|
+
// ------------------------------------------------------------------------------
|
|
10
|
+
// Misc
|
|
11
|
+
//
|
|
12
|
+
// `resolveStoreTypes` lives in `@byline/core` (`packages/core/src/storage/
|
|
13
|
+
// storage-utils.ts`) — it is dialect-independent. `getFirstOrThrow` stays
|
|
14
|
+
// here: it's a result helper tied to this adapter. Mirrors
|
|
15
|
+
// `packages/db-postgres/src/modules/storage/storage-utils.ts`.
|
|
16
|
+
// ------------------------------------------------------------------------------
|
|
17
|
+
export const getFirstOrThrow = (message) => (values) => {
|
|
18
|
+
const value = values[0];
|
|
19
|
+
if (value == null) {
|
|
20
|
+
throw ERR_DATABASE({ message }).log(getLogger());
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Coerce a raw driver `DATETIME`/`TIMESTAMP` value from a `db.execute(sql\`...\`)`
|
|
26
|
+
* call to a real `Date`. `drizzle-orm`'s mysql2 driver installs its own
|
|
27
|
+
* `typeCast` on every raw-execute call that has no schema-typed `fields`
|
|
28
|
+
* mapper — which every hand-written SQL read in this adapter is — that
|
|
29
|
+
* unconditionally returns those column types as **strings**
|
|
30
|
+
* (`'YYYY-MM-DD HH:MM:SS.ffffff'`, space-separated, no timezone marker),
|
|
31
|
+
* regardless of the pool's own `timezone` option. Confirmed live across
|
|
32
|
+
* three independent call sites in this adapter (Task 11's audit-log UNION,
|
|
33
|
+
* `normalizeRow`'s `value_timestamp_tz`, and `findDocuments`' main query —
|
|
34
|
+
* see the Task 11 report). Every `DATETIME`/`TIMESTAMP` column in this
|
|
35
|
+
* schema is UTC by convention, so appending `Z` after normalising the
|
|
36
|
+
* separator is the correct interpretation, not an assumed one. Tolerant of
|
|
37
|
+
* `null`/`undefined` and of the value already being a `Date` (defensive —
|
|
38
|
+
* not expected on this path, but cheap to allow and keeps callers simple).
|
|
39
|
+
*
|
|
40
|
+
* This is the sole gate between the raw driver row and three different
|
|
41
|
+
* public contract shapes (`normalizeRow`'s `value_timestamp_tz`,
|
|
42
|
+
* `findDocuments`'s `created_at`/`updated_at`, and the audit log's
|
|
43
|
+
* `occurredAt`), so a malformed value must not fall through as a silent
|
|
44
|
+
* `Invalid Date` — it throws instead (task 13b §M2; see
|
|
45
|
+
* `packages/db-postgres/src/modules/storage/normalize-row.ts`'s `toDate`
|
|
46
|
+
* for the pg-side equivalent of this guard). `context` is optional and
|
|
47
|
+
* purely cosmetic — it names which column the caller is coercing in the
|
|
48
|
+
* thrown message; omitted call sites still get a safe, if less specific,
|
|
49
|
+
* error instead of a silently wrong `Date`.
|
|
50
|
+
*/
|
|
51
|
+
export function toDate(value, context) {
|
|
52
|
+
if (value == null)
|
|
53
|
+
return null;
|
|
54
|
+
if (value instanceof Date)
|
|
55
|
+
return value;
|
|
56
|
+
const date = new Date(`${value.replace(' ', 'T')}Z`);
|
|
57
|
+
if (Number.isNaN(date.getTime())) {
|
|
58
|
+
throw new Error(`toDate: ${context ?? 'value'} is not a parseable timestamp — got ${JSON.stringify(value)}`);
|
|
59
|
+
}
|
|
60
|
+
return date;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read the affected-row count off a drizzle mysql2 `update()`/`delete()`
|
|
64
|
+
* result. MySQL has no `RETURNING`, so every guarded write in this adapter
|
|
65
|
+
* (optimistic-concurrency `UPDATE`/`DELETE` gates, bulk-mutation counts)
|
|
66
|
+
* uses this as its accept/reject or count signal instead. mysql2 resolves
|
|
67
|
+
* these to a `[ResultSetHeader, FieldPacket[]]` tuple — `affectedRows`
|
|
68
|
+
* lives on the first element, not `.rowCount` the way pg's driver shapes
|
|
69
|
+
* it (confirmed live against the test database; see
|
|
70
|
+
* `storage-commands.ts`'s `archivePublishedVersions` docblock for the
|
|
71
|
+
* original finding). Centralised here so the admin repositories (`../admin/
|
|
72
|
+
* *.ts`) share one cast instead of repeating it inline at every call site.
|
|
73
|
+
*/
|
|
74
|
+
export function affectedRowCount(result) {
|
|
75
|
+
return result[0]?.affectedRows ?? 0;
|
|
76
|
+
}
|
|
@@ -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,266 @@
|
|
|
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
|
+
* MySQL-specific dialect properties the shared `@byline/db-conformance`
|
|
10
|
+
* suite cannot express, because they pin behaviour the Postgres adapter
|
|
11
|
+
* either doesn't have (collation choices) or expresses differently (its own
|
|
12
|
+
* `COLLATE "C"` / native `numeric` type). Each pin below exists because a
|
|
13
|
+
* real, live-server-verified property depends on a specific schema choice
|
|
14
|
+
* in `packages/db-mysql/src/database/schema/{index,common}.ts` — see each
|
|
15
|
+
* test's comment for the column/config it pins and why.
|
|
16
|
+
*/
|
|
17
|
+
import { generateKeyBetween, generateNKeysBetween } from '@byline/core';
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
19
|
+
import { setupTestDB, teardownTestDB } from '../../../lib/test-helper.js';
|
|
20
|
+
const timestamp = Date.now();
|
|
21
|
+
function first(rows) {
|
|
22
|
+
const row = rows[0];
|
|
23
|
+
if (row == null)
|
|
24
|
+
throw new Error('expected at least one row, got none');
|
|
25
|
+
return row;
|
|
26
|
+
}
|
|
27
|
+
async function queryRows(pool, sql, params) {
|
|
28
|
+
const [rows] = await pool.query(sql, params);
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
describe('MySQL dialect pins (live database)', () => {
|
|
32
|
+
let testDb;
|
|
33
|
+
let rawPool;
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
testDb = setupTestDB();
|
|
36
|
+
rawPool = testDb.pool;
|
|
37
|
+
});
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
await teardownTestDB();
|
|
40
|
+
});
|
|
41
|
+
describe('order_key sort parity', () => {
|
|
42
|
+
// `byline_documents.order_key` is `varcharByteSorted` (`ascii_bin`)
|
|
43
|
+
// specifically so `ORDER BY order_key` in SQL agrees with plain
|
|
44
|
+
// JavaScript string comparison — the fractional-index algorithm in
|
|
45
|
+
// `@byline/core` (`generateKeyBetween`) is designed against byte-wise
|
|
46
|
+
// ordering, and MySQL's database-default collation
|
|
47
|
+
// (`utf8mb4_0900_ai_ci`) does not agree with JS on every key (e.g.
|
|
48
|
+
// `'Zz'` vs `'a0'` sort oppositely under the two collations). This test
|
|
49
|
+
// builds a realistic, mixed-structure key spread — not just a single
|
|
50
|
+
// initial batch, but keys inserted "between" existing ones the way a
|
|
51
|
+
// real drag-and-drop reorder would generate them — shuffles the insert
|
|
52
|
+
// order, and asserts the database's own `ORDER BY` agrees with the JS
|
|
53
|
+
// sort of the exact same key strings.
|
|
54
|
+
it('DB ORDER BY order_key matches JS string sort over the generateKeyBetween alphabet', async () => {
|
|
55
|
+
const testCollection = {
|
|
56
|
+
path: `order-key-pin-${timestamp}`,
|
|
57
|
+
labels: { singular: 'OrderKeyThing', plural: 'OrderKeyThings' },
|
|
58
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
59
|
+
};
|
|
60
|
+
const collectionId = first(await testDb.commandBuilders.collections.create(testCollection.path, testCollection)).id;
|
|
61
|
+
// An initial batch of 8 keys (generateNKeysBetween bisects
|
|
62
|
+
// recursively, so this alone already mixes integer-part lengths and
|
|
63
|
+
// fraction lengths — a richer structure than a flat sequential
|
|
64
|
+
// batch), then interleave 7 more by generating a key strictly
|
|
65
|
+
// between each already-adjacent pair, the shape a real "drop between
|
|
66
|
+
// these two rows" reorder produces.
|
|
67
|
+
const initial = generateNKeysBetween(null, null, 8);
|
|
68
|
+
const interleaved = initial.slice(0, -1).map((key, i) => {
|
|
69
|
+
const next = initial[i + 1];
|
|
70
|
+
return generateKeyBetween(key, next);
|
|
71
|
+
});
|
|
72
|
+
const cleanKeys = [...initial, ...interleaved];
|
|
73
|
+
// Create one document per key, in shuffled order, so insertion order
|
|
74
|
+
// carries no information about sort order.
|
|
75
|
+
const shuffled = [...cleanKeys];
|
|
76
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
77
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
78
|
+
const tmp = shuffled[i];
|
|
79
|
+
shuffled[i] = shuffled[j];
|
|
80
|
+
shuffled[j] = tmp;
|
|
81
|
+
}
|
|
82
|
+
for (const key of shuffled) {
|
|
83
|
+
const created = await testDb.commandBuilders.documents.createDocumentVersion({
|
|
84
|
+
collectionId,
|
|
85
|
+
collectionVersion: 1,
|
|
86
|
+
collectionConfig: testCollection,
|
|
87
|
+
action: 'create',
|
|
88
|
+
documentData: { title: key },
|
|
89
|
+
locale: 'all',
|
|
90
|
+
status: 'draft',
|
|
91
|
+
});
|
|
92
|
+
await testDb.commandBuilders.documents.setOrderKey({
|
|
93
|
+
document_id: created.document.document_id,
|
|
94
|
+
order_key: key,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const rows = await queryRows(rawPool, 'SELECT id, order_key FROM byline_documents WHERE collection_id = ? ORDER BY order_key ASC', [collectionId]);
|
|
98
|
+
const dbOrderedKeys = rows.map((r) => r.order_key);
|
|
99
|
+
const jsSortedKeys = [...cleanKeys].sort();
|
|
100
|
+
expect(dbOrderedKeys).toEqual(jsSortedKeys);
|
|
101
|
+
await testDb.commandBuilders.collections.delete(collectionId);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('ascii_bin id equality', () => {
|
|
105
|
+
// Every id/FK column uses `uuidChar` — `CHAR(36) CHARACTER SET ascii
|
|
106
|
+
// COLLATE ascii_bin` — precisely so id comparisons are byte-wise
|
|
107
|
+
// (case-sensitive), the MySQL analogue of Postgres's native `uuid`
|
|
108
|
+
// type (which has no notion of a case-insensitive collation at all).
|
|
109
|
+
// MySQL's database-default collation (`utf8mb4_0900_ai_ci`) folds
|
|
110
|
+
// case, so without `ascii_bin` a case-flipped id would wrongly compare
|
|
111
|
+
// equal.
|
|
112
|
+
it('a CHAR(36) id differing only in case does not match', async () => {
|
|
113
|
+
const testCollection = {
|
|
114
|
+
path: `ascii-bin-pin-${timestamp}`,
|
|
115
|
+
labels: { singular: 'AsciiBinThing', plural: 'AsciiBinThings' },
|
|
116
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
117
|
+
};
|
|
118
|
+
const collectionId = first(await testDb.commandBuilders.collections.create(testCollection.path, testCollection)).id;
|
|
119
|
+
const created = await testDb.commandBuilders.documents.createDocumentVersion({
|
|
120
|
+
collectionId,
|
|
121
|
+
collectionVersion: 1,
|
|
122
|
+
collectionConfig: testCollection,
|
|
123
|
+
action: 'create',
|
|
124
|
+
documentData: { title: 'Ascii Bin' },
|
|
125
|
+
locale: 'all',
|
|
126
|
+
status: 'draft',
|
|
127
|
+
});
|
|
128
|
+
const documentId = created.document.document_id;
|
|
129
|
+
expect(documentId).not.toBe(documentId.toUpperCase());
|
|
130
|
+
const exactMatch = await queryRows(rawPool, 'SELECT id FROM byline_documents WHERE id = ?', [
|
|
131
|
+
documentId,
|
|
132
|
+
]);
|
|
133
|
+
expect(exactMatch).toHaveLength(1);
|
|
134
|
+
const caseFlippedMatch = await queryRows(rawPool, 'SELECT id FROM byline_documents WHERE id = ?', [documentId.toUpperCase()]);
|
|
135
|
+
expect(caseFlippedMatch).toHaveLength(0);
|
|
136
|
+
await testDb.commandBuilders.collections.delete(collectionId);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('DATETIME(6) UTC round-trip', () => {
|
|
140
|
+
// The mysql2 pool opens with `timezone: 'Z'` (see `src/index.ts` and
|
|
141
|
+
// `test-helper.ts`) so `Date` values are sent and received as UTC,
|
|
142
|
+
// without local-timezone reinterpretation at the connection layer —
|
|
143
|
+
// MySQL's `DATETIME` has no timezone-aware storage the way Postgres's
|
|
144
|
+
// `TIMESTAMPTZ` does, so this pool option is the entire mechanism.
|
|
145
|
+
// `fsp: 6` on `value_timestamp_tz` (`common.ts`'s `auditTimestamp`
|
|
146
|
+
// discipline, mirrored on the store table) matches pg's `TIMESTAMPTZ(6)`
|
|
147
|
+
// resolution.
|
|
148
|
+
it('writes a Date and reads back the identical instant', async () => {
|
|
149
|
+
const testCollection = {
|
|
150
|
+
path: `datetime-utc-pin-${timestamp}`,
|
|
151
|
+
labels: { singular: 'DatetimeUtcThing', plural: 'DatetimeUtcThings' },
|
|
152
|
+
fields: [
|
|
153
|
+
{ name: 'title', type: 'text' },
|
|
154
|
+
{ name: 'atTime', type: 'datetime' },
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
// `getDocumentById` resolves the collection definition from the set
|
|
158
|
+
// `queryBuilders` was constructed with — re-run `setupTestDB` with
|
|
159
|
+
// this test's collection so that lookup succeeds (mirrors the
|
|
160
|
+
// "Recreate queryBuilders when collections are provided" note in
|
|
161
|
+
// `test-helper.ts`).
|
|
162
|
+
testDb = setupTestDB([testCollection]);
|
|
163
|
+
const collectionId = first(await testDb.commandBuilders.collections.create(testCollection.path, testCollection)).id;
|
|
164
|
+
const expected = new Date('2026-05-04T03:02:01.456Z');
|
|
165
|
+
const created = await testDb.commandBuilders.documents.createDocumentVersion({
|
|
166
|
+
collectionId,
|
|
167
|
+
collectionVersion: 1,
|
|
168
|
+
collectionConfig: testCollection,
|
|
169
|
+
action: 'create',
|
|
170
|
+
documentData: { title: 'UTC round-trip', atTime: expected },
|
|
171
|
+
locale: 'all',
|
|
172
|
+
status: 'draft',
|
|
173
|
+
});
|
|
174
|
+
const doc = await testDb.queryBuilders.documents.getDocumentById({
|
|
175
|
+
collection_id: collectionId,
|
|
176
|
+
document_id: created.document.document_id,
|
|
177
|
+
locale: 'all',
|
|
178
|
+
});
|
|
179
|
+
const value = doc?.fields?.atTime;
|
|
180
|
+
expect(value).toBeInstanceOf(Date);
|
|
181
|
+
expect(value.getTime()).toBe(expected.getTime());
|
|
182
|
+
await testDb.commandBuilders.collections.delete(collectionId);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('DECIMAL precision preservation', () => {
|
|
186
|
+
// The pool opens with `decimalNumbers: false` (`src/index.ts`,
|
|
187
|
+
// `test-helper.ts`) so `DECIMAL` columns arrive as strings rather than
|
|
188
|
+
// JS `number`s — MySQL/JS float coercion would silently lose precision
|
|
189
|
+
// on money/decimal values otherwise, matching pg's `numeric` handling
|
|
190
|
+
// (node-postgres also returns `numeric` as a string by default).
|
|
191
|
+
it('returns a decimal field value as a string, not a JS number', async () => {
|
|
192
|
+
const testCollection = {
|
|
193
|
+
path: `decimal-pin-${timestamp}`,
|
|
194
|
+
labels: { singular: 'DecimalThing', plural: 'DecimalThings' },
|
|
195
|
+
fields: [
|
|
196
|
+
{ name: 'title', type: 'text' },
|
|
197
|
+
{ name: 'price', type: 'decimal' },
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
testDb = setupTestDB([testCollection]);
|
|
201
|
+
const collectionId = first(await testDb.commandBuilders.collections.create(testCollection.path, testCollection)).id;
|
|
202
|
+
const created = await testDb.commandBuilders.documents.createDocumentVersion({
|
|
203
|
+
collectionId,
|
|
204
|
+
collectionVersion: 1,
|
|
205
|
+
collectionConfig: testCollection,
|
|
206
|
+
action: 'create',
|
|
207
|
+
documentData: { title: 'Decimal', price: '19.99' },
|
|
208
|
+
locale: 'all',
|
|
209
|
+
status: 'draft',
|
|
210
|
+
});
|
|
211
|
+
const doc = await testDb.queryBuilders.documents.getDocumentById({
|
|
212
|
+
collection_id: collectionId,
|
|
213
|
+
document_id: created.document.document_id,
|
|
214
|
+
locale: 'all',
|
|
215
|
+
});
|
|
216
|
+
expect(typeof doc?.fields?.price).toBe('string');
|
|
217
|
+
expect(doc?.fields?.price).toBe('19.99');
|
|
218
|
+
await testDb.commandBuilders.collections.delete(collectionId);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('LIKE case- and accent-insensitivity (elected divergence)', () => {
|
|
222
|
+
// The store `value` text columns keep the database's default
|
|
223
|
+
// `utf8mb4_0900_ai_ci` collation deliberately — unlike `order_key` /
|
|
224
|
+
// id columns, which are pinned to byte-wise collations for structural
|
|
225
|
+
// reasons, the *content* columns are left case- and accent-folding on
|
|
226
|
+
// purpose, so admin search (`LIKE`) matches more broadly than
|
|
227
|
+
// Postgres's `ILIKE` (case-only). This is an elected, spec-level
|
|
228
|
+
// divergence, not an oversight — do not "fix" it by pinning `value` to
|
|
229
|
+
// a binary collation. See `findDocuments`' query-search site and
|
|
230
|
+
// `packages/db-mysql/src/modules/storage/tests/storage-queries.test.ts`'s
|
|
231
|
+
// "query (LIKE admin search)" test for the full end-to-end behavioural
|
|
232
|
+
// pin through `findDocuments`.
|
|
233
|
+
//
|
|
234
|
+
// This test reads a real row through `byline_store_text.value` rather
|
|
235
|
+
// than comparing two SQL string literals — an earlier version of this
|
|
236
|
+
// test ran `SELECT ('Ünïcödé' LIKE ?) as matched`, which pins the
|
|
237
|
+
// *connection's* default collation (`collation_connection`, derived
|
|
238
|
+
// from the pool's `charset`), not the column's. Re-collating `value`
|
|
239
|
+
// to a binary collation would leave that version green while breaking
|
|
240
|
+
// the exact behaviour it claimed to pin — this version would catch it,
|
|
241
|
+
// because the comparison now happens against the actual indexed
|
|
242
|
+
// column.
|
|
243
|
+
it('LIKE matches a stored value in byline_store_text across case and accent variants', async () => {
|
|
244
|
+
const testCollection = {
|
|
245
|
+
path: `like-collation-pin-${timestamp}`,
|
|
246
|
+
labels: { singular: 'LikeCollationThing', plural: 'LikeCollationThings' },
|
|
247
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
248
|
+
};
|
|
249
|
+
testDb = setupTestDB([testCollection]);
|
|
250
|
+
const collectionId = first(await testDb.commandBuilders.collections.create(testCollection.path, testCollection)).id;
|
|
251
|
+
await testDb.commandBuilders.documents.createDocumentVersion({
|
|
252
|
+
collectionId,
|
|
253
|
+
collectionVersion: 1,
|
|
254
|
+
collectionConfig: testCollection,
|
|
255
|
+
action: 'create',
|
|
256
|
+
documentData: { title: 'Ünïcödé Sample' },
|
|
257
|
+
locale: 'all',
|
|
258
|
+
status: 'draft',
|
|
259
|
+
});
|
|
260
|
+
const rows = await queryRows(rawPool, 'SELECT value FROM byline_store_text WHERE field_name = ? AND value LIKE ?', ['title', '%unicode sample%']);
|
|
261
|
+
expect(rows).toHaveLength(1);
|
|
262
|
+
expect(first(rows).value).toBe('Ünïcödé Sample');
|
|
263
|
+
await testDb.commandBuilders.collections.delete(collectionId);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -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 {};
|