@byline/db-mysql 6.11.0 → 6.12.1
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/dist/database/schema/index.d.ts +34 -0
- package/dist/database/schema/index.js +5 -1
- package/dist/index.js +3 -0
- package/dist/modules/storage/storage-insert.js +2 -0
- package/dist/modules/storage/storage-queries.js +2 -0
- package/dist/modules/storage/stored-files.d.ts +27 -0
- package/dist/modules/storage/stored-files.js +111 -0
- package/package.json +11 -9
|
@@ -3163,6 +3163,40 @@ export declare const fileStore: import("drizzle-orm/mysql-core").MySqlTableWithC
|
|
|
3163
3163
|
identity: undefined;
|
|
3164
3164
|
generated: undefined;
|
|
3165
3165
|
}, {}, {}>;
|
|
3166
|
+
storage_validator_kind: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
3167
|
+
name: "storage_validator_kind";
|
|
3168
|
+
tableName: "byline_store_file";
|
|
3169
|
+
dataType: "string";
|
|
3170
|
+
columnType: "MySqlVarChar";
|
|
3171
|
+
data: string;
|
|
3172
|
+
driverParam: string | number;
|
|
3173
|
+
notNull: false;
|
|
3174
|
+
hasDefault: false;
|
|
3175
|
+
isPrimaryKey: false;
|
|
3176
|
+
isAutoincrement: false;
|
|
3177
|
+
hasRuntimeDefault: false;
|
|
3178
|
+
enumValues: [string, ...string[]];
|
|
3179
|
+
baseColumn: never;
|
|
3180
|
+
identity: undefined;
|
|
3181
|
+
generated: undefined;
|
|
3182
|
+
}, {}, {}>;
|
|
3183
|
+
storage_validator_value: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
3184
|
+
name: "storage_validator_value";
|
|
3185
|
+
tableName: "byline_store_file";
|
|
3186
|
+
dataType: "string";
|
|
3187
|
+
columnType: "MySqlVarChar";
|
|
3188
|
+
data: string;
|
|
3189
|
+
driverParam: string | number;
|
|
3190
|
+
notNull: false;
|
|
3191
|
+
hasDefault: false;
|
|
3192
|
+
isPrimaryKey: false;
|
|
3193
|
+
isAutoincrement: false;
|
|
3194
|
+
hasRuntimeDefault: false;
|
|
3195
|
+
enumValues: [string, ...string[]];
|
|
3196
|
+
baseColumn: never;
|
|
3197
|
+
identity: undefined;
|
|
3198
|
+
generated: undefined;
|
|
3199
|
+
}, {}, {}>;
|
|
3166
3200
|
storage_provider: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
3167
3201
|
name: "storage_provider";
|
|
3168
3202
|
tableName: "byline_store_file";
|
|
@@ -678,7 +678,11 @@ export const fileStore = mysqlTable('byline_store_file', {
|
|
|
678
678
|
// File metadata
|
|
679
679
|
mime_type: varchar('mime_type', { length: 100 }).notNull(),
|
|
680
680
|
file_size: bigint('file_size', { mode: 'number' }).notNull(), // Size in bytes
|
|
681
|
-
file_hash: varchar('file_hash', { length: 64 }), // SHA-256 hash
|
|
681
|
+
file_hash: varchar('file_hash', { length: 64 }), // SHA-256 content hash (lower-case hex)
|
|
682
|
+
// Storage validator of the stored version that file_hash was computed
|
|
683
|
+
// from (S3 version id / ETag, local stat). Written and read as a pair.
|
|
684
|
+
storage_validator_kind: varchar('storage_validator_kind', { length: 32 }),
|
|
685
|
+
storage_validator_value: varchar('storage_validator_value', { length: 1024 }),
|
|
682
686
|
// Storage information
|
|
683
687
|
storage_provider: varchar('storage_provider', { length: 50 }).notNull(), // 'local', 's3', 'gcs', etc.
|
|
684
688
|
storage_path: text('storage_path').notNull(),
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createReadSnapshot } from './modules/storage/read-snapshot.js';
|
|
|
20
20
|
import { SingletonCommands, SingletonQueries } from './modules/storage/singletons.js';
|
|
21
21
|
import { createCommandBuilders } from './modules/storage/storage-commands.js';
|
|
22
22
|
import { createQueryBuilders } from './modules/storage/storage-queries.js';
|
|
23
|
+
import { StoredFileCommands, StoredFileQueries } from './modules/storage/stored-files.js';
|
|
23
24
|
export const mysqlAdapter = ({ connectionString, collections, defaultContentLocale, connectionLimit = 20, idleTimeout = 2000, connectTimeout = 30000, maxIdle, }) => {
|
|
24
25
|
const pool = mysql.createPool({
|
|
25
26
|
uri: connectionString,
|
|
@@ -139,6 +140,7 @@ export const mysqlAdapter = ({ connectionString, collections, defaultContentLoca
|
|
|
139
140
|
// see `./modules/audit/audit-commands.js`.
|
|
140
141
|
audit: auditCommands,
|
|
141
142
|
singletons: singletonCommands,
|
|
143
|
+
storedFiles: new StoredFileCommands(dbManager),
|
|
142
144
|
},
|
|
143
145
|
queries: {
|
|
144
146
|
// `queryBuilders.collections` fully implements `ICollectionQueries`
|
|
@@ -151,6 +153,7 @@ export const mysqlAdapter = ({ connectionString, collections, defaultContentLoca
|
|
|
151
153
|
// `./modules/audit/audit-queries.js`.
|
|
152
154
|
audit: auditQueries,
|
|
153
155
|
singletons: singletonQueries,
|
|
156
|
+
storedFiles: new StoredFileQueries(dbManager),
|
|
154
157
|
},
|
|
155
158
|
withTransaction: (fn) => txManager.withTransaction(fn),
|
|
156
159
|
withReadSnapshot: createReadSnapshot(db, collections, defaultContentLocale),
|
|
@@ -102,6 +102,8 @@ export const prepareFieldInsertBuckets = (flattenedFields, document_version_id,
|
|
|
102
102
|
storage_path: field.storage_path,
|
|
103
103
|
storage_url: field.storage_url,
|
|
104
104
|
file_hash: field.file_hash,
|
|
105
|
+
storage_validator_kind: field.storage_validator_kind,
|
|
106
|
+
storage_validator_value: field.storage_validator_value,
|
|
105
107
|
image_width: field.image_width,
|
|
106
108
|
image_height: field.image_height,
|
|
107
109
|
image_format: field.image_format,
|
|
@@ -2056,6 +2056,8 @@ export class DocumentQueries {
|
|
|
2056
2056
|
storage_path: row.storage_path,
|
|
2057
2057
|
storage_url: row.storage_url,
|
|
2058
2058
|
file_hash: row.file_hash,
|
|
2059
|
+
storage_validator_kind: row.storage_validator_kind,
|
|
2060
|
+
storage_validator_value: row.storage_validator_value,
|
|
2059
2061
|
image_width: row.image_width,
|
|
2060
2062
|
image_height: row.image_height,
|
|
2061
2063
|
image_format: row.image_format,
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { IStoredFileCommands, IStoredFileQueries, ListStoredFilePathsParams, StorageValidator, StoredFilePathGroup } from '@byline/core';
|
|
9
|
+
import type { DBManager } from '../../lib/db-manager.js';
|
|
10
|
+
export declare class StoredFileQueries implements IStoredFileQueries {
|
|
11
|
+
private dbManager;
|
|
12
|
+
constructor(dbManager: DBManager);
|
|
13
|
+
private get db();
|
|
14
|
+
listStoredFilePaths(params: ListStoredFilePathsParams): Promise<StoredFilePathGroup[]>;
|
|
15
|
+
}
|
|
16
|
+
export declare class StoredFileCommands implements IStoredFileCommands {
|
|
17
|
+
private dbManager;
|
|
18
|
+
constructor(dbManager: DBManager);
|
|
19
|
+
private get db();
|
|
20
|
+
setIdentityForPath(params: {
|
|
21
|
+
storageProvider: string;
|
|
22
|
+
storagePath: string;
|
|
23
|
+
collectionId?: string;
|
|
24
|
+
fileHash: string;
|
|
25
|
+
storageValidator: StorageValidator;
|
|
26
|
+
}): Promise<number>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
// Stored-file identity maintenance (backfill / verify). Storage keys,
|
|
10
|
+
// provider names and recorded digests must compare exactly. The database
|
|
11
|
+
// default collation (utf8mb4_0900_ai_ci) folds case and accents, and even
|
|
12
|
+
// utf8mb4_bin is PAD SPACE (it equates 'a.pdf' and 'a.pdf '). utf8mb4_0900_bin
|
|
13
|
+
// is NO PAD but needs MySQL 8.0.17, above the adapter's 8.0.14 floor. So every
|
|
14
|
+
// comparison, DISTINCT, ORDER BY, join and GROUP BY here goes through
|
|
15
|
+
// CAST(... AS BINARY): byte equality, trailing spaces significant, byte order.
|
|
16
|
+
const binary = (expression) => sql `CAST(${expression} AS BINARY)`;
|
|
17
|
+
function scopeFilter(params, alias = '') {
|
|
18
|
+
const column = (name) => sql.raw(`${alias}${name}`);
|
|
19
|
+
const parts = [
|
|
20
|
+
sql `${binary(column('storage_provider'))} = ${binary(sql `${params.storageProvider}`)}`,
|
|
21
|
+
params.identity === 'missing'
|
|
22
|
+
? sql `${column('file_hash')} IS NULL`
|
|
23
|
+
: sql `${column('file_hash')} IS NOT NULL`,
|
|
24
|
+
];
|
|
25
|
+
if (params.collectionId !== undefined) {
|
|
26
|
+
parts.push(sql `${column('collection_id')} = ${params.collectionId}`);
|
|
27
|
+
}
|
|
28
|
+
return sql.join(parts, sql ` AND `);
|
|
29
|
+
}
|
|
30
|
+
function rowsOf(result) {
|
|
31
|
+
return result[0];
|
|
32
|
+
}
|
|
33
|
+
/** mysql2 returns binary-cast columns as Buffers; decode them back to text. */
|
|
34
|
+
function text(value) {
|
|
35
|
+
return Buffer.isBuffer(value) ? value.toString('utf8') : value;
|
|
36
|
+
}
|
|
37
|
+
export class StoredFileQueries {
|
|
38
|
+
dbManager;
|
|
39
|
+
constructor(dbManager) {
|
|
40
|
+
this.dbManager = dbManager;
|
|
41
|
+
}
|
|
42
|
+
get db() {
|
|
43
|
+
return this.dbManager.get();
|
|
44
|
+
}
|
|
45
|
+
async listStoredFilePaths(params) {
|
|
46
|
+
const after = params.afterPath === undefined
|
|
47
|
+
? sql ``
|
|
48
|
+
: sql ` AND ${binary(sql `storage_path`)} > ${binary(sql `${params.afterPath}`)}`;
|
|
49
|
+
// A page is a bounded set of distinct paths; `limit` never counts rows.
|
|
50
|
+
const pathsQuery = sql `
|
|
51
|
+
SELECT DISTINCT ${binary(sql `storage_path`)} AS path_key
|
|
52
|
+
FROM byline_store_file
|
|
53
|
+
WHERE ${scopeFilter(params)}${after}
|
|
54
|
+
ORDER BY path_key
|
|
55
|
+
LIMIT ${params.limit}
|
|
56
|
+
`;
|
|
57
|
+
if (params.identity === 'missing') {
|
|
58
|
+
const result = await this.db.execute(pathsQuery);
|
|
59
|
+
return rowsOf(result).map((row) => ({
|
|
60
|
+
storagePath: text(row.path_key),
|
|
61
|
+
hashes: [],
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
// Every recorded-hash group for each selected path, so a path's groups are
|
|
65
|
+
// never split across pages. Digests group by bytes, so differently spelled
|
|
66
|
+
// legacy values are reported separately.
|
|
67
|
+
const result = await this.db.execute(sql `
|
|
68
|
+
WITH paths AS (${pathsQuery})
|
|
69
|
+
SELECT p.path_key, ${binary(sql `f.file_hash`)} AS hash_key, COUNT(*) AS row_count
|
|
70
|
+
FROM byline_store_file f
|
|
71
|
+
JOIN paths p ON ${binary(sql `f.storage_path`)} = p.path_key
|
|
72
|
+
WHERE ${scopeFilter(params, 'f.')}
|
|
73
|
+
GROUP BY p.path_key, hash_key
|
|
74
|
+
ORDER BY p.path_key, hash_key
|
|
75
|
+
`);
|
|
76
|
+
const groups = [];
|
|
77
|
+
for (const row of rowsOf(result)) {
|
|
78
|
+
const storagePath = text(row.path_key);
|
|
79
|
+
let group = groups.at(-1);
|
|
80
|
+
if (!group || group.storagePath !== storagePath) {
|
|
81
|
+
group = { storagePath, hashes: [] };
|
|
82
|
+
groups.push(group);
|
|
83
|
+
}
|
|
84
|
+
group.hashes.push({ fileHash: text(row.hash_key), rowCount: Number(row.row_count) });
|
|
85
|
+
}
|
|
86
|
+
return groups;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export class StoredFileCommands {
|
|
90
|
+
dbManager;
|
|
91
|
+
constructor(dbManager) {
|
|
92
|
+
this.dbManager = dbManager;
|
|
93
|
+
}
|
|
94
|
+
get db() {
|
|
95
|
+
return this.dbManager.get();
|
|
96
|
+
}
|
|
97
|
+
async setIdentityForPath(params) {
|
|
98
|
+
const collection = params.collectionId === undefined ? sql `` : sql ` AND collection_id = ${params.collectionId}`;
|
|
99
|
+
const result = await this.db.execute(sql `
|
|
100
|
+
UPDATE byline_store_file
|
|
101
|
+
SET file_hash = ${params.fileHash},
|
|
102
|
+
storage_validator_kind = ${params.storageValidator.kind},
|
|
103
|
+
storage_validator_value = ${params.storageValidator.value},
|
|
104
|
+
updated_at = NOW()
|
|
105
|
+
WHERE ${binary(sql `storage_provider`)} = ${binary(sql `${params.storageProvider}`)}
|
|
106
|
+
AND ${binary(sql `storage_path`)} = ${binary(sql `${params.storagePath}`)}
|
|
107
|
+
AND file_hash IS NULL${collection}
|
|
108
|
+
`);
|
|
109
|
+
return result[0].affectedRows;
|
|
110
|
+
}
|
|
111
|
+
}
|
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": "6.
|
|
5
|
+
"version": "6.12.1",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.22.2"
|
|
8
8
|
},
|
|
@@ -47,24 +47,24 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@byline/admin": "6.
|
|
51
|
-
"@byline/core": "6.
|
|
50
|
+
"@byline/admin": "6.12.1",
|
|
51
|
+
"@byline/core": "6.12.1",
|
|
52
52
|
"drizzle-orm": "^0.45.4",
|
|
53
53
|
"mysql2": "^3.24.6",
|
|
54
54
|
"uuid": "^14.0.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@biomejs/biome": "2.6.
|
|
58
|
-
"@byline/auth": "6.
|
|
59
|
-
"@byline/client": "6.
|
|
57
|
+
"@biomejs/biome": "2.6.4",
|
|
58
|
+
"@byline/auth": "6.12.1",
|
|
59
|
+
"@byline/client": "6.12.1",
|
|
60
60
|
"@byline/db-conformance": "0.0.10",
|
|
61
|
-
"@types/node": "^26.6.
|
|
61
|
+
"@types/node": "^26.6.6",
|
|
62
62
|
"chokidar": "^6.0.0",
|
|
63
63
|
"chokidar-cli": "^3.0.0",
|
|
64
|
-
"dotenv": "^18.0.
|
|
64
|
+
"dotenv": "^18.0.9",
|
|
65
65
|
"drizzle-kit": "^0.31.11",
|
|
66
66
|
"npm-run-all": "^4.1.5",
|
|
67
|
-
"tsc-alias": "^1.9.
|
|
67
|
+
"tsc-alias": "^1.9.10",
|
|
68
68
|
"typescript": "^7.0.2",
|
|
69
69
|
"vitest": "^5.0.5"
|
|
70
70
|
},
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"db:init": "cd src/database && ./db_init.sh",
|
|
81
81
|
"drizzle:generate": "tsc -p tsconfig.json && drizzle-kit generate --config=drizzle.config.ts",
|
|
82
82
|
"drizzle:migrate": "drizzle-kit migrate --config=drizzle.config.ts",
|
|
83
|
+
"drizzle:rekey": "node --env-file-if-exists=.env scripts/drizzle-rekey.mjs",
|
|
84
|
+
"drizzle:rekey:test": "node --env-file-if-exists=.env.test scripts/drizzle-rekey.mjs",
|
|
83
85
|
"drizzle:push": "tsc -p tsconfig.json && drizzle-kit push --config=drizzle.config.ts",
|
|
84
86
|
"drizzle:drop": "drizzle-kit drop --config=drizzle.config.ts",
|
|
85
87
|
"test": "vitest run --mode=node",
|