@chronicle.app/etl-sqlite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/SqliteExtractor.d.ts +28 -0
- package/dist/SqliteExtractor.js +25 -0
- package/dist/appleEpoch.d.ts +38 -0
- package/dist/appleEpoch.js +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/timeRange.d.ts +32 -0
- package/dist/timeRange.js +26 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Louis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @chronicle.app/etl-sqlite
|
|
2
|
+
|
|
3
|
+
SQLite extraction with built-in `node:sqlite` on Node >=22.13.0.
|
|
4
|
+
|
|
5
|
+
Extend `SqliteExtractor`, declare `source` and `strategy`, and implement `extract()` using the protected `db` connection. Pass an existing database path as `input`. Always call `teardown()` in a `finally` block, including after setup or extraction fails.
|
|
6
|
+
|
|
7
|
+
The connection is read-only: missing databases are not created, and journal mode is not changed. Live WAL databases require access to the database and its sidecar files; a copied database must include uncheckpointed WAL contents or be a consistent SQLite backup.
|
|
8
|
+
|
|
9
|
+
`timeRangeConditions(column, range, options)` returns parameterized SQL conditions and values. The column is a trusted developer-supplied SQL identifier, not user input. Bounds are exclusive by default; conversion functions and inclusive operators are supported. Numeric dates are Unix milliseconds; the default SQL values are Unix seconds.
|
|
10
|
+
|
|
11
|
+
Apple epoch helpers retain the existing API: iOS inputs/outputs use nanoseconds since 2001, Safari inputs/outputs use seconds since 2001. `iosToUnixTimestamp` returns Unix milliseconds; other Unix arguments/results use seconds.
|
|
12
|
+
|
|
13
|
+
Node SQLite returns large INTEGER values only when `statement.setReadBigInts(true)` is enabled. Use that for source nanosecond timestamps and convert deliberately at the application boundary.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type Delivery, Extractor } from '@chronicle.app/etl';
|
|
4
|
+
/** Read an existing source database without changing its journal mode. */
|
|
5
|
+
export declare abstract class SqliteExtractor<SelfClass extends typeof SqliteExtractor = typeof SqliteExtractor> extends Extractor<SelfClass> {
|
|
6
|
+
static delivery: Delivery;
|
|
7
|
+
static schema: z.ZodObject<{
|
|
8
|
+
since: z.ZodOptional<z.ZodDate>;
|
|
9
|
+
until: z.ZodOptional<z.ZodDate>;
|
|
10
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
} & {
|
|
12
|
+
input: z.ZodString;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
input: string;
|
|
15
|
+
since?: Date | undefined;
|
|
16
|
+
until?: Date | undefined;
|
|
17
|
+
limit?: number | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
input: string;
|
|
20
|
+
since?: Date | undefined;
|
|
21
|
+
until?: Date | undefined;
|
|
22
|
+
limit?: number | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
protected db: DatabaseSync | null;
|
|
25
|
+
setup(): Promise<void>;
|
|
26
|
+
teardown(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=SqliteExtractor.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Extractor } from '@chronicle.app/etl';
|
|
4
|
+
/** Read an existing source database without changing its journal mode. */
|
|
5
|
+
export class SqliteExtractor extends Extractor {
|
|
6
|
+
static delivery = 'local';
|
|
7
|
+
static schema = Extractor.schema.extend({
|
|
8
|
+
input: z.string().min(1).describe('Path to an existing SQLite database'),
|
|
9
|
+
});
|
|
10
|
+
db = null;
|
|
11
|
+
async setup() {
|
|
12
|
+
if (this.db)
|
|
13
|
+
throw new Error('Database already initialized');
|
|
14
|
+
await super.setup();
|
|
15
|
+
const { input } = this.config;
|
|
16
|
+
this.db = new DatabaseSync(input, { readOnly: true });
|
|
17
|
+
}
|
|
18
|
+
async teardown() {
|
|
19
|
+
if (this.db) {
|
|
20
|
+
this.db.close();
|
|
21
|
+
this.db = null;
|
|
22
|
+
}
|
|
23
|
+
await super.teardown();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple epoch timestamp utilities.
|
|
3
|
+
*
|
|
4
|
+
* Apple's Core Data / Core Foundation timestamps count from
|
|
5
|
+
* January 1, 2001 00:00:00 UTC; Unix timestamps count from
|
|
6
|
+
* January 1, 1970 00:00:00 UTC. The difference is 978,307,200 seconds.
|
|
7
|
+
*
|
|
8
|
+
* Two unit families appear in Apple SQLite stores:
|
|
9
|
+
* - seconds since 2001 (Safari History.db, WhatsApp ChatStorage.sqlite,
|
|
10
|
+
* CallHistory.storedata, Podcasts MTLibrary.sqlite)
|
|
11
|
+
* - nanoseconds since 2001 (Messages chat.db)
|
|
12
|
+
*/
|
|
13
|
+
export declare const APPLE_EPOCH_OFFSET_SECONDS = 978307200;
|
|
14
|
+
/**
|
|
15
|
+
* Convert an iOS timestamp to a Unix timestamp
|
|
16
|
+
* @param iosTimestamp iOS timestamp (nanoseconds since Jan 1, 2001)
|
|
17
|
+
* @returns Unix timestamp in milliseconds
|
|
18
|
+
*/
|
|
19
|
+
export declare function iosToUnixTimestamp(iosTimestamp: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Convert a Unix timestamp to an iOS timestamp
|
|
22
|
+
* @param unixTimestamp Unix timestamp in seconds
|
|
23
|
+
* @returns iOS timestamp in nanoseconds
|
|
24
|
+
*/
|
|
25
|
+
export declare function unixToIosTimestamp(unixTimestamp: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Convert a Safari timestamp (seconds since 2001) to a Unix timestamp
|
|
28
|
+
* @param safariTimestamp Safari timestamp in seconds since Jan 1, 2001
|
|
29
|
+
* @returns Unix timestamp in seconds
|
|
30
|
+
*/
|
|
31
|
+
export declare function safariToUnixTimestamp(safariTimestamp: number): number;
|
|
32
|
+
/**
|
|
33
|
+
* Convert a Unix timestamp to a Safari timestamp
|
|
34
|
+
* @param unixTimestamp Unix timestamp in seconds
|
|
35
|
+
* @returns Safari timestamp in seconds since Jan 1, 2001
|
|
36
|
+
*/
|
|
37
|
+
export declare function unixToSafariTimestamp(unixTimestamp: number): number;
|
|
38
|
+
//# sourceMappingURL=appleEpoch.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple epoch timestamp utilities.
|
|
3
|
+
*
|
|
4
|
+
* Apple's Core Data / Core Foundation timestamps count from
|
|
5
|
+
* January 1, 2001 00:00:00 UTC; Unix timestamps count from
|
|
6
|
+
* January 1, 1970 00:00:00 UTC. The difference is 978,307,200 seconds.
|
|
7
|
+
*
|
|
8
|
+
* Two unit families appear in Apple SQLite stores:
|
|
9
|
+
* - seconds since 2001 (Safari History.db, WhatsApp ChatStorage.sqlite,
|
|
10
|
+
* CallHistory.storedata, Podcasts MTLibrary.sqlite)
|
|
11
|
+
* - nanoseconds since 2001 (Messages chat.db)
|
|
12
|
+
*/
|
|
13
|
+
export const APPLE_EPOCH_OFFSET_SECONDS = 978_307_200;
|
|
14
|
+
/**
|
|
15
|
+
* Convert an iOS timestamp to a Unix timestamp
|
|
16
|
+
* @param iosTimestamp iOS timestamp (nanoseconds since Jan 1, 2001)
|
|
17
|
+
* @returns Unix timestamp in milliseconds
|
|
18
|
+
*/
|
|
19
|
+
export function iosToUnixTimestamp(iosTimestamp) {
|
|
20
|
+
return (iosTimestamp / 1_000_000_000 + APPLE_EPOCH_OFFSET_SECONDS) * 1000;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert a Unix timestamp to an iOS timestamp
|
|
24
|
+
* @param unixTimestamp Unix timestamp in seconds
|
|
25
|
+
* @returns iOS timestamp in nanoseconds
|
|
26
|
+
*/
|
|
27
|
+
export function unixToIosTimestamp(unixTimestamp) {
|
|
28
|
+
return (unixTimestamp - APPLE_EPOCH_OFFSET_SECONDS) * 1_000_000_000;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Convert a Safari timestamp (seconds since 2001) to a Unix timestamp
|
|
32
|
+
* @param safariTimestamp Safari timestamp in seconds since Jan 1, 2001
|
|
33
|
+
* @returns Unix timestamp in seconds
|
|
34
|
+
*/
|
|
35
|
+
export function safariToUnixTimestamp(safariTimestamp) {
|
|
36
|
+
return safariTimestamp + APPLE_EPOCH_OFFSET_SECONDS;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert a Unix timestamp to a Safari timestamp
|
|
40
|
+
* @param unixTimestamp Unix timestamp in seconds
|
|
41
|
+
* @returns Safari timestamp in seconds since Jan 1, 2001
|
|
42
|
+
*/
|
|
43
|
+
export function unixToSafariTimestamp(unixTimestamp) {
|
|
44
|
+
return unixTimestamp - APPLE_EPOCH_OFFSET_SECONDS;
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A since/until window, as it appears on every extractor config. Values are
|
|
3
|
+
* normally Dates (the extractor schema coerces them) but strings/numbers are
|
|
4
|
+
* accepted and parsed.
|
|
5
|
+
*/
|
|
6
|
+
export interface TimeRange {
|
|
7
|
+
since?: Date | string | number | null;
|
|
8
|
+
until?: Date | string | number | null;
|
|
9
|
+
}
|
|
10
|
+
export interface TimeRangeOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Map a bound to the column's native representation. Defaults to Unix
|
|
13
|
+
* seconds. Pass an Apple-epoch conversion for Core Data columns
|
|
14
|
+
* (e.g. `(d) => unixToSafariTimestamp(d.getTime() / 1000)`), or a string
|
|
15
|
+
* formatter for text datetime columns.
|
|
16
|
+
*/
|
|
17
|
+
convert?: (date: Date) => number | string;
|
|
18
|
+
/** Comparator for the since bound (default `>`). */
|
|
19
|
+
sinceOp?: '>' | '>=';
|
|
20
|
+
/** Comparator for the until bound (default `<`). */
|
|
21
|
+
untilOp?: '<' | '<=';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build parameterized WHERE conditions for a since/until window over a raw SQL
|
|
25
|
+
* query. Returns one condition string (`"col > ?"`) plus its value per bound
|
|
26
|
+
* that is set; both empty when the range is unbounded.
|
|
27
|
+
*/
|
|
28
|
+
export declare function timeRangeConditions(column: string, range: TimeRange, options?: TimeRangeOptions): {
|
|
29
|
+
conditions: string[];
|
|
30
|
+
values: Array<number | string>;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=timeRange.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const defaultConvert = (date) => date.getTime() / 1000;
|
|
2
|
+
const asDate = (value) => {
|
|
3
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
4
|
+
if (!Number.isFinite(date.getTime()))
|
|
5
|
+
throw new Error('Invalid time range date');
|
|
6
|
+
return date;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Build parameterized WHERE conditions for a since/until window over a raw SQL
|
|
10
|
+
* query. Returns one condition string (`"col > ?"`) plus its value per bound
|
|
11
|
+
* that is set; both empty when the range is unbounded.
|
|
12
|
+
*/
|
|
13
|
+
export function timeRangeConditions(column, range, options = {}) {
|
|
14
|
+
const { convert = defaultConvert, sinceOp = '>', untilOp = '<' } = options;
|
|
15
|
+
const conditions = [];
|
|
16
|
+
const values = [];
|
|
17
|
+
if (range.since !== undefined && range.since !== null) {
|
|
18
|
+
conditions.push(`${column} ${sinceOp} ?`);
|
|
19
|
+
values.push(convert(asDate(range.since)));
|
|
20
|
+
}
|
|
21
|
+
if (range.until !== undefined && range.until !== null) {
|
|
22
|
+
conditions.push(`${column} ${untilOp} ?`);
|
|
23
|
+
values.push(convert(asDate(range.until)));
|
|
24
|
+
}
|
|
25
|
+
return { conditions, values };
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chronicle.app/etl-sqlite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/**/*.js",
|
|
8
|
+
"dist/**/*.d.ts"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -b tsconfig.lib.json",
|
|
12
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
13
|
+
"lint": "eslint src --ext .ts,.js",
|
|
14
|
+
"test": "node --test src/*.test.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "Andrew Louis",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"description": "Read-only SQLite extraction using built-in node:sqlite",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@chronicle.app/tsconfig": "0.1.0",
|
|
23
|
+
"@types/node": "^22.10.1",
|
|
24
|
+
"typescript": "^5.7.2"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://chronicle.app",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/chronicle-app/chronicle.git",
|
|
30
|
+
"directory": "core/etl-sqlite"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@chronicle.app/etl": "0.1.0",
|
|
38
|
+
"zod": "^3.24.1"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.13.0"
|
|
42
|
+
},
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|