@hithink-tech/hithink-finance-cli 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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/SECURITY.md +3 -0
- package/dist/application/config.d.ts +44 -0
- package/dist/application/config.js +111 -0
- package/dist/application/ports/auth-provider.d.ts +23 -0
- package/dist/application/ports/auth-provider.js +2 -0
- package/dist/application/source-policy.d.ts +13 -0
- package/dist/application/source-policy.js +31 -0
- package/dist/application/use-cases/data-clean.d.ts +2 -0
- package/dist/application/use-cases/data-clean.js +18 -0
- package/dist/application/use-cases/data-init.d.ts +4 -0
- package/dist/application/use-cases/data-init.js +9 -0
- package/dist/application/use-cases/data-remove.d.ts +2 -0
- package/dist/application/use-cases/data-remove.js +27 -0
- package/dist/application/use-cases/data-repair.d.ts +5 -0
- package/dist/application/use-cases/data-repair.js +5 -0
- package/dist/application/use-cases/data-sync.d.ts +25 -0
- package/dist/application/use-cases/data-sync.js +55 -0
- package/dist/application/use-cases/local-query.d.ts +4 -0
- package/dist/application/use-cases/local-query.js +88 -0
- package/dist/application/use-cases/market-history.d.ts +8 -0
- package/dist/application/use-cases/market-history.js +16 -0
- package/dist/application/use-cases/market-panel.d.ts +3 -0
- package/dist/application/use-cases/market-panel.js +5 -0
- package/dist/application/use-cases/remote-query.d.ts +4 -0
- package/dist/application/use-cases/remote-query.js +66 -0
- package/dist/cli/context.d.ts +17 -0
- package/dist/cli/context.js +52 -0
- package/dist/cli/i18n.d.ts +23 -0
- package/dist/cli/i18n.js +150 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.js +80 -0
- package/dist/cli/program.d.ts +20 -0
- package/dist/cli/program.js +111 -0
- package/dist/commands/auth/index.d.ts +10 -0
- package/dist/commands/auth/index.js +131 -0
- package/dist/commands/config/index.d.ts +13 -0
- package/dist/commands/config/index.js +26 -0
- package/dist/commands/data/index.d.ts +10 -0
- package/dist/commands/data/index.js +185 -0
- package/dist/commands/db/index.d.ts +4 -0
- package/dist/commands/db/index.js +72 -0
- package/dist/commands/financials/index.d.ts +5 -0
- package/dist/commands/financials/index.js +6 -0
- package/dist/commands/index/index.d.ts +5 -0
- package/dist/commands/index/index.js +6 -0
- package/dist/commands/market/index.d.ts +5 -0
- package/dist/commands/market/index.js +6 -0
- package/dist/commands/market/local.d.ts +4 -0
- package/dist/commands/market/local.js +97 -0
- package/dist/commands/remote.d.ts +11 -0
- package/dist/commands/remote.js +195 -0
- package/dist/commands/skills/index.d.ts +4 -0
- package/dist/commands/skills/index.js +43 -0
- package/dist/commands/special/index.d.ts +5 -0
- package/dist/commands/special/index.js +6 -0
- package/dist/commands/symbol/index.d.ts +5 -0
- package/dist/commands/symbol/index.js +6 -0
- package/dist/commands/system/capabilities.d.ts +4 -0
- package/dist/commands/system/capabilities.js +24 -0
- package/dist/commands/system/doctor.d.ts +4 -0
- package/dist/commands/system/doctor.js +15 -0
- package/dist/commands/system/schema.d.ts +4 -0
- package/dist/commands/system/schema.js +47 -0
- package/dist/commands/system/uninstall.d.ts +11 -0
- package/dist/commands/system/uninstall.js +68 -0
- package/dist/commands/system/update.d.ts +11 -0
- package/dist/commands/system/update.js +72 -0
- package/dist/contracts/envelope.d.ts +37 -0
- package/dist/contracts/envelope.js +36 -0
- package/dist/contracts/errors.d.ts +23 -0
- package/dist/contracts/errors.js +33 -0
- package/dist/contracts/local-capabilities.d.ts +13 -0
- package/dist/contracts/local-capabilities.js +37 -0
- package/dist/contracts/registry.d.ts +9 -0
- package/dist/contracts/registry.js +4 -0
- package/dist/contracts/remote-capabilities.d.ts +24 -0
- package/dist/contracts/remote-capabilities.js +667 -0
- package/dist/domains/financials/types.d.ts +6 -0
- package/dist/domains/financials/types.js +2 -0
- package/dist/domains/index/types.d.ts +6 -0
- package/dist/domains/index/types.js +2 -0
- package/dist/domains/market/types.d.ts +6 -0
- package/dist/domains/market/types.js +2 -0
- package/dist/domains/special/types.d.ts +5 -0
- package/dist/domains/special/types.js +2 -0
- package/dist/domains/symbol/types.d.ts +6 -0
- package/dist/domains/symbol/types.js +2 -0
- package/dist/infrastructure/credentials/api-key-provider.d.ts +14 -0
- package/dist/infrastructure/credentials/api-key-provider.js +102 -0
- package/dist/infrastructure/credentials/keyring.d.ts +15 -0
- package/dist/infrastructure/credentials/keyring.js +21 -0
- package/dist/infrastructure/credentials/redact.d.ts +3 -0
- package/dist/infrastructure/credentials/redact.js +26 -0
- package/dist/infrastructure/duckdb/connection.d.ts +8 -0
- package/dist/infrastructure/duckdb/connection.js +18 -0
- package/dist/infrastructure/duckdb/dump-client.d.ts +20 -0
- package/dist/infrastructure/duckdb/dump-client.js +66 -0
- package/dist/infrastructure/duckdb/factors.d.ts +3 -0
- package/dist/infrastructure/duckdb/factors.js +83 -0
- package/dist/infrastructure/duckdb/importer.d.ts +11 -0
- package/dist/infrastructure/duckdb/importer.js +105 -0
- package/dist/infrastructure/duckdb/migrations.d.ts +17 -0
- package/dist/infrastructure/duckdb/migrations.js +92 -0
- package/dist/infrastructure/duckdb/quality.d.ts +12 -0
- package/dist/infrastructure/duckdb/quality.js +56 -0
- package/dist/infrastructure/duckdb/schema.d.ts +4 -0
- package/dist/infrastructure/duckdb/schema.js +14 -0
- package/dist/infrastructure/filesystem/atomic-file.d.ts +2 -0
- package/dist/infrastructure/filesystem/atomic-file.js +38 -0
- package/dist/infrastructure/filesystem/platform-paths.d.ts +15 -0
- package/dist/infrastructure/filesystem/platform-paths.js +42 -0
- package/dist/infrastructure/filesystem/process-lock.d.ts +6 -0
- package/dist/infrastructure/filesystem/process-lock.js +56 -0
- package/dist/infrastructure/filesystem/stdin.d.ts +5 -0
- package/dist/infrastructure/filesystem/stdin.js +9 -0
- package/dist/infrastructure/fuyao/client.d.ts +31 -0
- package/dist/infrastructure/fuyao/client.js +145 -0
- package/dist/infrastructure/fuyao/envelope.d.ts +9 -0
- package/dist/infrastructure/fuyao/envelope.js +8 -0
- package/dist/infrastructure/fuyao/pagination.d.ts +16 -0
- package/dist/infrastructure/fuyao/pagination.js +44 -0
- package/dist/infrastructure/fuyao/retry.d.ts +5 -0
- package/dist/infrastructure/fuyao/retry.js +18 -0
- package/dist/infrastructure/fuyao/windowing.d.ts +8 -0
- package/dist/infrastructure/fuyao/windowing.js +25 -0
- package/dist/infrastructure/skills/drift.d.ts +3 -0
- package/dist/infrastructure/skills/drift.js +8 -0
- package/dist/infrastructure/skills/installer.d.ts +13 -0
- package/dist/infrastructure/skills/installer.js +48 -0
- package/dist/infrastructure/skills/manifest.d.ts +11 -0
- package/dist/infrastructure/skills/manifest.js +62 -0
- package/dist/infrastructure/updater/cache.d.ts +15 -0
- package/dist/infrastructure/updater/cache.js +76 -0
- package/dist/infrastructure/updater/check.d.ts +13 -0
- package/dist/infrastructure/updater/check.js +51 -0
- package/dist/infrastructure/updater/install.d.ts +4 -0
- package/dist/infrastructure/updater/install.js +39 -0
- package/dist/output/json.d.ts +3 -0
- package/dist/output/json.js +16 -0
- package/dist/output/renderer.d.ts +4 -0
- package/dist/output/renderer.js +47 -0
- package/dist/output/table.d.ts +2 -0
- package/dist/output/table.js +31 -0
- package/migrations/001-initial.sql +95 -0
- package/migrations/manifest.json +12 -0
- package/package.json +66 -0
- package/schemas/capabilities.json +290 -0
- package/schemas/command-envelope.schema.json +12 -0
- package/scripts/postinstall.mjs +27 -0
- package/scripts/update-check.mjs +23 -0
- package/skills/hithink-finance-data/SKILL.md +74 -0
- package/skills/hithink-finance-data/references/data-clean.md +33 -0
- package/skills/hithink-finance-data/references/data-init.md +35 -0
- package/skills/hithink-finance-data/references/data-migrate.md +33 -0
- package/skills/hithink-finance-data/references/data-remove.md +33 -0
- package/skills/hithink-finance-data/references/data-repair.md +33 -0
- package/skills/hithink-finance-data/references/data-status.md +33 -0
- package/skills/hithink-finance-data/references/data-sync.md +34 -0
- package/skills/hithink-finance-data/references/data-validate.md +33 -0
- package/skills/hithink-finance-data/references/db-describe.md +33 -0
- package/skills/hithink-finance-data/references/db-export.md +33 -0
- package/skills/hithink-finance-data/references/db-query.md +33 -0
- package/skills/hithink-finance-financials/SKILL.md +59 -0
- package/skills/hithink-finance-financials/references/financials-balance-sheet.md +40 -0
- package/skills/hithink-finance-financials/references/financials-cash-flow.md +40 -0
- package/skills/hithink-finance-financials/references/financials-income.md +40 -0
- package/skills/hithink-finance-financials/references/financials-indicators.md +37 -0
- package/skills/hithink-finance-index/SKILL.md +59 -0
- package/skills/hithink-finance-index/references/index-catalog.md +36 -0
- package/skills/hithink-finance-index/references/index-constituents.md +36 -0
- package/skills/hithink-finance-index/references/index-history.md +38 -0
- package/skills/hithink-finance-index/references/index-snapshot.md +37 -0
- package/skills/hithink-finance-market/SKILL.md +63 -0
- package/skills/hithink-finance-market/references/market-adjustment-factors.md +33 -0
- package/skills/hithink-finance-market/references/market-calendar.md +35 -0
- package/skills/hithink-finance-market/references/market-corporate-actions.md +38 -0
- package/skills/hithink-finance-market/references/market-history.md +39 -0
- package/skills/hithink-finance-market/references/market-panel.md +33 -0
- package/skills/hithink-finance-market/references/market-snapshot.md +39 -0
- package/skills/hithink-finance-research/SKILL.md +58 -0
- package/skills/hithink-finance-research/references/research-workflow.md +34 -0
- package/skills/hithink-finance-shared/SKILL.md +55 -0
- package/skills/hithink-finance-shared/references/auth-and-config.md +31 -0
- package/skills/hithink-finance-shared/references/global-rules.md +21 -0
- package/skills/hithink-finance-shared/references/lifecycle.md +23 -0
- package/skills/hithink-finance-shared/references/skills-management.md +22 -0
- package/skills/hithink-finance-special-data/SKILL.md +68 -0
- package/skills/hithink-finance-special-data/references/special-anomaly-list.md +36 -0
- package/skills/hithink-finance-special-data/references/special-anomaly-stock.md +37 -0
- package/skills/hithink-finance-special-data/references/special-dragon-tiger.md +37 -0
- package/skills/hithink-finance-special-data/references/special-hot-stock-history.md +36 -0
- package/skills/hithink-finance-special-data/references/special-hot-stock-trend.md +38 -0
- package/skills/hithink-finance-special-data/references/special-hot-stock.md +36 -0
- package/skills/hithink-finance-special-data/references/special-limit-up-ladder.md +35 -0
- package/skills/hithink-finance-special-data/references/special-limit-up-pool.md +40 -0
- package/skills/hithink-finance-special-data/references/special-skyrocket.md +36 -0
- package/skills/hithink-finance-symbol/SKILL.md +56 -0
- package/skills/hithink-finance-symbol/references/symbol-list.md +39 -0
- package/skills/hithink-finance-symbol/references/symbol-search.md +39 -0
- package/skills/manifest.json +55 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthProvider, AuthSession, AuthStatus, LoginInput } from '../../application/ports/auth-provider.js';
|
|
2
|
+
import type { CredentialStore } from './keyring.js';
|
|
3
|
+
export declare class ApiKeyAuthProvider implements AuthProvider {
|
|
4
|
+
private readonly store;
|
|
5
|
+
private readonly env;
|
|
6
|
+
readonly method = "api-key";
|
|
7
|
+
constructor(store: CredentialStore, env?: NodeJS.ProcessEnv);
|
|
8
|
+
resolve(profile: string, explicit?: string): Promise<AuthSession>;
|
|
9
|
+
login(input: LoginInput): Promise<AuthStatus>;
|
|
10
|
+
logout(profile: string): Promise<void>;
|
|
11
|
+
logoutAll(): Promise<void>;
|
|
12
|
+
status(profile: string): Promise<AuthStatus>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=api-key-provider.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CliError } from '../../contracts/errors.js';
|
|
2
|
+
const PROFILE_PREFIX = 'profile:';
|
|
3
|
+
const API_KEY_RECOVERY_HINT = 'Get an API key at https://fuyao.aicubes.cn/admin, then run `hithink-finance auth login`. For non-interactive use, pipe it to `hithink-finance auth login --api-key-stdin`, or set HITHINK_FINANCE_API_KEY for the current process.';
|
|
4
|
+
function account(profile) {
|
|
5
|
+
return `${PROFILE_PREFIX}${profile}`;
|
|
6
|
+
}
|
|
7
|
+
function credentialStoreError() {
|
|
8
|
+
return new CliError({
|
|
9
|
+
code: 'AUTH_CREDENTIAL_STORE_UNAVAILABLE',
|
|
10
|
+
category: 'authentication',
|
|
11
|
+
message: 'The system credential store is unavailable.',
|
|
12
|
+
hint: process.platform === 'linux'
|
|
13
|
+
? `Start a Secret Service provider, or use HITHINK_FINANCE_API_KEY for this process. ${API_KEY_RECOVERY_HINT}`
|
|
14
|
+
: `Unlock the system credential store, or use HITHINK_FINANCE_API_KEY for this process. ${API_KEY_RECOVERY_HINT}`,
|
|
15
|
+
retryable: false,
|
|
16
|
+
exitCode: 3,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export class ApiKeyAuthProvider {
|
|
20
|
+
store;
|
|
21
|
+
env;
|
|
22
|
+
method = 'api-key';
|
|
23
|
+
constructor(store, env = process.env) {
|
|
24
|
+
this.store = store;
|
|
25
|
+
this.env = env;
|
|
26
|
+
}
|
|
27
|
+
async resolve(profile, explicit) {
|
|
28
|
+
if (explicit !== undefined && explicit.length > 0) {
|
|
29
|
+
return { method: 'api-key', profile, apiKey: explicit, source: 'explicit' };
|
|
30
|
+
}
|
|
31
|
+
const environmentKey = this.env.HITHINK_FINANCE_API_KEY;
|
|
32
|
+
if (environmentKey !== undefined && environmentKey.length > 0) {
|
|
33
|
+
return { method: 'api-key', profile, apiKey: environmentKey, source: 'environment' };
|
|
34
|
+
}
|
|
35
|
+
let stored;
|
|
36
|
+
try {
|
|
37
|
+
stored = await this.store.get(account(profile));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw credentialStoreError();
|
|
41
|
+
}
|
|
42
|
+
if (stored != null && stored.length > 0) {
|
|
43
|
+
return { method: 'api-key', profile, apiKey: stored, source: 'keyring' };
|
|
44
|
+
}
|
|
45
|
+
throw new CliError({
|
|
46
|
+
code: 'AUTH_API_KEY_MISSING',
|
|
47
|
+
category: 'authentication',
|
|
48
|
+
message: 'No API key is configured for the selected profile.',
|
|
49
|
+
hint: API_KEY_RECOVERY_HINT,
|
|
50
|
+
retryable: false,
|
|
51
|
+
exitCode: 3,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async login(input) {
|
|
55
|
+
if (input.apiKey.length === 0) {
|
|
56
|
+
throw new CliError({
|
|
57
|
+
code: 'AUTH_API_KEY_EMPTY',
|
|
58
|
+
category: 'validation',
|
|
59
|
+
message: 'The API key cannot be empty.',
|
|
60
|
+
hint: 'Provide the key through hidden input, --api-key-stdin, or --api-key.',
|
|
61
|
+
retryable: false,
|
|
62
|
+
exitCode: 2,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await this.store.set(account(input.profile), input.apiKey);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw credentialStoreError();
|
|
70
|
+
}
|
|
71
|
+
return { method: 'api-key', profile: input.profile, configured: true };
|
|
72
|
+
}
|
|
73
|
+
async logout(profile) {
|
|
74
|
+
try {
|
|
75
|
+
await this.store.delete(account(profile));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw credentialStoreError();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async logoutAll() {
|
|
82
|
+
try {
|
|
83
|
+
const accounts = await this.store.listAccounts();
|
|
84
|
+
await Promise.all(accounts
|
|
85
|
+
.filter((candidate) => candidate.startsWith(PROFILE_PREFIX))
|
|
86
|
+
.map((candidate) => this.store.delete(candidate)));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw credentialStoreError();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async status(profile) {
|
|
93
|
+
try {
|
|
94
|
+
const key = await this.store.get(account(profile));
|
|
95
|
+
return { method: 'api-key', profile, configured: key != null && key.length > 0 };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw credentialStoreError();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=api-key-provider.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CredentialStore {
|
|
2
|
+
get(account: string): Promise<string | undefined>;
|
|
3
|
+
set(account: string, secret: string): Promise<void>;
|
|
4
|
+
delete(account: string): Promise<boolean>;
|
|
5
|
+
listAccounts(): Promise<string[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare class KeyringCredentialStore implements CredentialStore {
|
|
8
|
+
private readonly service;
|
|
9
|
+
constructor(service?: string);
|
|
10
|
+
get(account: string): Promise<string | undefined>;
|
|
11
|
+
set(account: string, secret: string): Promise<void>;
|
|
12
|
+
delete(account: string): Promise<boolean>;
|
|
13
|
+
listAccounts(): Promise<string[]>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=keyring.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AsyncEntry, findCredentialsAsync } from '@napi-rs/keyring';
|
|
2
|
+
export class KeyringCredentialStore {
|
|
3
|
+
service;
|
|
4
|
+
constructor(service = 'hithink-finance') {
|
|
5
|
+
this.service = service;
|
|
6
|
+
}
|
|
7
|
+
async get(account) {
|
|
8
|
+
return new AsyncEntry(this.service, account).getPassword();
|
|
9
|
+
}
|
|
10
|
+
async set(account, secret) {
|
|
11
|
+
await new AsyncEntry(this.service, account).setPassword(secret);
|
|
12
|
+
}
|
|
13
|
+
async delete(account) {
|
|
14
|
+
return new AsyncEntry(this.service, account).deleteCredential();
|
|
15
|
+
}
|
|
16
|
+
async listAccounts() {
|
|
17
|
+
const credentials = await findCredentialsAsync(this.service);
|
|
18
|
+
return credentials.map((credential) => credential.account);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=keyring.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const SECRET_KEYS = new Set(['apikey', 'token', 'authorization', 'cookie', 'secret', 'password']);
|
|
2
|
+
const SECRET_ASSIGNMENT = /\b(api[-_ ]?key|apikey|token|authorization|cookie|secret|password)\s*([=:])\s*([^\s,;]+)/giu;
|
|
3
|
+
export function redactText(value, knownSecrets = []) {
|
|
4
|
+
let redacted = value.replace(SECRET_ASSIGNMENT, (_match, key, separator) => {
|
|
5
|
+
return `${key}${separator}[REDACTED]`;
|
|
6
|
+
});
|
|
7
|
+
for (const secret of knownSecrets) {
|
|
8
|
+
if (secret.length > 0)
|
|
9
|
+
redacted = redacted.replaceAll(secret, '[REDACTED]');
|
|
10
|
+
}
|
|
11
|
+
return redacted;
|
|
12
|
+
}
|
|
13
|
+
export function redactValue(value, knownSecrets = []) {
|
|
14
|
+
if (typeof value === 'string')
|
|
15
|
+
return redactText(value, knownSecrets);
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map((entry) => redactValue(entry, knownSecrets));
|
|
18
|
+
if (value !== null && typeof value === 'object') {
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
|
|
20
|
+
const normalized = key.toLowerCase().replaceAll(/[^a-z]/g, '');
|
|
21
|
+
return [key, SECRET_KEYS.has(normalized) ? '[REDACTED]' : redactValue(entry, knownSecrets)];
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
+
export interface OpenDatabase {
|
|
3
|
+
path: string;
|
|
4
|
+
connection: DuckDBConnection;
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function openDatabase(databasePath: string): Promise<OpenDatabase>;
|
|
8
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DuckDBInstance } from '@duckdb/node-api';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
export async function openDatabase(databasePath) {
|
|
5
|
+
const absolutePath = path.resolve(databasePath);
|
|
6
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
7
|
+
const instance = await DuckDBInstance.create(absolutePath);
|
|
8
|
+
const connection = await instance.connect();
|
|
9
|
+
return {
|
|
10
|
+
path: absolutePath,
|
|
11
|
+
connection,
|
|
12
|
+
close() {
|
|
13
|
+
connection.closeSync();
|
|
14
|
+
instance.closeSync();
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DumpKind = 'daily-k' | 'daily-k-10d' | 'adjustment-factors';
|
|
2
|
+
export interface FetchFuyaoDumpOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
kind: DumpKind;
|
|
6
|
+
cacheDir: string;
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface FetchedDump {
|
|
11
|
+
path: string;
|
|
12
|
+
releaseId: string;
|
|
13
|
+
sha256: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function downloadDump(url: URL, targetPath: string, expectedSha256?: string, fetchImplementation?: typeof globalThis.fetch): Promise<{
|
|
16
|
+
path: string;
|
|
17
|
+
sha256: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function fetchFuyaoDump(options: FetchFuyaoDumpOptions): Promise<FetchedDump>;
|
|
20
|
+
//# sourceMappingURL=dump-client.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
export async function downloadDump(url, targetPath, expectedSha256, fetchImplementation = globalThis.fetch) {
|
|
8
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
9
|
+
const temporary = `${absoluteTarget}.${process.pid}.download`;
|
|
10
|
+
await mkdir(path.dirname(absoluteTarget), { recursive: true });
|
|
11
|
+
const response = await fetchImplementation(url);
|
|
12
|
+
if (!response.ok || response.body === null)
|
|
13
|
+
throw new Error(`Dump download failed: HTTP ${response.status}`);
|
|
14
|
+
try {
|
|
15
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(temporary, { flags: 'wx' }));
|
|
16
|
+
const sha256 = createHash('sha256')
|
|
17
|
+
.update(await readFile(temporary))
|
|
18
|
+
.digest('hex');
|
|
19
|
+
if (expectedSha256 !== undefined && sha256 !== expectedSha256)
|
|
20
|
+
throw new Error('Dump checksum mismatch');
|
|
21
|
+
await rename(temporary, absoluteTarget);
|
|
22
|
+
return { path: absoluteTarget, sha256 };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
await rm(temporary, { force: true });
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function releaseId(url) {
|
|
30
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
31
|
+
return segments.slice(-2).join('/');
|
|
32
|
+
}
|
|
33
|
+
async function signDump(options, fetchImplementation) {
|
|
34
|
+
const url = new URL(`/api/dump/market-dumps/${options.kind}/download-url`, options.baseUrl);
|
|
35
|
+
const response = await fetchImplementation(url, {
|
|
36
|
+
headers: { 'X-api-key': options.apiKey, accept: 'application/json' },
|
|
37
|
+
signal: AbortSignal.timeout(30_000),
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok)
|
|
40
|
+
throw new Error(`Dump signing failed: HTTP ${response.status}`);
|
|
41
|
+
const envelope = (await response.json());
|
|
42
|
+
if (envelope.code !== 0 || envelope.data?.presigned_url === undefined)
|
|
43
|
+
throw new Error(`Dump signing failed: ${envelope.code ?? 'invalid'} ${envelope.message ?? ''}`);
|
|
44
|
+
return new URL(envelope.data.presigned_url);
|
|
45
|
+
}
|
|
46
|
+
export async function fetchFuyaoDump(options) {
|
|
47
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
48
|
+
const sleep = options.sleep ??
|
|
49
|
+
((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
50
|
+
let lastError;
|
|
51
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
52
|
+
try {
|
|
53
|
+
const signedUrl = await signDump(options, fetchImplementation);
|
|
54
|
+
const target = path.join(options.cacheDir, `${options.kind}.parquet`);
|
|
55
|
+
const downloaded = await downloadDump(signedUrl, target, undefined, fetchImplementation);
|
|
56
|
+
return { ...downloaded, releaseId: releaseId(signedUrl) };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
lastError = error;
|
|
60
|
+
if (attempt === 0)
|
|
61
|
+
await sleep(250);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastError;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=dump-client.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export async function rebuildAdjustmentFactors(connection) {
|
|
2
|
+
await connection.run('BEGIN TRANSACTION');
|
|
3
|
+
try {
|
|
4
|
+
await connection.run('DELETE FROM calc_adjust_factor_daily');
|
|
5
|
+
await connection.run(`
|
|
6
|
+
INSERT INTO calc_adjust_factor_daily(thscode, date, forward_factor, backward_factor)
|
|
7
|
+
WITH
|
|
8
|
+
-- CTE 1: effective_event — 确定除权事件的实际生效日期
|
|
9
|
+
-- 每个除权事件在 ex_date 当天或之后第一个有交易记录的日期生效
|
|
10
|
+
effective_event AS (
|
|
11
|
+
SELECT e.thscode, e.dividend_per_share AS d,
|
|
12
|
+
e.per_share_bonus AS s, -- 每股送转股数
|
|
13
|
+
e.rights_ratio AS r, -- 配股比例
|
|
14
|
+
COALESCE(e.rights_price, 0) AS p, -- 配股价格,无配股时默认为0
|
|
15
|
+
-- 查找除权日当天或之后第一个有K线数据的交易日作为生效日期
|
|
16
|
+
(SELECT MIN(k.date) FROM raw_kline_daily k
|
|
17
|
+
WHERE k.thscode=e.thscode AND k.date>=e.ex_date) AS eff_date
|
|
18
|
+
FROM raw_adjustment_events e
|
|
19
|
+
),
|
|
20
|
+
-- CTE 2: kline_prev — 获取每个交易日的前一日收盘价
|
|
21
|
+
-- 使用 LAG 窗口函数取同股票的上一个交易日收盘价
|
|
22
|
+
kline_prev AS (
|
|
23
|
+
SELECT thscode, date,
|
|
24
|
+
LAG(close) OVER(PARTITION BY thscode ORDER BY date) AS prev_close
|
|
25
|
+
FROM raw_kline_daily
|
|
26
|
+
),
|
|
27
|
+
-- CTE 3: event_ratio — 计算除权日对应的复权比率
|
|
28
|
+
-- 公式: ratio = (prev_close * (1 + s + r)) / (prev_close - d + r * p)
|
|
29
|
+
-- 其中 s=送转股数, r=配股比例, d=每股分红, p=配股价
|
|
30
|
+
-- NULLIF 防止除零错误
|
|
31
|
+
event_ratio AS (
|
|
32
|
+
SELECT e.thscode, e.eff_date AS date,
|
|
33
|
+
(k.prev_close * (1 + e.s + e.r)) /
|
|
34
|
+
NULLIF(k.prev_close - e.d + e.r * e.p, 0) AS ratio
|
|
35
|
+
FROM effective_event e JOIN kline_prev k
|
|
36
|
+
ON k.thscode=e.thscode AND k.date=e.eff_date
|
|
37
|
+
WHERE k.prev_close IS NOT NULL
|
|
38
|
+
),
|
|
39
|
+
-- CTE 4: day_ratio — 合并同一天多个除权事件的比率
|
|
40
|
+
-- 使用对数求和: EXP(SUM(LN(ratio))) = ratio1 * ratio2 * ...
|
|
41
|
+
-- 只在 ratio > 0 时参与计算
|
|
42
|
+
day_ratio AS (
|
|
43
|
+
SELECT thscode, date,
|
|
44
|
+
EXP(SUM(LN(ratio))) AS ratio
|
|
45
|
+
FROM event_ratio WHERE ratio>0 GROUP BY thscode, date
|
|
46
|
+
),
|
|
47
|
+
-- CTE 5: backward — 计算每个交易日的后复权因子
|
|
48
|
+
-- 使用窗口函数按股票分区、日期排序累积乘积
|
|
49
|
+
-- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 从上市首日累积到当日
|
|
50
|
+
-- COALESCE(r.ratio, 1) 表示非除权日的比率为1(不变化)
|
|
51
|
+
backward AS (
|
|
52
|
+
SELECT k.thscode, k.date,
|
|
53
|
+
EXP(SUM(LN(COALESCE(r.ratio,1))) OVER(
|
|
54
|
+
PARTITION BY k.thscode ORDER BY k.date
|
|
55
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS backward_factor
|
|
56
|
+
FROM raw_kline_daily k LEFT JOIN day_ratio r USING(thscode, date)
|
|
57
|
+
),
|
|
58
|
+
-- CTE 6: normalized — 计算前复权因子
|
|
59
|
+
-- 前复权因子 = 当日后复权因子 / 最后一个交易日的后复权因子
|
|
60
|
+
-- LAST_VALUE 窗口函数获取该股票全部交易日的最后一个后复权因子
|
|
61
|
+
normalized AS (
|
|
62
|
+
SELECT *,
|
|
63
|
+
LAST_VALUE(backward_factor) OVER(
|
|
64
|
+
PARTITION BY thscode ORDER BY date
|
|
65
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_factor
|
|
66
|
+
FROM backward
|
|
67
|
+
)
|
|
68
|
+
-- 最终输出:thscode, date, forward_factor, backward_factor
|
|
69
|
+
SELECT thscode, date,
|
|
70
|
+
backward_factor / NULLIF(last_factor, 0) AS forward_factor,
|
|
71
|
+
backward_factor
|
|
72
|
+
FROM normalized
|
|
73
|
+
`);
|
|
74
|
+
const reader = await connection.runAndReadAll('SELECT count(*) FROM calc_adjust_factor_daily');
|
|
75
|
+
await connection.run('COMMIT');
|
|
76
|
+
return Number(reader.getRowsJson()[0]?.[0] ?? 0);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
await connection.run('ROLLBACK').catch(() => undefined);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=factors.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
+
export interface ParquetBundle {
|
|
3
|
+
klinePath: string;
|
|
4
|
+
eventsPath: string;
|
|
5
|
+
symbolsPath?: string;
|
|
6
|
+
batchId: string;
|
|
7
|
+
source: string;
|
|
8
|
+
mode?: 'FULL' | 'INCREMENTAL';
|
|
9
|
+
}
|
|
10
|
+
export declare function importParquetBundle(connection: DuckDBConnection, bundle: ParquetBundle): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=importer.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
async function parquetColumns(connection, parquetPath) {
|
|
2
|
+
const reader = await connection.runAndReadAll('DESCRIBE SELECT * FROM read_parquet($path)', {
|
|
3
|
+
path: parquetPath,
|
|
4
|
+
});
|
|
5
|
+
return new Set(reader.getRowsJson().map((row) => String(row[0])));
|
|
6
|
+
}
|
|
7
|
+
function requireColumns(actual, required, kind) {
|
|
8
|
+
const missing = required.filter((column) => !actual.has(column));
|
|
9
|
+
if (missing.length > 0)
|
|
10
|
+
throw new Error(`${kind} dump is missing columns: ${missing.join(', ')}`);
|
|
11
|
+
}
|
|
12
|
+
export async function importParquetBundle(connection, bundle) {
|
|
13
|
+
await connection.run('BEGIN TRANSACTION');
|
|
14
|
+
try {
|
|
15
|
+
const klineColumns = await parquetColumns(connection, bundle.klinePath);
|
|
16
|
+
const eventColumns = await parquetColumns(connection, bundle.eventsPath);
|
|
17
|
+
const publishedKline = klineColumns.has('date_ms');
|
|
18
|
+
const publishedEvents = eventColumns.has('ex_date_ms');
|
|
19
|
+
requireColumns(klineColumns, publishedKline
|
|
20
|
+
? [
|
|
21
|
+
'thscode',
|
|
22
|
+
'date_ms',
|
|
23
|
+
'open_price',
|
|
24
|
+
'high_price',
|
|
25
|
+
'low_price',
|
|
26
|
+
'close_price',
|
|
27
|
+
'volume',
|
|
28
|
+
'turnover',
|
|
29
|
+
]
|
|
30
|
+
: ['thscode', 'date', 'open', 'high', 'low', 'close', 'volume', 'amount'], 'daily-k');
|
|
31
|
+
requireColumns(eventColumns, publishedEvents
|
|
32
|
+
? [
|
|
33
|
+
'thscode',
|
|
34
|
+
'ex_date_ms',
|
|
35
|
+
'dividend_per_share',
|
|
36
|
+
'per_share_bonus',
|
|
37
|
+
'allotment_ratio',
|
|
38
|
+
'allotment_price',
|
|
39
|
+
]
|
|
40
|
+
: [
|
|
41
|
+
'thscode',
|
|
42
|
+
'ex_date',
|
|
43
|
+
'dividend_per_share',
|
|
44
|
+
'per_share_bonus',
|
|
45
|
+
'rights_ratio',
|
|
46
|
+
'rights_price',
|
|
47
|
+
], 'adjustment-factors');
|
|
48
|
+
await connection.run("INSERT INTO _import_batches(batch_id,source,started_at,status) VALUES ($batch,$source,CURRENT_TIMESTAMP,'running')", { batch: bundle.batchId, source: bundle.source });
|
|
49
|
+
await connection.run('DELETE FROM stg_kline_daily; DELETE FROM stg_adjustment_events; DELETE FROM stg_symbol');
|
|
50
|
+
await connection.run(publishedKline
|
|
51
|
+
? `INSERT INTO stg_kline_daily SELECT thscode, CAST(epoch_ms(date_ms + 28800000) AS DATE), open_price, high_price, low_price, close_price, NULL::DOUBLE, volume, turnover, $batch FROM read_parquet($path)`
|
|
52
|
+
: `INSERT INTO stg_kline_daily SELECT thscode,date,open,high,low,close,prev_close,volume,amount,$batch FROM read_parquet($path)`, { path: bundle.klinePath, batch: bundle.batchId });
|
|
53
|
+
await connection.run(publishedEvents
|
|
54
|
+
? `INSERT INTO stg_adjustment_events SELECT thscode, CAST(epoch_ms(ex_date_ms + 28800000) AS DATE), dividend_per_share, per_share_bonus, allotment_ratio, allotment_price, $batch FROM read_parquet($path)`
|
|
55
|
+
: `INSERT INTO stg_adjustment_events SELECT thscode,ex_date,dividend_per_share,per_share_bonus,rights_ratio,rights_price,$batch FROM read_parquet($path)`, { path: bundle.eventsPath, batch: bundle.batchId });
|
|
56
|
+
if (bundle.symbolsPath !== undefined) {
|
|
57
|
+
await connection.run('INSERT INTO stg_symbol SELECT * FROM read_parquet($path)', {
|
|
58
|
+
path: bundle.symbolsPath,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await connection.run(`
|
|
63
|
+
INSERT INTO stg_symbol
|
|
64
|
+
SELECT DISTINCT
|
|
65
|
+
thscode,
|
|
66
|
+
split_part(thscode, '.', 1),
|
|
67
|
+
NULL::VARCHAR,
|
|
68
|
+
split_part(thscode, '.', 2),
|
|
69
|
+
'a-share',
|
|
70
|
+
CURRENT_TIMESTAMP
|
|
71
|
+
FROM stg_kline_daily
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
if ((bundle.mode ?? 'FULL') === 'FULL') {
|
|
75
|
+
await connection.run('DELETE FROM raw_kline_daily');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
await connection.run(`
|
|
79
|
+
DELETE FROM raw_kline_daily
|
|
80
|
+
WHERE date BETWEEN (SELECT min(date) FROM stg_kline_daily)
|
|
81
|
+
AND (SELECT max(date) FROM stg_kline_daily)
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
await connection.run('INSERT OR REPLACE INTO raw_kline_daily SELECT * FROM stg_kline_daily');
|
|
85
|
+
await connection.run('DELETE FROM raw_adjustment_events');
|
|
86
|
+
await connection.run('INSERT OR REPLACE INTO raw_adjustment_events SELECT * FROM stg_adjustment_events');
|
|
87
|
+
await connection.run(`
|
|
88
|
+
INSERT INTO dim_symbol
|
|
89
|
+
SELECT * FROM stg_symbol
|
|
90
|
+
ON CONFLICT (thscode) DO UPDATE SET
|
|
91
|
+
ticker=COALESCE(excluded.ticker,dim_symbol.ticker),
|
|
92
|
+
name=COALESCE(excluded.name,dim_symbol.name),
|
|
93
|
+
exchange=COALESCE(excluded.exchange,dim_symbol.exchange),
|
|
94
|
+
asset_type=excluded.asset_type,
|
|
95
|
+
updated_at=excluded.updated_at
|
|
96
|
+
`);
|
|
97
|
+
await connection.run("UPDATE _import_batches SET completed_at=CURRENT_TIMESTAMP,status='complete',row_count=(SELECT count(*) FROM stg_kline_daily) WHERE batch_id=$batch", { batch: bundle.batchId });
|
|
98
|
+
await connection.run('COMMIT');
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
await connection.run('ROLLBACK').catch(() => undefined);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=importer.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
+
export interface Migration {
|
|
3
|
+
version: number;
|
|
4
|
+
name: string;
|
|
5
|
+
type: 'safe' | 'heavy' | 'destructive';
|
|
6
|
+
checksum: string;
|
|
7
|
+
sql: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function planMigrations(connection: DuckDBConnection, available?: readonly Migration[]): Promise<Migration[]>;
|
|
10
|
+
export declare function applyMigrations(connection: DuckDBConnection, available?: readonly Migration[], options?: {
|
|
11
|
+
allowHeavy?: boolean;
|
|
12
|
+
allowDestructive?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function assertSchemaCompatibility(connection: DuckDBConnection, supportedVersion?: number): Promise<{
|
|
15
|
+
version: number;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { CliError } from '../../contracts/errors.js';
|
|
6
|
+
import { SUPPORTED_SCHEMA_VERSION } from './schema.js';
|
|
7
|
+
function normalizeLineEndings(value) {
|
|
8
|
+
return value.replaceAll('\r\n', '\n');
|
|
9
|
+
}
|
|
10
|
+
const manifestSchema = z.object({
|
|
11
|
+
schemaVersion: z.number().int().positive(),
|
|
12
|
+
migrations: z.array(z.object({
|
|
13
|
+
version: z.number().int().positive(),
|
|
14
|
+
name: z.string().min(1),
|
|
15
|
+
type: z.enum(['safe', 'heavy', 'destructive']),
|
|
16
|
+
file: z.string().min(1),
|
|
17
|
+
checksum: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
function migrationRoot() {
|
|
21
|
+
return path.resolve(import.meta.dirname, '../../../migrations');
|
|
22
|
+
}
|
|
23
|
+
async function bundledMigrations() {
|
|
24
|
+
const root = migrationRoot();
|
|
25
|
+
const manifest = manifestSchema.parse(JSON.parse(await readFile(path.join(root, 'manifest.json'), 'utf8')));
|
|
26
|
+
const migrations = [];
|
|
27
|
+
for (const entry of manifest.migrations) {
|
|
28
|
+
const sql = normalizeLineEndings(await readFile(path.join(root, entry.file), 'utf8'));
|
|
29
|
+
const checksum = createHash('sha256').update(sql).digest('hex');
|
|
30
|
+
if (checksum !== entry.checksum) {
|
|
31
|
+
throw new Error(`Migration checksum mismatch for ${entry.file}`);
|
|
32
|
+
}
|
|
33
|
+
migrations.push({ ...entry, sql });
|
|
34
|
+
}
|
|
35
|
+
return migrations;
|
|
36
|
+
}
|
|
37
|
+
async function schemaVersion(connection) {
|
|
38
|
+
const tableReader = await connection.runAndReadAll("SELECT count(*) FROM information_schema.tables WHERE table_schema='main' AND table_name='_meta'");
|
|
39
|
+
if (Number(tableReader.getRowsJson()[0]?.[0] ?? 0) === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
const reader = await connection.runAndReadAll("SELECT value FROM _meta WHERE key='schema_version' LIMIT 1");
|
|
42
|
+
const value = reader.getRowsJson()[0]?.[0];
|
|
43
|
+
return value === undefined ? 0 : Number(value);
|
|
44
|
+
}
|
|
45
|
+
export async function planMigrations(connection, available) {
|
|
46
|
+
const current = await schemaVersion(connection);
|
|
47
|
+
const migrations = available === undefined ? await bundledMigrations() : [...available];
|
|
48
|
+
return migrations
|
|
49
|
+
.filter((migration) => migration.version > current)
|
|
50
|
+
.sort((a, b) => a.version - b.version);
|
|
51
|
+
}
|
|
52
|
+
export async function applyMigrations(connection, available, options = {}) {
|
|
53
|
+
const pending = await planMigrations(connection, available);
|
|
54
|
+
for (const migration of pending) {
|
|
55
|
+
if ((migration.type === 'heavy' && options.allowHeavy !== true) ||
|
|
56
|
+
(migration.type === 'destructive' && options.allowDestructive !== true)) {
|
|
57
|
+
throw new CliError({
|
|
58
|
+
code: 'DATA_MIGRATION_CONFIRMATION_REQUIRED',
|
|
59
|
+
category: 'local-data',
|
|
60
|
+
message: `Migration ${migration.version} (${migration.name}) requires explicit confirmation.`,
|
|
61
|
+
hint: 'Run `hithink-finance data migrate --apply --yes` after reviewing the plan.',
|
|
62
|
+
retryable: false,
|
|
63
|
+
exitCode: 5,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
await connection.run('BEGIN TRANSACTION');
|
|
67
|
+
try {
|
|
68
|
+
await connection.run(migration.sql);
|
|
69
|
+
await connection.run("INSERT OR REPLACE INTO _meta(key, value) VALUES ('schema_version', $version), ('schema_checksum', $checksum)", { version: String(migration.version), checksum: migration.checksum });
|
|
70
|
+
await connection.run('COMMIT');
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
await connection.run('ROLLBACK').catch(() => undefined);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function assertSchemaCompatibility(connection, supportedVersion = SUPPORTED_SCHEMA_VERSION) {
|
|
79
|
+
const version = await schemaVersion(connection);
|
|
80
|
+
if (version > supportedVersion) {
|
|
81
|
+
throw new CliError({
|
|
82
|
+
code: 'DATA_SCHEMA_TOO_NEW',
|
|
83
|
+
category: 'local-data',
|
|
84
|
+
message: `Database schema ${version} is newer than supported schema ${supportedVersion}.`,
|
|
85
|
+
hint: 'Upgrade hithink-finance before writing to this database.',
|
|
86
|
+
retryable: false,
|
|
87
|
+
exitCode: 5,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { version };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
+
export interface QualityIssue {
|
|
3
|
+
code: string;
|
|
4
|
+
count: number;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface QualityResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
issues: QualityIssue[];
|
|
10
|
+
}
|
|
11
|
+
export declare function validateDatabase(connection: DuckDBConnection): Promise<QualityResult>;
|
|
12
|
+
//# sourceMappingURL=quality.d.ts.map
|