@krantho/persistence-sqlite 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/dist/auth-adapter.d.ts +23 -0
- package/dist/auth-adapter.d.ts.map +1 -0
- package/dist/auth-adapter.js +159 -0
- package/dist/auth-adapter.js.map +1 -0
- package/dist/checkpoints.d.ts +20 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +119 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/connection.d.ts +29 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +77 -0
- package/dist/connection.js.map +1 -0
- package/dist/content-migration.d.ts +18 -0
- package/dist/content-migration.d.ts.map +1 -0
- package/dist/content-migration.js +68 -0
- package/dist/content-migration.js.map +1 -0
- package/dist/content-query.d.ts +16 -0
- package/dist/content-query.d.ts.map +1 -0
- package/dist/content-query.js +155 -0
- package/dist/content-query.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lock-adapter.d.ts +11 -0
- package/dist/lock-adapter.d.ts.map +1 -0
- package/dist/lock-adapter.js +63 -0
- package/dist/lock-adapter.js.map +1 -0
- package/dist/media-adapter.d.ts +27 -0
- package/dist/media-adapter.d.ts.map +1 -0
- package/dist/media-adapter.js +175 -0
- package/dist/media-adapter.js.map +1 -0
- package/dist/media-folder-adapter.d.ts +13 -0
- package/dist/media-folder-adapter.d.ts.map +1 -0
- package/dist/media-folder-adapter.js +58 -0
- package/dist/media-folder-adapter.js.map +1 -0
- package/dist/migration-runner.d.ts +28 -0
- package/dist/migration-runner.d.ts.map +1 -0
- package/dist/migration-runner.js +109 -0
- package/dist/migration-runner.js.map +1 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +32 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/v001-initial-schema.d.ts +10 -0
- package/dist/migrations/v001-initial-schema.d.ts.map +1 -0
- package/dist/migrations/v001-initial-schema.js +94 -0
- package/dist/migrations/v001-initial-schema.js.map +1 -0
- package/dist/migrations/v002-auth-tables.d.ts +9 -0
- package/dist/migrations/v002-auth-tables.d.ts.map +1 -0
- package/dist/migrations/v002-auth-tables.js +38 -0
- package/dist/migrations/v002-auth-tables.js.map +1 -0
- package/dist/migrations/v003-user-permissions.d.ts +9 -0
- package/dist/migrations/v003-user-permissions.d.ts.map +1 -0
- package/dist/migrations/v003-user-permissions.js +21 -0
- package/dist/migrations/v003-user-permissions.js.map +1 -0
- package/dist/migrations/v004-media-hotspot.d.ts +14 -0
- package/dist/migrations/v004-media-hotspot.d.ts.map +1 -0
- package/dist/migrations/v004-media-hotspot.js +24 -0
- package/dist/migrations/v004-media-hotspot.js.map +1 -0
- package/dist/migrations/v005-content-locks.d.ts +9 -0
- package/dist/migrations/v005-content-locks.d.ts.map +1 -0
- package/dist/migrations/v005-content-locks.js +24 -0
- package/dist/migrations/v005-content-locks.js.map +1 -0
- package/dist/migrations/v006-webhook-deliveries.d.ts +6 -0
- package/dist/migrations/v006-webhook-deliveries.d.ts.map +1 -0
- package/dist/migrations/v006-webhook-deliveries.js +24 -0
- package/dist/migrations/v006-webhook-deliveries.js.map +1 -0
- package/dist/migrations/v007-media-folders.d.ts +10 -0
- package/dist/migrations/v007-media-folders.d.ts.map +1 -0
- package/dist/migrations/v007-media-folders.js +23 -0
- package/dist/migrations/v007-media-folders.js.map +1 -0
- package/dist/migrations/v008-site-settings.d.ts +9 -0
- package/dist/migrations/v008-site-settings.d.ts.map +1 -0
- package/dist/migrations/v008-site-settings.js +20 -0
- package/dist/migrations/v008-site-settings.js.map +1 -0
- package/dist/migrations/v009-seo-config.d.ts +15 -0
- package/dist/migrations/v009-seo-config.d.ts.map +1 -0
- package/dist/migrations/v009-seo-config.js +29 -0
- package/dist/migrations/v009-seo-config.js.map +1 -0
- package/dist/migrations/v010-content-live-version.d.ts +15 -0
- package/dist/migrations/v010-content-live-version.d.ts.map +1 -0
- package/dist/migrations/v010-content-live-version.js +40 -0
- package/dist/migrations/v010-content-live-version.js.map +1 -0
- package/dist/migrations/v011-media.d.ts +12 -0
- package/dist/migrations/v011-media.d.ts.map +1 -0
- package/dist/migrations/v011-media.js +43 -0
- package/dist/migrations/v011-media.js.map +1 -0
- package/dist/migrations/v012-page-cache.d.ts +12 -0
- package/dist/migrations/v012-page-cache.d.ts.map +1 -0
- package/dist/migrations/v012-page-cache.js +31 -0
- package/dist/migrations/v012-page-cache.js.map +1 -0
- package/dist/migrations/v013-content-contracts.d.ts +17 -0
- package/dist/migrations/v013-content-contracts.d.ts.map +1 -0
- package/dist/migrations/v013-content-contracts.js +46 -0
- package/dist/migrations/v013-content-contracts.js.map +1 -0
- package/dist/migrations/v014-content-manifest-commit.d.ts +7 -0
- package/dist/migrations/v014-content-manifest-commit.d.ts.map +1 -0
- package/dist/migrations/v014-content-manifest-commit.js +12 -0
- package/dist/migrations/v014-content-manifest-commit.js.map +1 -0
- package/dist/page-cache-store.d.ts +17 -0
- package/dist/page-cache-store.d.ts.map +1 -0
- package/dist/page-cache-store.js +76 -0
- package/dist/page-cache-store.js.map +1 -0
- package/dist/queries.d.ts +82 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +25 -0
- package/dist/queries.js.map +1 -0
- package/dist/retention-adapter.d.ts +13 -0
- package/dist/retention-adapter.d.ts.map +1 -0
- package/dist/retention-adapter.js +62 -0
- package/dist/retention-adapter.js.map +1 -0
- package/dist/row-mapping.d.ts +14 -0
- package/dist/row-mapping.d.ts.map +1 -0
- package/dist/row-mapping.js +71 -0
- package/dist/row-mapping.js.map +1 -0
- package/dist/seo-config-adapter.d.ts +14 -0
- package/dist/seo-config-adapter.d.ts.map +1 -0
- package/dist/seo-config-adapter.js +57 -0
- package/dist/seo-config-adapter.js.map +1 -0
- package/dist/site-settings-adapter.d.ts +10 -0
- package/dist/site-settings-adapter.d.ts.map +1 -0
- package/dist/site-settings-adapter.js +27 -0
- package/dist/site-settings-adapter.js.map +1 -0
- package/dist/sqlite-adapter.d.ts +69 -0
- package/dist/sqlite-adapter.d.ts.map +1 -0
- package/dist/sqlite-adapter.js +242 -0
- package/dist/sqlite-adapter.js.map +1 -0
- package/dist/webhook-delivery-adapter.d.ts +27 -0
- package/dist/webhook-delivery-adapter.d.ts.map +1 -0
- package/dist/webhook-delivery-adapter.js +93 -0
- package/dist/webhook-delivery-adapter.js.map +1 -0
- package/package.json +38 -3
- package/README.md +0 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isDirectSortColumn, isSafeSortField, statusCondition } from '@krantho/persistence';
|
|
2
|
+
import { META_COLUMNS, SUMMARY_COLUMNS } from './queries.js';
|
|
3
|
+
import { rowToContentDocument, rowToDocumentMeta, rowToSummary } from './row-mapping.js';
|
|
4
|
+
/** Characters of a snippet before the match, and from the match on */
|
|
5
|
+
const SNIPPET_BEFORE = 80;
|
|
6
|
+
const SNIPPET_LENGTH = 200;
|
|
7
|
+
/** The column set a projection reads; `full` reads the whole row */
|
|
8
|
+
const PROJECTION_COLUMNS = {
|
|
9
|
+
meta: META_COLUMNS,
|
|
10
|
+
summary: SUMMARY_COLUMNS,
|
|
11
|
+
full: '*',
|
|
12
|
+
};
|
|
13
|
+
/** Escape LIKE wildcards (and the escape character) so the text matches literally */
|
|
14
|
+
function escapeLike(text) {
|
|
15
|
+
return text.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
16
|
+
}
|
|
17
|
+
/** The ORDER BY of a query: the requested sort, then `collection, slug, locale` so paging is stable */
|
|
18
|
+
function orderByClause(sort) {
|
|
19
|
+
const [field, direction] = sort ? sort.split(':') : [];
|
|
20
|
+
const tieBreak = 'collection, slug, locale';
|
|
21
|
+
if (!field)
|
|
22
|
+
return ` ORDER BY ${tieBreak}`;
|
|
23
|
+
const dir = direction === 'desc' ? 'DESC' : 'ASC';
|
|
24
|
+
if (isDirectSortColumn(field))
|
|
25
|
+
return ` ORDER BY ${field} ${dir}, ${tieBreak}`;
|
|
26
|
+
if (isSafeSortField(field)) {
|
|
27
|
+
return ` ORDER BY json_extract(fields, '$.${field}') ${dir}, ${tieBreak}`;
|
|
28
|
+
}
|
|
29
|
+
return ` ORDER BY ${tieBreak}`;
|
|
30
|
+
}
|
|
31
|
+
/** The SQL expression of a text column the match folds and searches */
|
|
32
|
+
const MATCH_COLUMNS = {
|
|
33
|
+
slug: 'slug',
|
|
34
|
+
fields: 'fields',
|
|
35
|
+
body: 'body',
|
|
36
|
+
};
|
|
37
|
+
/** The condition that a document matches the text in any of the targets */
|
|
38
|
+
function matchCondition(targets) {
|
|
39
|
+
const likes = targets.map((target) => `kr_lower(${MATCH_COLUMNS[target]}) LIKE ? ESCAPE '\\'`);
|
|
40
|
+
return likes.length > 0 ? `(${likes.join(' OR ')})` : '0';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The substring of the first match: from the body when it matches there, else from
|
|
44
|
+
* the fields' JSON. `instr` and `substr` count characters, so the position found in
|
|
45
|
+
* the folded text applies to the original.
|
|
46
|
+
*/
|
|
47
|
+
function snippetColumn(targets) {
|
|
48
|
+
const cases = ['body', 'fields']
|
|
49
|
+
.filter((target) => targets.includes(target))
|
|
50
|
+
.map((target) => {
|
|
51
|
+
const column = MATCH_COLUMNS[target];
|
|
52
|
+
const position = `instr(kr_lower(${column}), ?)`;
|
|
53
|
+
return `WHEN ${position} > 0 THEN substr(${column}, max(1, ${position} - ${SNIPPET_BEFORE}), ${SNIPPET_LENGTH})`;
|
|
54
|
+
});
|
|
55
|
+
if (cases.length === 0)
|
|
56
|
+
return { sql: '', paramCount: 0 };
|
|
57
|
+
return { sql: `, CASE ${cases.join(' ')} END AS snippet`, paramCount: cases.length * 2 };
|
|
58
|
+
}
|
|
59
|
+
/** The WHERE conditions of the query's current or live rows, and their parameters */
|
|
60
|
+
function scopeConditions(spec) {
|
|
61
|
+
const conditions = [];
|
|
62
|
+
const params = [];
|
|
63
|
+
if (spec.collections) {
|
|
64
|
+
conditions.push(`collection IN (${spec.collections.map(() => '?').join(', ')})`);
|
|
65
|
+
params.push(...spec.collections);
|
|
66
|
+
}
|
|
67
|
+
if (spec.locale) {
|
|
68
|
+
conditions.push('locale = ?');
|
|
69
|
+
params.push(spec.locale);
|
|
70
|
+
}
|
|
71
|
+
return { conditions, params };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The FROM/WHERE of the rows the query reads. `current` is the current, not deleted
|
|
75
|
+
* rows; `published` the live row of each current, not deleted document, found in a
|
|
76
|
+
* subquery over the same table so the outer columns stay those of the live row.
|
|
77
|
+
*/
|
|
78
|
+
function sourceClause(spec) {
|
|
79
|
+
const { conditions, params } = scopeConditions(spec);
|
|
80
|
+
if (spec.state === 'current') {
|
|
81
|
+
const where = ['is_current = 1', 'deleted_at IS NULL', ...conditions];
|
|
82
|
+
if (spec.status)
|
|
83
|
+
where.push(statusCondition(spec.status));
|
|
84
|
+
return { where, params };
|
|
85
|
+
}
|
|
86
|
+
const inner = conditions.map((condition) => `cur.${condition}`);
|
|
87
|
+
const liveIds = `SELECT live.id FROM content cur
|
|
88
|
+
JOIN content live ON live.collection = cur.collection AND live.slug = cur.slug
|
|
89
|
+
AND live.locale = cur.locale AND live.version = cur.live_version
|
|
90
|
+
WHERE ${['cur.is_current = 1', 'cur.deleted_at IS NULL', ...inner].join(' AND ')}`;
|
|
91
|
+
return { where: [`id IN (${liveIds})`], params };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build the one statement a content query runs: the projection's columns, the rows of
|
|
95
|
+
* its state, the text match, the order and a page of `limit + 1` rows.
|
|
96
|
+
*
|
|
97
|
+
* Every name in the SQL is a whitelisted column; collections, locale, match text and
|
|
98
|
+
* paging are parameters.
|
|
99
|
+
*/
|
|
100
|
+
export function buildContentQuery(spec) {
|
|
101
|
+
const source = sourceClause(spec);
|
|
102
|
+
const where = [...source.where];
|
|
103
|
+
const params = [];
|
|
104
|
+
let snippet = { sql: '', paramCount: 0 };
|
|
105
|
+
let needle = '';
|
|
106
|
+
if (spec.match) {
|
|
107
|
+
needle = spec.match.text.toLowerCase();
|
|
108
|
+
snippet = snippetColumn(spec.match.in);
|
|
109
|
+
where.push(matchCondition(spec.match.in));
|
|
110
|
+
}
|
|
111
|
+
// Parameters are bound in the order their `?` appear: the snippet in the select
|
|
112
|
+
// list, the scope and the match in the WHERE.
|
|
113
|
+
params.push(...Array.from({ length: snippet.paramCount }, () => needle));
|
|
114
|
+
params.push(...source.params);
|
|
115
|
+
if (spec.match) {
|
|
116
|
+
params.push(...spec.match.in.map(() => `%${escapeLike(needle)}%`));
|
|
117
|
+
}
|
|
118
|
+
let sql = `SELECT ${PROJECTION_COLUMNS[spec.projection]}${snippet.sql} FROM content WHERE ${where.join(' AND ')}`;
|
|
119
|
+
sql += orderByClause(spec.sort);
|
|
120
|
+
if (spec.limit !== undefined) {
|
|
121
|
+
sql += ' LIMIT ?';
|
|
122
|
+
params.push(spec.limit + 1);
|
|
123
|
+
}
|
|
124
|
+
if (spec.offset !== undefined) {
|
|
125
|
+
sql += spec.limit === undefined ? ' LIMIT -1 OFFSET ?' : ' OFFSET ?';
|
|
126
|
+
params.push(spec.offset);
|
|
127
|
+
}
|
|
128
|
+
return { sql, params };
|
|
129
|
+
}
|
|
130
|
+
/** A snippet is the matched text with its whitespace runs collapsed to one space */
|
|
131
|
+
function collapseWhitespace(text) {
|
|
132
|
+
return text.replace(/\s+/g, ' ');
|
|
133
|
+
}
|
|
134
|
+
function rowItem(projection, row) {
|
|
135
|
+
if (projection === 'meta')
|
|
136
|
+
return rowToDocumentMeta(row);
|
|
137
|
+
if (projection === 'summary')
|
|
138
|
+
return rowToSummary(row);
|
|
139
|
+
return rowToContentDocument(row);
|
|
140
|
+
}
|
|
141
|
+
/** Run a content query: one statement, one page */
|
|
142
|
+
export function runContentQuery(db, spec) {
|
|
143
|
+
const { sql, params } = buildContentQuery(spec);
|
|
144
|
+
const rows = db.prepare(sql).all(...params);
|
|
145
|
+
const hasMore = spec.limit !== undefined && rows.length > spec.limit;
|
|
146
|
+
const page = hasMore ? rows.slice(0, spec.limit) : rows;
|
|
147
|
+
const items = page.map((row) => {
|
|
148
|
+
const item = rowItem(spec.projection, row);
|
|
149
|
+
if (typeof row['snippet'] === 'string')
|
|
150
|
+
item.snippet = collapseWhitespace(row['snippet']);
|
|
151
|
+
return item;
|
|
152
|
+
});
|
|
153
|
+
return { items, hasMore };
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=content-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-query.js","sourceRoot":"","sources":["../src/content-query.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { SqliteAuthAdapter } from './auth-adapter.js';
|
|
2
|
+
export { SqliteCheckpointAdapter } from './checkpoints.js';
|
|
3
|
+
export type { SqliteConnectionOptions } from './connection.js';
|
|
4
|
+
export { DEFAULT_SQLITE_CONNECTION_OPTIONS, openDatabase } from './connection.js';
|
|
5
|
+
export { computeContentChecksum, decideMigration, SqliteContentMigrationAdapter, } from './content-migration.js';
|
|
6
|
+
export { SqliteLockAdapter } from './lock-adapter.js';
|
|
7
|
+
export { SqliteMediaAdapter } from './media-adapter.js';
|
|
8
|
+
export { SqliteMediaFolderAdapter } from './media-folder-adapter.js';
|
|
9
|
+
export type { SqliteMigration } from './migration-runner.js';
|
|
10
|
+
export { getAppliedMigrations, getSchemaVersion, runMigrations } from './migration-runner.js';
|
|
11
|
+
export { sqliteMigrations } from './migrations/index.js';
|
|
12
|
+
export { SqlitePageCacheStore } from './page-cache-store.js';
|
|
13
|
+
export type { ContentRow } from './queries.js';
|
|
14
|
+
export { SqliteRetentionAdapter } from './retention-adapter.js';
|
|
15
|
+
export { SqliteSeoConfigAdapter } from './seo-config-adapter.js';
|
|
16
|
+
export { SqliteSiteSettingsAdapter } from './site-settings-adapter.js';
|
|
17
|
+
export type { SQLiteAdapterOptions, SqliteDefaultStatus } from './sqlite-adapter.js';
|
|
18
|
+
export { DEFAULT_SQLITE_STATUS, SQLiteAdapter } from './sqlite-adapter.js';
|
|
19
|
+
export { SqliteWebhookDeliveryAdapter } from './webhook-delivery-adapter.js';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,iCAAiC,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACjF,OAAO,EACL,sBAAsB,EACtB,eAAe,EACf,6BAA6B,GAC9B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACpE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAA;AACtE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACpF,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { SqliteAuthAdapter } from './auth-adapter.js';
|
|
2
|
+
export { SqliteCheckpointAdapter } from './checkpoints.js';
|
|
3
|
+
export { DEFAULT_SQLITE_CONNECTION_OPTIONS, openDatabase } from './connection.js';
|
|
4
|
+
export { computeContentChecksum, decideMigration, SqliteContentMigrationAdapter, } from './content-migration.js';
|
|
5
|
+
export { SqliteLockAdapter } from './lock-adapter.js';
|
|
6
|
+
export { SqliteMediaAdapter } from './media-adapter.js';
|
|
7
|
+
export { SqliteMediaFolderAdapter } from './media-folder-adapter.js';
|
|
8
|
+
export { getAppliedMigrations, getSchemaVersion, runMigrations } from './migration-runner.js';
|
|
9
|
+
export { sqliteMigrations } from './migrations/index.js';
|
|
10
|
+
export { SqlitePageCacheStore } from './page-cache-store.js';
|
|
11
|
+
export { SqliteRetentionAdapter } from './retention-adapter.js';
|
|
12
|
+
export { SqliteSeoConfigAdapter } from './seo-config-adapter.js';
|
|
13
|
+
export { SqliteSiteSettingsAdapter } from './site-settings-adapter.js';
|
|
14
|
+
export { DEFAULT_SQLITE_STATUS, SQLiteAdapter } from './sqlite-adapter.js';
|
|
15
|
+
export { SqliteWebhookDeliveryAdapter } from './webhook-delivery-adapter.js';
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,OAAO,EAAE,iCAAiC,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACjF,OAAO,EACL,sBAAsB,EACtB,eAAe,EACf,6BAA6B,GAC9B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAEpE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAE5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAA;AAEtE,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ContentLock, ILockAdapter } from '@krantho/persistence';
|
|
2
|
+
/** SQLite implementation of advisory content locking */
|
|
3
|
+
export declare class SqliteLockAdapter implements ILockAdapter {
|
|
4
|
+
private readonly db;
|
|
5
|
+
constructor(db: import('better-sqlite3').Database);
|
|
6
|
+
getLock(collection: string, slug: string, locale: string): Promise<ContentLock | null>;
|
|
7
|
+
acquireLock(collection: string, slug: string, locale: string, lockedBy: string, expiresAt: string): Promise<void>;
|
|
8
|
+
releaseLock(collection: string, slug: string, locale: string): Promise<void>;
|
|
9
|
+
listLocks(): Promise<ContentLock[]>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=lock-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-adapter.d.ts","sourceRoot":"","sources":["../src/lock-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAWrE,wDAAwD;AACxD,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmC;gBAE1C,EAAE,EAAE,OAAO,gBAAgB,EAAE,QAAQ;IAI3C,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAyBtF,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAeV,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5E,SAAS,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;CAgB1C"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** SQLite implementation of advisory content locking */
|
|
2
|
+
export class SqliteLockAdapter {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
async getLock(collection, slug, locale) {
|
|
8
|
+
const row = this.db
|
|
9
|
+
.prepare('SELECT * FROM content_locks WHERE collection = ? AND slug = ? AND locale = ?')
|
|
10
|
+
.get(collection, slug, locale);
|
|
11
|
+
if (!row)
|
|
12
|
+
return null;
|
|
13
|
+
// Clean up expired locks
|
|
14
|
+
if (new Date(row.expires_at) <= new Date()) {
|
|
15
|
+
this.db
|
|
16
|
+
.prepare('DELETE FROM content_locks WHERE collection = ? AND slug = ? AND locale = ?')
|
|
17
|
+
.run(collection, slug, locale);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
collection: row.collection,
|
|
22
|
+
slug: row.slug,
|
|
23
|
+
locale: row.locale,
|
|
24
|
+
lockedBy: row.locked_by,
|
|
25
|
+
lockedAt: row.locked_at,
|
|
26
|
+
expiresAt: row.expires_at,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async acquireLock(collection, slug, locale, lockedBy, expiresAt) {
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
// Atomic upsert — avoids race condition between SELECT + INSERT
|
|
32
|
+
this.db
|
|
33
|
+
.prepare(`INSERT INTO content_locks (collection, slug, locale, locked_by, locked_at, expires_at)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
35
|
+
ON CONFLICT(collection, slug, locale) DO UPDATE SET
|
|
36
|
+
locked_by = excluded.locked_by,
|
|
37
|
+
locked_at = excluded.locked_at,
|
|
38
|
+
expires_at = excluded.expires_at`)
|
|
39
|
+
.run(collection, slug, locale, lockedBy, now, expiresAt);
|
|
40
|
+
}
|
|
41
|
+
async releaseLock(collection, slug, locale) {
|
|
42
|
+
this.db
|
|
43
|
+
.prepare('DELETE FROM content_locks WHERE collection = ? AND slug = ? AND locale = ?')
|
|
44
|
+
.run(collection, slug, locale);
|
|
45
|
+
}
|
|
46
|
+
async listLocks() {
|
|
47
|
+
// Expiry times are ISO 8601 UTC strings, so they compare as text
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
this.db.prepare('DELETE FROM content_locks WHERE expires_at <= ?').run(now);
|
|
50
|
+
const rows = this.db
|
|
51
|
+
.prepare('SELECT * FROM content_locks ORDER BY collection, slug, locale')
|
|
52
|
+
.all();
|
|
53
|
+
return rows.map((row) => ({
|
|
54
|
+
collection: row.collection,
|
|
55
|
+
slug: row.slug,
|
|
56
|
+
locale: row.locale,
|
|
57
|
+
lockedBy: row.locked_by,
|
|
58
|
+
lockedAt: row.locked_at,
|
|
59
|
+
expiresAt: row.expires_at,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=lock-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-adapter.js","sourceRoot":"","sources":["../src/lock-adapter.ts"],"names":[],"mappings":"AAWA,wDAAwD;AACxD,MAAM,OAAO,iBAAiB;IACX,EAAE,CAAmC;IAEtD,YAAY,EAAqC;QAC/C,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAkB,EAAE,IAAY,EAAE,MAAc;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,8EAA8E,CAAC;aACvF,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAwB,CAAA;QAEvD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QAErB,yBAAyB;QACzB,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YAC3C,IAAI,CAAC,EAAE;iBACJ,OAAO,CAAC,4EAA4E,CAAC;iBACrF,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;YAChC,OAAO,IAAI,CAAA;QACb,CAAC;QAED,OAAO;YACL,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,SAAS,EAAE,GAAG,CAAC,UAAU;SAC1B,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CACf,UAAkB,EAClB,IAAY,EACZ,MAAc,EACd,QAAgB,EAChB,SAAiB;QAEjB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACpC,gEAAgE;QAChE,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;;;;;4CAKoC,CACrC;aACA,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,CAAC,CAAA;IAC5D,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,UAAkB,EAAE,IAAY,EAAE,MAAc;QAChE,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,4EAA4E,CAAC;aACrF,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,SAAS;QACb,iEAAiE;QACjE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACpC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,iDAAiD,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3E,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,+DAA+D,CAAC;aACxE,GAAG,EAAe,CAAA;QACrB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACxB,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,SAAS,EAAE,GAAG,CAAC,UAAU;SAC1B,CAAC,CAAC,CAAA;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IMediaMetadataAdapter, MediaListOptions, MediaRecord, MediaRecordChanges, MediaStats, NewMediaRecord } from '@krantho/persistence';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
/**
|
|
4
|
+
* SQLite implementation of IMediaMetadataAdapter.
|
|
5
|
+
*
|
|
6
|
+
* Append-only: every change inserts a new version row and flips
|
|
7
|
+
* `is_current`; a delete appends a tombstone with `deleted_at` set.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SqliteMediaAdapter implements IMediaMetadataAdapter {
|
|
10
|
+
private readonly db;
|
|
11
|
+
constructor(db: Database.Database);
|
|
12
|
+
insert(record: NewMediaRecord): Promise<MediaRecord>;
|
|
13
|
+
get(id: string): Promise<MediaRecord | null>;
|
|
14
|
+
list(options?: MediaListOptions | undefined): Promise<{
|
|
15
|
+
items: MediaRecord[];
|
|
16
|
+
total: number;
|
|
17
|
+
}>;
|
|
18
|
+
appendVersion(id: string, changes: MediaRecordChanges): Promise<MediaRecord | null>;
|
|
19
|
+
markDeleted(id: string): Promise<MediaRecord | null>;
|
|
20
|
+
stats(): Promise<MediaStats>;
|
|
21
|
+
listTags(): Promise<string[]>;
|
|
22
|
+
private currentRow;
|
|
23
|
+
private requireCurrent;
|
|
24
|
+
/** Insert the next version of `id` and make it current. Runs inside a transaction. */
|
|
25
|
+
private writeVersion;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=media-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-adapter.d.ts","sourceRoot":"","sources":["../src/media-adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EAGrB,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,cAAc,EACf,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AA8B1C;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,qBAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAE5C,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC;IAQpD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAK5C,IAAI,CACR,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,GACrC,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAc7C,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAUnF,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAUpD,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IAe5B,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAWnC,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,cAAc;IAMtB,sFAAsF;IACtF,OAAO,CAAC,YAAY;CA+BrB"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { escapeSqlLikePattern } from '@krantho/persistence';
|
|
3
|
+
const CURRENT = 'is_current = 1 AND deleted_at IS NULL';
|
|
4
|
+
/**
|
|
5
|
+
* SQLite implementation of IMediaMetadataAdapter.
|
|
6
|
+
*
|
|
7
|
+
* Append-only: every change inserts a new version row and flips
|
|
8
|
+
* `is_current`; a delete appends a tombstone with `deleted_at` set.
|
|
9
|
+
*/
|
|
10
|
+
export class SqliteMediaAdapter {
|
|
11
|
+
db;
|
|
12
|
+
constructor(db) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
}
|
|
15
|
+
async insert(record) {
|
|
16
|
+
const { id, ...data } = record;
|
|
17
|
+
return this.db.transaction(() => {
|
|
18
|
+
this.writeVersion(id, data, null);
|
|
19
|
+
return this.requireCurrent(id);
|
|
20
|
+
})();
|
|
21
|
+
}
|
|
22
|
+
async get(id) {
|
|
23
|
+
const row = this.currentRow(id);
|
|
24
|
+
return row ? rowToRecord(row) : null;
|
|
25
|
+
}
|
|
26
|
+
async list(options) {
|
|
27
|
+
const { where, params } = buildFilter(options);
|
|
28
|
+
const page = buildPage(options);
|
|
29
|
+
const rows = this.db
|
|
30
|
+
.prepare(`SELECT * FROM media ${where} ORDER BY created_at DESC${page.sql}`)
|
|
31
|
+
.all(...params, ...page.params);
|
|
32
|
+
const count = this.db
|
|
33
|
+
.prepare(`SELECT COUNT(*) AS count FROM media ${where}`)
|
|
34
|
+
.get(...params);
|
|
35
|
+
return { items: rows.map(rowToRecord), total: count.count };
|
|
36
|
+
}
|
|
37
|
+
async appendVersion(id, changes) {
|
|
38
|
+
return this.db.transaction(() => {
|
|
39
|
+
const current = this.currentRow(id);
|
|
40
|
+
if (!current)
|
|
41
|
+
return null;
|
|
42
|
+
const { id: _id, createdAt: _createdAt, ...data } = rowToRecord(current);
|
|
43
|
+
this.writeVersion(id, { ...data, ...changes }, null);
|
|
44
|
+
return this.requireCurrent(id);
|
|
45
|
+
})();
|
|
46
|
+
}
|
|
47
|
+
async markDeleted(id) {
|
|
48
|
+
return this.db.transaction(() => {
|
|
49
|
+
const current = this.currentRow(id);
|
|
50
|
+
if (!current)
|
|
51
|
+
return null;
|
|
52
|
+
const superseded = rowToRecord(current);
|
|
53
|
+
this.writeVersion(id, tombstone(superseded), new Date().toISOString());
|
|
54
|
+
return superseded;
|
|
55
|
+
})();
|
|
56
|
+
}
|
|
57
|
+
async stats() {
|
|
58
|
+
const totals = this.db
|
|
59
|
+
.prepare(`SELECT COUNT(*) AS items, COALESCE(SUM(size), 0) AS size FROM media WHERE ${CURRENT}`)
|
|
60
|
+
.get();
|
|
61
|
+
const folders = this.db
|
|
62
|
+
.prepare(`SELECT folder, COUNT(*) AS count FROM media WHERE ${CURRENT} AND folder IS NOT NULL GROUP BY folder`)
|
|
63
|
+
.all();
|
|
64
|
+
const byFolder = Object.fromEntries(folders.map((row) => [row.folder, row.count]));
|
|
65
|
+
return { totalItems: totals.items, totalSize: totals.size, byFolder };
|
|
66
|
+
}
|
|
67
|
+
async listTags() {
|
|
68
|
+
const rows = this.db
|
|
69
|
+
.prepare(`SELECT tags FROM media WHERE ${CURRENT} AND tags IS NOT NULL AND tags != '[]'`)
|
|
70
|
+
.all();
|
|
71
|
+
const tagSet = new Set();
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
for (const tag of parseJson(row.tags) ?? [])
|
|
74
|
+
tagSet.add(tag);
|
|
75
|
+
}
|
|
76
|
+
return [...tagSet].sort();
|
|
77
|
+
}
|
|
78
|
+
currentRow(id) {
|
|
79
|
+
return this.db.prepare(`SELECT * FROM media WHERE id = ? AND ${CURRENT}`).get(id);
|
|
80
|
+
}
|
|
81
|
+
requireCurrent(id) {
|
|
82
|
+
const row = this.currentRow(id);
|
|
83
|
+
if (!row)
|
|
84
|
+
throw new Error(`Media item ${id} not found after write`);
|
|
85
|
+
return rowToRecord(row);
|
|
86
|
+
}
|
|
87
|
+
/** Insert the next version of `id` and make it current. Runs inside a transaction. */
|
|
88
|
+
writeVersion(id, data, deletedAt) {
|
|
89
|
+
const max = this.db
|
|
90
|
+
.prepare('SELECT MAX(version) AS max_version FROM media WHERE id = ?')
|
|
91
|
+
.get(id);
|
|
92
|
+
const nextVersion = (max.max_version ?? 0) + 1;
|
|
93
|
+
this.db.prepare('UPDATE media SET is_current = 0 WHERE id = ? AND is_current = 1').run(id);
|
|
94
|
+
this.db
|
|
95
|
+
.prepare(`INSERT INTO media (row_id, id, version, is_current, filename, storage_key, url, alt_text, mime_type, size, width, height, folder, tags, crop, hotspot, deleted_at, created_at)
|
|
96
|
+
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
97
|
+
.run(randomUUID(), id, nextVersion, data.filename, data.storageKey, data.url, data.altText, data.mimeType, data.size, data.width, data.height, data.folder, JSON.stringify(data.tags), data.crop ? JSON.stringify(data.crop) : null, data.hotspot ? JSON.stringify(data.hotspot) : null, deletedAt, deletedAt ?? new Date().toISOString());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** The tombstone version that replaces a deleted record */
|
|
101
|
+
function tombstone(record) {
|
|
102
|
+
return {
|
|
103
|
+
filename: record.filename,
|
|
104
|
+
storageKey: record.storageKey,
|
|
105
|
+
url: record.url,
|
|
106
|
+
altText: '',
|
|
107
|
+
mimeType: record.mimeType,
|
|
108
|
+
size: 0,
|
|
109
|
+
width: null,
|
|
110
|
+
height: null,
|
|
111
|
+
folder: null,
|
|
112
|
+
tags: [],
|
|
113
|
+
crop: null,
|
|
114
|
+
hotspot: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** WHERE clause for current records matching search, folder, and tag filters */
|
|
118
|
+
function buildFilter(options) {
|
|
119
|
+
const conditions = [CURRENT];
|
|
120
|
+
const params = [];
|
|
121
|
+
if (options?.search) {
|
|
122
|
+
conditions.push("filename LIKE ? ESCAPE '\\'");
|
|
123
|
+
params.push(`%${escapeSqlLikePattern(options.search)}%`);
|
|
124
|
+
}
|
|
125
|
+
if (options?.folder !== undefined) {
|
|
126
|
+
if (options.folder === '') {
|
|
127
|
+
conditions.push('folder IS NULL');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
conditions.push('folder = ?');
|
|
131
|
+
params.push(options.folder);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (options?.tag) {
|
|
135
|
+
conditions.push("tags LIKE ? ESCAPE '\\'");
|
|
136
|
+
params.push(`%${escapeSqlLikePattern(JSON.stringify(options.tag))}%`);
|
|
137
|
+
}
|
|
138
|
+
return { where: `WHERE ${conditions.join(' AND ')}`, params };
|
|
139
|
+
}
|
|
140
|
+
/** LIMIT/OFFSET clause; SQLite needs a LIMIT (-1 = none) before an OFFSET */
|
|
141
|
+
function buildPage(options) {
|
|
142
|
+
if (options?.limit === undefined && options?.offset === undefined) {
|
|
143
|
+
return { sql: '', params: [] };
|
|
144
|
+
}
|
|
145
|
+
return { sql: ' LIMIT ? OFFSET ?', params: [options.limit ?? -1, options.offset ?? 0] };
|
|
146
|
+
}
|
|
147
|
+
function parseJson(text) {
|
|
148
|
+
if (!text)
|
|
149
|
+
return null;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(text);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function rowToRecord(row) {
|
|
158
|
+
return {
|
|
159
|
+
id: row.id,
|
|
160
|
+
filename: row.filename,
|
|
161
|
+
storageKey: row.storage_key,
|
|
162
|
+
url: row.url,
|
|
163
|
+
altText: row.alt_text,
|
|
164
|
+
mimeType: row.mime_type,
|
|
165
|
+
size: row.size,
|
|
166
|
+
width: row.width,
|
|
167
|
+
height: row.height,
|
|
168
|
+
folder: row.folder,
|
|
169
|
+
tags: parseJson(row.tags) ?? [],
|
|
170
|
+
crop: parseJson(row.crop),
|
|
171
|
+
hotspot: parseJson(row.hotspot),
|
|
172
|
+
createdAt: row.created_at,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=media-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-adapter.js","sourceRoot":"","sources":["../src/media-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAWxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AA6B3D,MAAM,OAAO,GAAG,uCAAuC,CAAA;AAEvD;;;;;GAKG;AACH,MAAM,OAAO,kBAAkB;IACA;IAA7B,YAA6B,EAAqB;QAArB,OAAE,GAAF,EAAE,CAAmB;IAAG,CAAC;IAEtD,KAAK,CAAC,MAAM,CAAC,MAAsB;QACjC,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAA;QAC9B,OAAO,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;YACjC,OAAO,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAA;QAChC,CAAC,CAAC,EAAE,CAAA;IACN,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAU;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;QAC/B,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACtC,CAAC;IAED,KAAK,CAAC,IAAI,CACR,OAAsC;QAEtC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;QAC9C,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAA;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,uBAAuB,KAAK,4BAA4B,IAAI,CAAC,GAAG,EAAE,CAAC;aAC3E,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAe,CAAA;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE;aAClB,OAAO,CAAC,uCAAuC,KAAK,EAAE,CAAC;aACvD,GAAG,CAAC,GAAG,MAAM,CAEf,CAAA;QACD,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAA;IAC7D,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,EAAU,EAAE,OAA2B;QACzD,OAAO,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;YACnC,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAA;YACzB,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;YACxE,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,CAAC,CAAA;YACpD,OAAO,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAA;QAChC,CAAC,CAAC,EAAE,CAAA;IACN,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,EAAU;QAC1B,OAAO,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;YACnC,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAA;YACzB,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;YACvC,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,CAAC,UAAU,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;YACtE,OAAO,UAAU,CAAA;QACnB,CAAC,CAAC,EAAE,CAAA;IACN,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE;aACnB,OAAO,CACN,6EAA6E,OAAO,EAAE,CACvF;aACA,GAAG,EAAqC,CAAA;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE;aACpB,OAAO,CACN,qDAAqD,OAAO,yCAAyC,CACtG;aACA,GAAG,EAA8C,CAAA;QACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAClF,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAA;IACvE,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,gCAAgC,OAAO,wCAAwC,CAAC;aACxF,GAAG,EAA6B,CAAA;QACnC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAA;QAChC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,KAAK,MAAM,GAAG,IAAI,SAAS,CAAW,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACxE,CAAC;QACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;IAC3B,CAAC;IAEO,UAAU,CAAC,EAAU;QAC3B,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,wCAAwC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAEnE,CAAA;IACf,CAAC;IAEO,cAAc,CAAC,EAAU;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,cAAc,EAAE,wBAAwB,CAAC,CAAA;QACnE,OAAO,WAAW,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IAED,sFAAsF;IAC9E,YAAY,CAAC,EAAU,EAAE,IAAiB,EAAE,SAAwB;QAC1E,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,4DAA4D,CAAC;aACrE,GAAG,CAAC,EAAE,CAAmC,CAAA;QAC5C,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;QAC9C,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,iEAAiE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC1F,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;uEAC+D,CAChE;aACA,GAAG,CACF,UAAU,EAAE,EACZ,EAAE,EACF,WAAW,EACX,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,GAAG,EACR,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EACzB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAC5C,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAClD,SAAS,EACT,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CACtC,CAAA;IACL,CAAC;CACF;AAED,2DAA2D;AAC3D,SAAS,SAAS,CAAC,MAAmB;IACpC,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,OAAO,EAAE,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,CAAC;QACP,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,IAAI;QACZ,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,IAAI;KACd,CAAA;AACH,CAAC;AAED,gFAAgF;AAChF,SAAS,WAAW,CAAC,OAAqC;IACxD,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5B,MAAM,MAAM,GAAc,EAAE,CAAA;IAE5B,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;QACpB,UAAU,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAA;QAC9C,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1D,CAAC;IACD,IAAI,OAAO,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YAC1B,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACnC,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IACD,IAAI,OAAO,EAAE,GAAG,EAAE,CAAC;QACjB,UAAU,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;QAC1C,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACvE,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,SAAS,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,CAAA;AAC/D,CAAC;AAED,6EAA6E;AAC7E,SAAS,SAAS,CAAC,OAAqC;IACtD,IAAI,OAAO,EAAE,KAAK,KAAK,SAAS,IAAI,OAAO,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;QAClE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IAChC,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE,CAAA;AACzF,CAAC;AAED,SAAS,SAAS,CAAI,IAAmB;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAa;IAChC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,UAAU,EAAE,GAAG,CAAC,WAAW;QAC3B,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,OAAO,EAAE,GAAG,CAAC,QAAQ;QACrB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,SAAS,CAAW,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QACzC,IAAI,EAAE,SAAS,CAAY,GAAG,CAAC,IAAI,CAAC;QACpC,OAAO,EAAE,SAAS,CAAe,GAAG,CAAC,OAAO,CAAC;QAC7C,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IMediaFolderStorage, MediaFolder } from '@krantho/persistence';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
/** SQLite implementation of IMediaFolderStorage, over the `media_folders` table (v007) */
|
|
4
|
+
export declare class SqliteMediaFolderAdapter implements IMediaFolderStorage {
|
|
5
|
+
private readonly db;
|
|
6
|
+
constructor(db: Database.Database);
|
|
7
|
+
listFolders(): Promise<MediaFolder[]>;
|
|
8
|
+
createFolder(name: string, parentId?: string | null): Promise<MediaFolder>;
|
|
9
|
+
renameFolder(id: string, name: string): Promise<MediaFolder | null>;
|
|
10
|
+
deleteFolder(id: string): Promise<void>;
|
|
11
|
+
private exists;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=media-folder-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-folder-adapter.d.ts","sourceRoot":"","sources":["../src/media-folder-adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC5E,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAS1C,0FAA0F;AAC1F,qBAAa,wBAAyB,YAAW,mBAAmB;IACtD,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAE5C,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAKrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC;IAiB1E,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAUnE,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB7C,OAAO,CAAC,MAAM;CAGf"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** SQLite implementation of IMediaFolderStorage, over the `media_folders` table (v007) */
|
|
3
|
+
export class SqliteMediaFolderAdapter {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
async listFolders() {
|
|
9
|
+
const rows = this.db.prepare('SELECT * FROM media_folders ORDER BY name').all();
|
|
10
|
+
return rows.map(rowToFolder);
|
|
11
|
+
}
|
|
12
|
+
async createFolder(name, parentId) {
|
|
13
|
+
if (parentId && !this.exists('SELECT id FROM media_folders WHERE id = ?', parentId)) {
|
|
14
|
+
throw new Error(`Parent folder "${parentId}" not found`);
|
|
15
|
+
}
|
|
16
|
+
const folder = {
|
|
17
|
+
id: randomUUID(),
|
|
18
|
+
name: name.trim(),
|
|
19
|
+
parentId: parentId ?? null,
|
|
20
|
+
createdAt: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
this.db
|
|
23
|
+
.prepare('INSERT INTO media_folders (id, name, parent_id, created_at) VALUES (?, ?, ?, ?)')
|
|
24
|
+
.run(folder.id, folder.name, folder.parentId, folder.createdAt);
|
|
25
|
+
return folder;
|
|
26
|
+
}
|
|
27
|
+
async renameFolder(id, name) {
|
|
28
|
+
const existing = this.db.prepare('SELECT * FROM media_folders WHERE id = ?').get(id);
|
|
29
|
+
if (!existing)
|
|
30
|
+
return null;
|
|
31
|
+
this.db.prepare('UPDATE media_folders SET name = ? WHERE id = ?').run(name.trim(), id);
|
|
32
|
+
return { ...rowToFolder(existing), name: name.trim() };
|
|
33
|
+
}
|
|
34
|
+
async deleteFolder(id) {
|
|
35
|
+
if (!this.exists('SELECT id FROM media_folders WHERE id = ?', id)) {
|
|
36
|
+
throw new Error(`Folder "${id}" not found`);
|
|
37
|
+
}
|
|
38
|
+
if (this.exists('SELECT id FROM media_folders WHERE parent_id = ? LIMIT 1', id)) {
|
|
39
|
+
throw new Error('Cannot delete folder: has child folders');
|
|
40
|
+
}
|
|
41
|
+
if (this.exists('SELECT id FROM media WHERE folder = ? AND is_current = 1 AND deleted_at IS NULL LIMIT 1', id)) {
|
|
42
|
+
throw new Error('Cannot delete folder: contains media items');
|
|
43
|
+
}
|
|
44
|
+
this.db.prepare('DELETE FROM media_folders WHERE id = ?').run(id);
|
|
45
|
+
}
|
|
46
|
+
exists(query, id) {
|
|
47
|
+
return this.db.prepare(query).get(id) !== undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function rowToFolder(row) {
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
name: row.name,
|
|
54
|
+
parentId: row.parent_id,
|
|
55
|
+
createdAt: row.created_at,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=media-folder-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-folder-adapter.js","sourceRoot":"","sources":["../src/media-folder-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAWxC,0FAA0F;AAC1F,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,EAAqB;QAArB,OAAE,GAAF,EAAE,CAAmB;IAAG,CAAC;IAEtD,KAAK,CAAC,WAAW;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAC,GAAG,EAAiB,CAAA;QAC9F,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,IAAY,EAAE,QAAwB;QACvD,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,2CAA2C,EAAE,QAAQ,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,aAAa,CAAC,CAAA;QAC1D,CAAC;QAED,MAAM,MAAM,GAAgB;YAC1B,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YACjB,QAAQ,EAAE,QAAQ,IAAI,IAAI;YAC1B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAA;QACD,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,iFAAiF,CAAC;aAC1F,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;QACjE,OAAO,MAAM,CAAA;IACf,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAU,EAAE,IAAY;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC,GAAG,CAAC,EAAE,CAEtE,CAAA;QACb,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAA;QAE1B,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,gDAAgD,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;QACtF,OAAO,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAA;IACxD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAU;QAC3B,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,2CAA2C,EAAE,EAAE,CAAC,EAAE,CAAC;YAClE,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;QAC7C,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,0DAA0D,EAAE,EAAE,CAAC,EAAE,CAAC;YAChF,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;QAC5D,CAAC;QACD,IACE,IAAI,CAAC,MAAM,CACT,yFAAyF,EACzF,EAAE,CACH,EACD,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;QAC/D,CAAC;QAED,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACnE,CAAC;IAEO,MAAM,CAAC,KAAa,EAAE,EAAU;QACtC,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,SAAS,CAAA;IACrD,CAAC;CACF;AAED,SAAS,WAAW,CAAC,GAAc;IACjC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AppliedMigration, MigrationResult } from '@krantho/persistence';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
/** A SQLite migration with synchronous apply function */
|
|
4
|
+
export interface SqliteMigration {
|
|
5
|
+
version: number;
|
|
6
|
+
name: string;
|
|
7
|
+
apply: (db: Database.Database) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run pending migrations on a SQLite database.
|
|
11
|
+
*
|
|
12
|
+
* Self-bootstrapping: creates the schema_migrations table if it doesn't exist.
|
|
13
|
+
* Each migration runs inside a transaction. Halts on first failure.
|
|
14
|
+
*
|
|
15
|
+
* Deciding what is pending and applying it is a check-then-act, so it runs inside
|
|
16
|
+
* a BEGIN IMMEDIATE transaction: the write lock is taken before the ledger is read
|
|
17
|
+
* and held until the last migration is recorded. Several processes may open the
|
|
18
|
+
* same database file at once — a server, a CLI, a framework spawning one build
|
|
19
|
+
* worker per core — and without the lock they all read an empty ledger and race to
|
|
20
|
+
* create the same tables. The loser waits on the connection's busy timeout and then
|
|
21
|
+
* re-reads the ledger inside the lock, finding the work already done.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runMigrations(db: Database.Database, migrations: readonly SqliteMigration[]): MigrationResult;
|
|
24
|
+
/** Get the current schema version from the database */
|
|
25
|
+
export declare function getSchemaVersion(db: Database.Database): number;
|
|
26
|
+
/** Get all applied migrations */
|
|
27
|
+
export declare function getAppliedMigrations(db: Database.Database): AppliedMigration[];
|
|
28
|
+
//# sourceMappingURL=migration-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-runner.d.ts","sourceRoot":"","sources":["../src/migration-runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAqB,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAMhG,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAAA;CACvC;AAgFD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,eAAe,CAajB;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAa9D;AAED,iCAAiC;AACjC,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CAU9E"}
|