@framers/sql-storage-adapter 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/adapters/electron/preload.cjs +289 -0
- package/dist/adapters/electron/renderer.d.ts +9 -0
- package/dist/adapters/electron/renderer.d.ts.map +1 -0
- package/dist/adapters/electron/renderer.js +8 -0
- package/dist/adapters/electron/renderer.js.map +1 -0
- package/dist/cjs/adapters/__tests__/indexedDbAdapter.spec.d.ts +2 -0
- package/dist/cjs/adapters/__tests__/indexedDbAdapter.spec.js +133 -0
- package/dist/cjs/adapters/baseStorageAdapter.d.ts +368 -0
- package/dist/cjs/adapters/baseStorageAdapter.js +744 -0
- package/dist/cjs/adapters/betterSqliteAdapter.d.ts +124 -0
- package/dist/cjs/adapters/betterSqliteAdapter.js +334 -0
- package/dist/cjs/adapters/capacitorSqliteAdapter.d.ts +83 -0
- package/dist/cjs/adapters/capacitorSqliteAdapter.js +250 -0
- package/dist/cjs/adapters/electron/electronMainAdapter.d.ts +251 -0
- package/dist/cjs/adapters/electron/electronMainAdapter.js +392 -0
- package/dist/cjs/adapters/electron/electronRendererAdapter.d.ts +177 -0
- package/dist/cjs/adapters/electron/electronRendererAdapter.js +283 -0
- package/dist/cjs/adapters/electron/index.d.ts +74 -0
- package/dist/cjs/adapters/electron/index.js +86 -0
- package/dist/cjs/adapters/electron/ipc/channels.d.ts +196 -0
- package/dist/cjs/adapters/electron/ipc/channels.js +127 -0
- package/dist/cjs/adapters/electron/ipc/index.d.ts +11 -0
- package/dist/cjs/adapters/electron/ipc/index.js +38 -0
- package/dist/cjs/adapters/electron/ipc/protocol.d.ts +78 -0
- package/dist/cjs/adapters/electron/ipc/protocol.js +341 -0
- package/dist/cjs/adapters/electron/ipc/types.d.ts +248 -0
- package/dist/cjs/adapters/electron/ipc/types.js +15 -0
- package/dist/cjs/adapters/electron/migration/autoMigrator.d.ts +184 -0
- package/dist/cjs/adapters/electron/migration/autoMigrator.js +441 -0
- package/dist/cjs/adapters/electron/migration/index.d.ts +9 -0
- package/dist/cjs/adapters/electron/migration/index.js +29 -0
- package/dist/cjs/adapters/electron/preload.d.ts +126 -0
- package/dist/cjs/adapters/electron/preload.js +233 -0
- package/dist/cjs/adapters/electron/recovery/corruptionDetector.d.ts +214 -0
- package/dist/cjs/adapters/electron/recovery/corruptionDetector.js +359 -0
- package/dist/cjs/adapters/electron/recovery/index.d.ts +11 -0
- package/dist/cjs/adapters/electron/recovery/index.js +34 -0
- package/dist/cjs/adapters/electron/recovery/walCheckpoint.d.ts +186 -0
- package/dist/cjs/adapters/electron/recovery/walCheckpoint.js +263 -0
- package/dist/cjs/adapters/electron/renderer.d.ts +9 -0
- package/dist/cjs/adapters/electron/renderer.js +33 -0
- package/dist/cjs/adapters/electron/window/index.d.ts +9 -0
- package/dist/cjs/adapters/electron/window/index.js +29 -0
- package/dist/cjs/adapters/electron/window/windowManager.d.ts +190 -0
- package/dist/cjs/adapters/electron/window/windowManager.js +321 -0
- package/dist/cjs/adapters/indexedDbAdapter.d.ts +490 -0
- package/dist/cjs/adapters/indexedDbAdapter.js +993 -0
- package/dist/cjs/adapters/postgresAdapter.d.ts +424 -0
- package/dist/cjs/adapters/postgresAdapter.js +889 -0
- package/dist/cjs/adapters/sqlJsAdapter.d.ts +64 -0
- package/dist/cjs/adapters/sqlJsAdapter.js +268 -0
- package/dist/cjs/adapters/supabase.d.ts +58 -0
- package/dist/cjs/adapters/supabase.js +401 -0
- package/dist/cjs/codecs/BrowserBlobCodec.d.ts +14 -0
- package/dist/cjs/codecs/BrowserBlobCodec.js +57 -0
- package/dist/cjs/codecs/NodeBlobCodec.d.ts +14 -0
- package/dist/cjs/codecs/NodeBlobCodec.js +59 -0
- package/dist/cjs/core/contracts/blobCodec.d.ts +15 -0
- package/dist/cjs/core/contracts/blobCodec.js +15 -0
- package/dist/cjs/core/contracts/context.d.ts +221 -0
- package/dist/cjs/core/contracts/context.js +15 -0
- package/dist/cjs/core/contracts/dialect.d.ts +154 -0
- package/dist/cjs/core/contracts/dialect.js +15 -0
- package/dist/cjs/core/contracts/events.d.ts +225 -0
- package/dist/cjs/core/contracts/events.js +15 -0
- package/dist/cjs/core/contracts/exporter.d.ts +13 -0
- package/dist/cjs/core/contracts/exporter.js +15 -0
- package/dist/cjs/core/contracts/extensions.d.ts +73 -0
- package/dist/cjs/core/contracts/extensions.js +15 -0
- package/dist/cjs/core/contracts/features.d.ts +25 -0
- package/dist/cjs/core/contracts/features.js +61 -0
- package/dist/cjs/core/contracts/fts.d.ts +66 -0
- package/dist/cjs/core/contracts/fts.js +15 -0
- package/dist/cjs/core/contracts/hooks.d.ts +481 -0
- package/dist/cjs/core/contracts/hooks.js +140 -0
- package/dist/cjs/core/contracts/index.d.ts +260 -0
- package/dist/cjs/core/contracts/index.js +41 -0
- package/dist/cjs/core/contracts/limitations.d.ts +46 -0
- package/dist/cjs/core/contracts/limitations.js +218 -0
- package/dist/cjs/core/contracts/performance.d.ts +308 -0
- package/dist/cjs/core/contracts/performance.js +144 -0
- package/dist/cjs/core/database.d.ts +220 -0
- package/dist/cjs/core/database.js +136 -0
- package/dist/cjs/core/portable.d.ts +23 -0
- package/dist/cjs/core/portable.js +66 -0
- package/dist/cjs/core/resolver.d.ts +77 -0
- package/dist/cjs/core/resolver.js +235 -0
- package/dist/cjs/dialects/PostgresDialect.d.ts +35 -0
- package/dist/cjs/dialects/PostgresDialect.js +143 -0
- package/dist/cjs/dialects/SqliteDialect.d.ts +35 -0
- package/dist/cjs/dialects/SqliteDialect.js +142 -0
- package/dist/cjs/dialects/shared.d.ts +17 -0
- package/dist/cjs/dialects/shared.js +90 -0
- package/dist/cjs/exporters/PostgresExporter.d.ts +14 -0
- package/dist/cjs/exporters/PostgresExporter.js +61 -0
- package/dist/cjs/exporters/SqliteFileExporter.d.ts +16 -0
- package/dist/cjs/exporters/SqliteFileExporter.js +43 -0
- package/dist/cjs/features/backup/cloudBackup.d.ts +219 -0
- package/dist/cjs/features/backup/cloudBackup.js +209 -0
- package/dist/cjs/features/kv/idb.d.ts +79 -0
- package/dist/cjs/features/kv/idb.js +32 -0
- package/dist/cjs/features/kv/index.d.ts +14 -0
- package/dist/cjs/features/kv/index.js +40 -0
- package/dist/cjs/features/kv/indexedDbKeyValueStore.d.ts +77 -0
- package/dist/cjs/features/kv/indexedDbKeyValueStore.js +116 -0
- package/dist/cjs/features/kv/keyValueStore.d.ts +48 -0
- package/dist/cjs/features/kv/keyValueStore.js +67 -0
- package/dist/cjs/features/kv/sqlKeyValueStore.d.ts +34 -0
- package/dist/cjs/features/kv/sqlKeyValueStore.js +81 -0
- package/dist/cjs/features/kv/valueCodec.d.ts +35 -0
- package/dist/cjs/features/kv/valueCodec.js +426 -0
- package/dist/cjs/features/migrations/dataExport.d.ts +77 -0
- package/dist/cjs/features/migrations/dataExport.js +194 -0
- package/dist/cjs/features/migrations/dataImport.d.ts +54 -0
- package/dist/cjs/features/migrations/dataImport.js +300 -0
- package/dist/cjs/features/migrations/migration.d.ts +89 -0
- package/dist/cjs/features/migrations/migration.js +187 -0
- package/dist/cjs/features/migrations/schemaMigrations.d.ts +93 -0
- package/dist/cjs/features/migrations/schemaMigrations.js +220 -0
- package/dist/cjs/features/sync/conflicts/conflictResolver.d.ts +222 -0
- package/dist/cjs/features/sync/conflicts/conflictResolver.js +366 -0
- package/dist/cjs/features/sync/conflicts/index.d.ts +9 -0
- package/dist/cjs/features/sync/conflicts/index.js +21 -0
- package/dist/cjs/features/sync/crossPlatformSync.d.ts +281 -0
- package/dist/cjs/features/sync/crossPlatformSync.js +551 -0
- package/dist/cjs/features/sync/devices/deviceManager.d.ts +243 -0
- package/dist/cjs/features/sync/devices/deviceManager.js +450 -0
- package/dist/cjs/features/sync/devices/index.d.ts +10 -0
- package/dist/cjs/features/sync/devices/index.js +21 -0
- package/dist/cjs/features/sync/index.d.ts +37 -0
- package/dist/cjs/features/sync/index.js +111 -0
- package/dist/cjs/features/sync/protocol/index.d.ts +11 -0
- package/dist/cjs/features/sync/protocol/index.js +23 -0
- package/dist/cjs/features/sync/protocol/messages.d.ts +348 -0
- package/dist/cjs/features/sync/protocol/messages.js +215 -0
- package/dist/cjs/features/sync/protocol/vectorClock.d.ts +164 -0
- package/dist/cjs/features/sync/protocol/vectorClock.js +256 -0
- package/dist/cjs/features/sync/syncManager.d.ts +348 -0
- package/dist/cjs/features/sync/syncManager.js +455 -0
- package/dist/cjs/features/sync/tables/index.d.ts +10 -0
- package/dist/cjs/features/sync/tables/index.js +21 -0
- package/dist/cjs/features/sync/tables/syncLogManager.d.ts +216 -0
- package/dist/cjs/features/sync/tables/syncLogManager.js +423 -0
- package/dist/cjs/features/sync/transport/httpTransport.d.ts +123 -0
- package/dist/cjs/features/sync/transport/httpTransport.js +346 -0
- package/dist/cjs/features/sync/transport/index.d.ts +12 -0
- package/dist/cjs/features/sync/transport/index.js +25 -0
- package/dist/cjs/features/sync/transport/transport.d.ts +259 -0
- package/dist/cjs/features/sync/transport/transport.js +163 -0
- package/dist/cjs/features/sync/transport/websocketTransport.d.ts +126 -0
- package/dist/cjs/features/sync/transport/websocketTransport.js +342 -0
- package/dist/cjs/fts/PostgresFts.d.ts +29 -0
- package/dist/cjs/fts/PostgresFts.js +77 -0
- package/dist/cjs/fts/SqliteFts5.d.ts +22 -0
- package/dist/cjs/fts/SqliteFts5.js +58 -0
- package/dist/cjs/index.d.ts +30 -0
- package/dist/cjs/index.js +101 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/shared/connectionGate.d.ts +94 -0
- package/dist/cjs/shared/connectionGate.js +77 -0
- package/dist/cjs/shared/errors.d.ts +23 -0
- package/dist/cjs/shared/errors.js +45 -0
- package/dist/cjs/shared/logger.d.ts +26 -0
- package/dist/cjs/shared/logger.js +40 -0
- package/dist/cjs/shared/parameterUtils.d.ts +16 -0
- package/dist/cjs/shared/parameterUtils.js +59 -0
- package/dist/cjs/shared/postgresTarget.d.ts +11 -0
- package/dist/cjs/shared/postgresTarget.js +30 -0
- package/dist/cjs/shared/segment.d.ts +43 -0
- package/dist/cjs/shared/segment.js +63 -0
- package/dist/cjs/shared/serializedTransaction.d.ts +14 -0
- package/dist/cjs/shared/serializedTransaction.js +74 -0
- package/dist/cjs/shared/splitSqlStatements.d.ts +27 -0
- package/dist/cjs/shared/splitSqlStatements.js +163 -0
- package/dist/cjs/shared/sqlPlaceholders.d.ts +48 -0
- package/dist/cjs/shared/sqlPlaceholders.js +263 -0
- package/dist/cjs/types/index.d.ts +29 -0
- package/dist/cjs/types/index.js +33 -0
- package/dist/core/portable.d.ts +23 -0
- package/dist/core/portable.d.ts.map +1 -0
- package/dist/core/portable.js +23 -0
- package/dist/core/portable.js.map +1 -0
- package/dist/features/migrations/schemaMigrations.d.ts +93 -0
- package/dist/features/migrations/schemaMigrations.d.ts.map +1 -0
- package/dist/features/migrations/schemaMigrations.js +216 -0
- package/dist/features/migrations/schemaMigrations.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +139 -24
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A versioned schema-migration runner over StorageAdapter, for PostgreSQL and
|
|
3
|
+
* SQLite alike. It applies hand-written SQL files in number order, each in its
|
|
4
|
+
* own transaction, records each in a ledger table with the SHA-256 of its text,
|
|
5
|
+
* refuses an applied file whose text changed, and stops at the first error.
|
|
6
|
+
*
|
|
7
|
+
* Locking: on PostgreSQL each file's transaction first takes a
|
|
8
|
+
* transaction-scoped advisory lock, and checks the ledger again under it, so
|
|
9
|
+
* two runners at once apply each file once. A file marked to run outside a
|
|
10
|
+
* transaction (CREATE INDEX CONCURRENTLY) runs under the session-level lock on
|
|
11
|
+
* one held connection. On SQLite the file's write transaction is the lock.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { StorageAdapter } from '../../core/contracts/index.js';
|
|
16
|
+
/** One migration: its file name, which orders it, and its SQL. */
|
|
17
|
+
export interface SchemaMigration {
|
|
18
|
+
name: string;
|
|
19
|
+
sql: string;
|
|
20
|
+
}
|
|
21
|
+
/** How the runner names its ledger and lock, and what it does with surprises. */
|
|
22
|
+
export interface SchemaMigrationOptions {
|
|
23
|
+
/** The ledger table, schema-qualified if need be. Default: `schema_migrations`. */
|
|
24
|
+
table?: string;
|
|
25
|
+
/** The ledger column that holds each file's SHA-256 in hex. Default: `checksum`. */
|
|
26
|
+
checksumColumn?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The PostgreSQL advisory lock key: an integer, or text hashed with
|
|
29
|
+
* `hashtext()`. Default: 724313571. Runners that share a database share a key.
|
|
30
|
+
*/
|
|
31
|
+
lockKey?: number | string;
|
|
32
|
+
/**
|
|
33
|
+
* A ledger row whose file the list lacks (the database is ahead of the
|
|
34
|
+
* code): `'refuse'` (the default) stops before applying anything; `'ignore'`
|
|
35
|
+
* goes on.
|
|
36
|
+
*/
|
|
37
|
+
unknown?: 'refuse' | 'ignore';
|
|
38
|
+
/**
|
|
39
|
+
* A file whose first line, trimmed, is this runs outside a transaction.
|
|
40
|
+
* Default: `-- outside transaction`.
|
|
41
|
+
*/
|
|
42
|
+
outsideTransactionMarker?: string;
|
|
43
|
+
}
|
|
44
|
+
/** What a run or a status check found. */
|
|
45
|
+
export interface SchemaMigrationReport {
|
|
46
|
+
/** Applied by this run, in order (a status check applies none, and lists the files the ledger holds unchanged). */
|
|
47
|
+
applied: string[];
|
|
48
|
+
/** Already in the ledger with the same checksum: a run skipped them. */
|
|
49
|
+
skipped: string[];
|
|
50
|
+
/** In the list and not in the ledger: a status check's pending files. */
|
|
51
|
+
pending: string[];
|
|
52
|
+
/** In the ledger with a different checksum. */
|
|
53
|
+
changed: string[];
|
|
54
|
+
/** In the ledger and not in the list. */
|
|
55
|
+
unknown: string[];
|
|
56
|
+
}
|
|
57
|
+
/** Options for reading a migrations folder. */
|
|
58
|
+
export interface ReadSchemaMigrationsOptions {
|
|
59
|
+
/** Reads `<dir>/<dialect>` when that folder exists, else `<dir>`. */
|
|
60
|
+
dialect?: 'postgres' | 'sqlite';
|
|
61
|
+
/**
|
|
62
|
+
* The file-name form, whose first capture group is the number that orders
|
|
63
|
+
* the file. Default: digits, an underscore, then lower-case letters, digits
|
|
64
|
+
* and underscores, `.sql` (`001_init.sql`, `0001_accounts.sql`).
|
|
65
|
+
*/
|
|
66
|
+
namePattern?: RegExp;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reads the migrations of a folder in number order: `<dir>/<dialect>` when
|
|
70
|
+
* that folder exists, else `<dir>`. Files not ending in `.sql` are left out; a
|
|
71
|
+
* `.sql` name out of form, or a number used twice, is refused. Node only.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readSchemaMigrations(dir: string, options?: ReadSchemaMigrationsOptions): Promise<SchemaMigration[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Compares the list with the ledger and applies nothing: `applied` lists the
|
|
76
|
+
* files the ledger holds unchanged, `pending` the files it lacks, `changed`
|
|
77
|
+
* the files whose text differs from what was applied, `unknown` the ledger
|
|
78
|
+
* rows the list lacks. Creates the ledger when it is missing.
|
|
79
|
+
*/
|
|
80
|
+
export declare function schemaMigrationStatus(adapter: StorageAdapter, migrations: SchemaMigration[], options?: SchemaMigrationOptions): Promise<SchemaMigrationReport>;
|
|
81
|
+
/**
|
|
82
|
+
* Applies every migration the ledger lacks, in list order, and reports what
|
|
83
|
+
* it did: `applied` (this run) and `skipped` (already applied). Refuses to
|
|
84
|
+
* start when an applied file's text changed, or (unless `unknown: 'ignore'`)
|
|
85
|
+
* when the ledger holds a file the list lacks. A file runs in its own
|
|
86
|
+
* transaction and holds no BEGIN or COMMIT; a file whose first line is the
|
|
87
|
+
* outside-transaction marker runs outside one (on PostgreSQL that needs
|
|
88
|
+
* `withConnection`, which PostgresAdapter has). Stops at the first error,
|
|
89
|
+
* after rolling that file back, with an error that names it and carries the
|
|
90
|
+
* database's error as `cause`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function runSchemaMigrations(adapter: StorageAdapter, migrations: SchemaMigration[], options?: SchemaMigrationOptions): Promise<SchemaMigrationReport>;
|
|
93
|
+
//# sourceMappingURL=schemaMigrations.d.ts.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var schemaMigrations_exports = {};
|
|
29
|
+
__export(schemaMigrations_exports, {
|
|
30
|
+
readSchemaMigrations: () => readSchemaMigrations,
|
|
31
|
+
runSchemaMigrations: () => runSchemaMigrations,
|
|
32
|
+
schemaMigrationStatus: () => schemaMigrationStatus
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(schemaMigrations_exports);
|
|
35
|
+
var import_features = require("../../core/contracts/features.js");
|
|
36
|
+
var import_logger = require("../../shared/logger.js");
|
|
37
|
+
const DEFAULT_TABLE = "schema_migrations";
|
|
38
|
+
const DEFAULT_CHECKSUM_COLUMN = "checksum";
|
|
39
|
+
const DEFAULT_LOCK_KEY = 724313571;
|
|
40
|
+
const DEFAULT_MARKER = "-- outside transaction";
|
|
41
|
+
const DEFAULT_NAME = /^(\d+)_[a-z0-9_]+\.sql$/;
|
|
42
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
43
|
+
const sha256Hex = async (text) => {
|
|
44
|
+
const subtle = globalThis.crypto?.subtle;
|
|
45
|
+
if (subtle) {
|
|
46
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
47
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
48
|
+
}
|
|
49
|
+
const { createHash } = await Promise.resolve().then(() => __toESM(require("crypto")));
|
|
50
|
+
return createHash("sha256").update(text).digest("hex");
|
|
51
|
+
};
|
|
52
|
+
const checkIdentifier = (what, value) => {
|
|
53
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) {
|
|
54
|
+
throw new Error(`${what} must be an SQL identifier (letters, digits and underscores, optionally schema-qualified); got ${JSON.stringify(value)}.`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
async function readSchemaMigrations(dir, options = {}) {
|
|
59
|
+
const { readdir, readFile, stat } = await Promise.resolve().then(() => __toESM(require("fs/promises")));
|
|
60
|
+
const path = await Promise.resolve().then(() => __toESM(require("path")));
|
|
61
|
+
const pattern = options.namePattern ?? DEFAULT_NAME;
|
|
62
|
+
let folder = dir;
|
|
63
|
+
if (options.dialect) {
|
|
64
|
+
const own = path.join(dir, options.dialect);
|
|
65
|
+
const isFolder = await stat(own).then((entry) => entry.isDirectory(), () => false);
|
|
66
|
+
if (isFolder)
|
|
67
|
+
folder = own;
|
|
68
|
+
}
|
|
69
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const name of await readdir(folder)) {
|
|
72
|
+
if (!name.endsWith(".sql"))
|
|
73
|
+
continue;
|
|
74
|
+
const number = pattern.exec(name)?.[1];
|
|
75
|
+
if (number === void 0) {
|
|
76
|
+
throw new Error(`${name}: a migration's name is digits, an underscore and a lower-case name, ending in .sql.`);
|
|
77
|
+
}
|
|
78
|
+
const key = String(Number(number));
|
|
79
|
+
if (numbers.has(key))
|
|
80
|
+
throw new Error(`${name}: the number ${number} is used twice (also ${numbers.get(key)}).`);
|
|
81
|
+
numbers.set(key, name);
|
|
82
|
+
entries.push({ name, number: Number(number) });
|
|
83
|
+
}
|
|
84
|
+
entries.sort((a, b) => a.number - b.number || a.name.localeCompare(b.name));
|
|
85
|
+
return Promise.all(entries.map(async ({ name }) => ({ name, sql: await readFile(path.join(folder, name), "utf8") })));
|
|
86
|
+
}
|
|
87
|
+
const resolve = (adapter, options) => {
|
|
88
|
+
const table = checkIdentifier("table", options.table ?? DEFAULT_TABLE);
|
|
89
|
+
const checksumColumn = checkIdentifier("checksumColumn", options.checksumColumn ?? DEFAULT_CHECKSUM_COLUMN);
|
|
90
|
+
const key = options.lockKey ?? DEFAULT_LOCK_KEY;
|
|
91
|
+
if (typeof key === "number" && !Number.isSafeInteger(key))
|
|
92
|
+
throw new Error(`lockKey must be a safe integer or text; got ${key}.`);
|
|
93
|
+
const dialect = (0, import_features.createStorageFeatures)(adapter).dialect.name;
|
|
94
|
+
const keyExpr = typeof key === "number" ? "?" : "hashtext(?)";
|
|
95
|
+
return {
|
|
96
|
+
table,
|
|
97
|
+
checksumColumn,
|
|
98
|
+
lock: dialect === "postgres" ? {
|
|
99
|
+
sql: `SELECT pg_advisory_xact_lock(${keyExpr})`,
|
|
100
|
+
session: `SELECT pg_advisory_lock(${keyExpr})`,
|
|
101
|
+
unlock: `SELECT pg_advisory_unlock(${keyExpr})`,
|
|
102
|
+
params: [key]
|
|
103
|
+
} : null,
|
|
104
|
+
unknown: options.unknown ?? "refuse",
|
|
105
|
+
marker: options.outsideTransactionMarker ?? DEFAULT_MARKER,
|
|
106
|
+
dialect
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const ledgerDdl = (r) => r.dialect === "postgres" ? `CREATE TABLE IF NOT EXISTS ${r.table} (name TEXT PRIMARY KEY, ${r.checksumColumn} TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())` : `CREATE TABLE IF NOT EXISTS ${r.table} (name TEXT PRIMARY KEY, ${r.checksumColumn} TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`;
|
|
110
|
+
const inLockedTransaction = (adapter, r, work) => adapter.transaction(async (trx) => {
|
|
111
|
+
if (r.lock)
|
|
112
|
+
await trx.get(r.lock.sql, r.lock.params);
|
|
113
|
+
return work(trx);
|
|
114
|
+
});
|
|
115
|
+
const ledgerRows = async (db, r) => {
|
|
116
|
+
const rows = await db.all(`SELECT name, ${r.checksumColumn} AS checksum FROM ${r.table}`);
|
|
117
|
+
return new Map(rows.map((row) => [row.name, row.checksum]));
|
|
118
|
+
};
|
|
119
|
+
const compare = (list, ledger) => {
|
|
120
|
+
const report = { applied: [], pending: [], changed: [], unknown: [] };
|
|
121
|
+
const carried = new Set(list.map((migration) => migration.name));
|
|
122
|
+
for (const migration of list) {
|
|
123
|
+
const recorded2 = ledger.get(migration.name);
|
|
124
|
+
if (recorded2 === void 0)
|
|
125
|
+
report.pending.push(migration.name);
|
|
126
|
+
else if (recorded2 !== migration.checksum)
|
|
127
|
+
report.changed.push(migration.name);
|
|
128
|
+
else
|
|
129
|
+
report.applied.push(migration.name);
|
|
130
|
+
}
|
|
131
|
+
report.unknown = [...ledger.keys()].filter((name) => !carried.has(name)).sort();
|
|
132
|
+
return report;
|
|
133
|
+
};
|
|
134
|
+
const withChecksums = (migrations) => Promise.all(migrations.map(async (migration) => ({ ...migration, checksum: await sha256Hex(migration.sql) })));
|
|
135
|
+
const ensureLedger = (adapter, r) => inLockedTransaction(adapter, r, async (trx) => {
|
|
136
|
+
await trx.exec(ledgerDdl(r));
|
|
137
|
+
});
|
|
138
|
+
async function schemaMigrationStatus(adapter, migrations, options = {}) {
|
|
139
|
+
const r = resolve(adapter, options);
|
|
140
|
+
await ensureLedger(adapter, r);
|
|
141
|
+
return { ...compare(await withChecksums(migrations), await ledgerRows(adapter, r)), skipped: [] };
|
|
142
|
+
}
|
|
143
|
+
async function runSchemaMigrations(adapter, migrations, options = {}) {
|
|
144
|
+
const r = resolve(adapter, options);
|
|
145
|
+
const list = await withChecksums(migrations);
|
|
146
|
+
await ensureLedger(adapter, r);
|
|
147
|
+
const before = compare(list, await ledgerRows(adapter, r));
|
|
148
|
+
if (before.changed.length) {
|
|
149
|
+
throw new Error(`Applied migrations differ from the files given: ${before.changed.join(", ")}. An applied migration never changes.`);
|
|
150
|
+
}
|
|
151
|
+
if (before.unknown.length && r.unknown === "refuse") {
|
|
152
|
+
throw new Error(`The database has applied migrations the list lacks: ${before.unknown.join(", ")}. Run the newer code, or pass unknown: 'ignore'.`);
|
|
153
|
+
}
|
|
154
|
+
const report = { applied: [], skipped: [...before.applied], pending: [], changed: [], unknown: before.unknown };
|
|
155
|
+
for (const migration of list) {
|
|
156
|
+
if (!before.pending.includes(migration.name))
|
|
157
|
+
continue;
|
|
158
|
+
let applied;
|
|
159
|
+
try {
|
|
160
|
+
applied = outsideTransaction(migration, r) ? await applyOutside(adapter, r, migration) : await applyInside(adapter, r, migration);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
const failure = new Error(`Migration ${migration.name} failed: ${message}`);
|
|
164
|
+
failure.cause = error;
|
|
165
|
+
throw failure;
|
|
166
|
+
}
|
|
167
|
+
(applied ? report.applied : report.skipped).push(migration.name);
|
|
168
|
+
if (applied)
|
|
169
|
+
(0, import_logger.getStorageLogger)().info({ migration: migration.name, table: r.table }, `Applied migration ${migration.name}.`);
|
|
170
|
+
}
|
|
171
|
+
return report;
|
|
172
|
+
}
|
|
173
|
+
const outsideTransaction = (migration, r) => migration.sql.split("\n", 1)[0]?.trim() === r.marker;
|
|
174
|
+
const recorded = async (db, r, name) => (await db.get(`SELECT ${r.checksumColumn} AS checksum FROM ${r.table} WHERE name = ?`, [name]))?.checksum;
|
|
175
|
+
const appliedAlready = (migration, checksum) => {
|
|
176
|
+
if (checksum === void 0)
|
|
177
|
+
return false;
|
|
178
|
+
if (checksum !== migration.checksum) {
|
|
179
|
+
throw new Error(`it was applied with checksum ${checksum} and now reads ${migration.checksum}`);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
};
|
|
183
|
+
const record = (db, r, migration) => db.run(`INSERT INTO ${r.table} (name, ${r.checksumColumn}) VALUES (?, ?)`, [migration.name, migration.checksum]);
|
|
184
|
+
const applyInside = (adapter, r, migration) => inLockedTransaction(adapter, r, async (trx) => {
|
|
185
|
+
if (appliedAlready(migration, await recorded(trx, r, migration.name)))
|
|
186
|
+
return false;
|
|
187
|
+
await trx.exec(migration.sql);
|
|
188
|
+
await record(trx, r, migration);
|
|
189
|
+
return true;
|
|
190
|
+
});
|
|
191
|
+
const applyOutside = async (adapter, r, migration) => {
|
|
192
|
+
const work = async (db) => {
|
|
193
|
+
if (appliedAlready(migration, await recorded(db, r, migration.name)))
|
|
194
|
+
return false;
|
|
195
|
+
await db.exec(migration.sql);
|
|
196
|
+
await record(db, r, migration);
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
if (!r.lock)
|
|
200
|
+
return work(adapter);
|
|
201
|
+
const held = adapter;
|
|
202
|
+
if (typeof held.withConnection !== "function") {
|
|
203
|
+
throw new Error("a file outside a transaction needs an adapter with withConnection() on PostgreSQL, to hold the lock on one session");
|
|
204
|
+
}
|
|
205
|
+
const { params, session, unlock } = r.lock;
|
|
206
|
+
return held.withConnection(async (connection) => {
|
|
207
|
+
await connection.get(session, params);
|
|
208
|
+
try {
|
|
209
|
+
return await work(connection);
|
|
210
|
+
} finally {
|
|
211
|
+
await connection.get(unlock, params);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
216
|
+
0 && (module.exports = {
|
|
217
|
+
readSchemaMigrations,
|
|
218
|
+
runSchemaMigrations,
|
|
219
|
+
schemaMigrationStatus
|
|
220
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Detection and Resolution.
|
|
3
|
+
*
|
|
4
|
+
* Provides conflict detection using vector clocks and
|
|
5
|
+
* configurable resolution strategies with UI hooks.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import type { VectorClockData } from '../protocol/vectorClock.js';
|
|
10
|
+
/**
|
|
11
|
+
* Conflict resolution strategy.
|
|
12
|
+
*/
|
|
13
|
+
export type ConflictStrategy = 'last-write-wins' | 'local-wins' | 'remote-wins' | 'merge' | 'manual';
|
|
14
|
+
/**
|
|
15
|
+
* Resolution decision for a conflict.
|
|
16
|
+
*/
|
|
17
|
+
export type ResolutionDecision = 'use_local' | 'use_remote' | 'use_merged' | 'keep_both' | 'defer';
|
|
18
|
+
/**
|
|
19
|
+
* Represents a sync conflict between local and remote data.
|
|
20
|
+
*/
|
|
21
|
+
export interface SyncConflict {
|
|
22
|
+
/** Unique conflict identifier */
|
|
23
|
+
conflictId: string;
|
|
24
|
+
/** Table where conflict occurred */
|
|
25
|
+
tableName: string;
|
|
26
|
+
/** Primary key of the conflicting record */
|
|
27
|
+
recordId: string;
|
|
28
|
+
/** Local version of the data */
|
|
29
|
+
localData: Record<string, unknown>;
|
|
30
|
+
/** Remote version of the data */
|
|
31
|
+
remoteData: Record<string, unknown>;
|
|
32
|
+
/** Local vector clock */
|
|
33
|
+
localClock: VectorClockData;
|
|
34
|
+
/** Remote vector clock */
|
|
35
|
+
remoteClock: VectorClockData;
|
|
36
|
+
/** Local device ID */
|
|
37
|
+
localDeviceId: string;
|
|
38
|
+
/** Remote device ID */
|
|
39
|
+
remoteDeviceId: string;
|
|
40
|
+
/** When the conflict was detected */
|
|
41
|
+
detectedAt: number;
|
|
42
|
+
/** Fields that differ between versions */
|
|
43
|
+
conflictingFields: string[];
|
|
44
|
+
/** Conflict status */
|
|
45
|
+
status: 'pending' | 'resolved' | 'deferred';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolution result after conflict is resolved.
|
|
49
|
+
*/
|
|
50
|
+
export interface ConflictResolution {
|
|
51
|
+
/** How the conflict was resolved */
|
|
52
|
+
decision: ResolutionDecision;
|
|
53
|
+
/** Merged data if decision is 'use_merged' */
|
|
54
|
+
mergedData?: Record<string, unknown>;
|
|
55
|
+
/** Merged vector clock */
|
|
56
|
+
mergedClock: VectorClockData;
|
|
57
|
+
/** Who/what resolved the conflict */
|
|
58
|
+
resolvedBy: 'auto' | 'user' | 'strategy';
|
|
59
|
+
/** Resolution timestamp */
|
|
60
|
+
resolvedAt: number;
|
|
61
|
+
/** Optional reason for the resolution */
|
|
62
|
+
reason?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* UI hooks for conflict resolution.
|
|
66
|
+
*/
|
|
67
|
+
export interface ConflictUIHooks {
|
|
68
|
+
/**
|
|
69
|
+
* Called when a conflict requires manual resolution.
|
|
70
|
+
* Return the resolution decision from the UI.
|
|
71
|
+
*/
|
|
72
|
+
onConflictNeedsResolution?: (conflict: SyncConflict) => Promise<ConflictResolution>;
|
|
73
|
+
/**
|
|
74
|
+
* Called when a conflict is detected (before auto-resolution).
|
|
75
|
+
*/
|
|
76
|
+
onConflictDetected?: (conflict: SyncConflict) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Called after a conflict is resolved.
|
|
79
|
+
*/
|
|
80
|
+
onConflictResolved?: (conflict: SyncConflict, resolution: ConflictResolution) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Called when auto-merge fails and manual intervention is needed.
|
|
83
|
+
*/
|
|
84
|
+
onMergeFailed?: (conflict: SyncConflict, error: Error) => void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Configuration for the conflict resolver.
|
|
88
|
+
*/
|
|
89
|
+
export interface ConflictResolverOptions {
|
|
90
|
+
/** Default resolution strategy */
|
|
91
|
+
defaultStrategy: ConflictStrategy;
|
|
92
|
+
/** Per-table strategy overrides */
|
|
93
|
+
tableStrategies?: Record<string, ConflictStrategy>;
|
|
94
|
+
/** Per-field merge functions for 'merge' strategy */
|
|
95
|
+
fieldMergers?: Record<string, FieldMerger>;
|
|
96
|
+
/** UI hooks for manual resolution */
|
|
97
|
+
hooks?: ConflictUIHooks;
|
|
98
|
+
/** Maximum time to wait for manual resolution (ms) */
|
|
99
|
+
manualResolutionTimeout?: number;
|
|
100
|
+
/** Auto-resolve conflicts older than this (ms) */
|
|
101
|
+
autoResolveAfter?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Function to merge a single field.
|
|
105
|
+
*/
|
|
106
|
+
export type FieldMerger = (fieldName: string, localValue: unknown, remoteValue: unknown, localTimestamp: number, remoteTimestamp: number) => unknown;
|
|
107
|
+
/**
|
|
108
|
+
* Built-in field mergers.
|
|
109
|
+
*/
|
|
110
|
+
export declare const FieldMergers: {
|
|
111
|
+
/**
|
|
112
|
+
* Use the most recent value based on timestamp.
|
|
113
|
+
*/
|
|
114
|
+
lastWriteWins: FieldMerger;
|
|
115
|
+
/**
|
|
116
|
+
* For numeric fields, take the maximum value.
|
|
117
|
+
*/
|
|
118
|
+
max: FieldMerger;
|
|
119
|
+
/**
|
|
120
|
+
* For numeric fields, sum the values.
|
|
121
|
+
*/
|
|
122
|
+
sum: FieldMerger;
|
|
123
|
+
/**
|
|
124
|
+
* For arrays, union the values.
|
|
125
|
+
*/
|
|
126
|
+
union: FieldMerger;
|
|
127
|
+
/**
|
|
128
|
+
* For strings, concatenate with separator.
|
|
129
|
+
*/
|
|
130
|
+
concat: FieldMerger;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Conflict resolver for cross-platform sync.
|
|
134
|
+
*
|
|
135
|
+
* Detects conflicts using vector clock comparison and resolves
|
|
136
|
+
* them using configurable strategies or UI hooks.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const resolver = new ConflictResolver({
|
|
141
|
+
* defaultStrategy: 'last-write-wins',
|
|
142
|
+
* tableStrategies: {
|
|
143
|
+
* notes: 'merge',
|
|
144
|
+
* settings: 'local-wins',
|
|
145
|
+
* },
|
|
146
|
+
* hooks: {
|
|
147
|
+
* onConflictNeedsResolution: async (conflict) => {
|
|
148
|
+
* return showConflictDialog(conflict);
|
|
149
|
+
* },
|
|
150
|
+
* },
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* const resolution = await resolver.resolve(conflict);
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export declare class ConflictResolver {
|
|
157
|
+
private _options;
|
|
158
|
+
private _pendingConflicts;
|
|
159
|
+
private _resolvedConflicts;
|
|
160
|
+
constructor(options: ConflictResolverOptions);
|
|
161
|
+
/**
|
|
162
|
+
* Detect if there is a conflict between local and remote data.
|
|
163
|
+
*/
|
|
164
|
+
detectConflict(tableName: string, recordId: string, localData: Record<string, unknown>, remoteData: Record<string, unknown>, localClock: VectorClockData, remoteClock: VectorClockData, localDeviceId: string, remoteDeviceId: string): SyncConflict | null;
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a conflict using the configured strategy.
|
|
167
|
+
*/
|
|
168
|
+
resolve(conflict: SyncConflict): Promise<ConflictResolution>;
|
|
169
|
+
/**
|
|
170
|
+
* Get pending conflicts.
|
|
171
|
+
*/
|
|
172
|
+
getPendingConflicts(): SyncConflict[];
|
|
173
|
+
/**
|
|
174
|
+
* Get a specific pending conflict.
|
|
175
|
+
*/
|
|
176
|
+
getConflict(conflictId: string): SyncConflict | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* Manually resolve a conflict with a user decision.
|
|
179
|
+
*/
|
|
180
|
+
resolveManually(conflictId: string, decision: ResolutionDecision, mergedData?: Record<string, unknown>): Promise<ConflictResolution>;
|
|
181
|
+
/**
|
|
182
|
+
* Auto-resolve old conflicts that have been pending too long.
|
|
183
|
+
*/
|
|
184
|
+
autoResolveStale(): ConflictResolution[];
|
|
185
|
+
/**
|
|
186
|
+
* Get the resolution strategy for a table.
|
|
187
|
+
*/
|
|
188
|
+
private _getStrategy;
|
|
189
|
+
/**
|
|
190
|
+
* Find fields that differ between local and remote data.
|
|
191
|
+
*/
|
|
192
|
+
private _findConflictingFields;
|
|
193
|
+
/**
|
|
194
|
+
* Check if two values are equal (deep comparison for objects/arrays).
|
|
195
|
+
*/
|
|
196
|
+
private _valuesEqual;
|
|
197
|
+
/**
|
|
198
|
+
* Resolve using last-write-wins strategy.
|
|
199
|
+
*/
|
|
200
|
+
private _resolveLastWriteWins;
|
|
201
|
+
/**
|
|
202
|
+
* Resolve using local-wins strategy.
|
|
203
|
+
*/
|
|
204
|
+
private _resolveLocalWins;
|
|
205
|
+
/**
|
|
206
|
+
* Resolve using remote-wins strategy.
|
|
207
|
+
*/
|
|
208
|
+
private _resolveRemoteWins;
|
|
209
|
+
/**
|
|
210
|
+
* Resolve using field-level merge strategy.
|
|
211
|
+
*/
|
|
212
|
+
private _resolveMerge;
|
|
213
|
+
/**
|
|
214
|
+
* Resolve using manual/UI strategy.
|
|
215
|
+
*/
|
|
216
|
+
private _resolveManual;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Create a conflict resolver instance.
|
|
220
|
+
*/
|
|
221
|
+
export declare const createConflictResolver: (options: ConflictResolverOptions) => ConflictResolver;
|
|
222
|
+
//# sourceMappingURL=conflictResolver.d.ts.map
|