feat!: persist T-Bank data and synchronize reads
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 10m45s
Conventional commits / commits (pull_request) Successful in 29s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 10m37s
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 10m45s
Conventional commits / commits (pull_request) Successful in 29s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 10m37s
Store account membership, portfolio, operations, forecasts and market sources in SQLite. Coalesce scoped refreshes, fence connection revisions, preserve complete financial snapshots and stabilize operation period state. BREAKING CHANGE: investment reads return data/meta resource envelopes and locally persisted snapshots; consumers must use the matching OpenAPI contract and explicit synchronization endpoints.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN IMMEDIATE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankAccount" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"openedOn" TEXT,
|
||||
"upstreamOrder" INTEGER NOT NULL,
|
||||
"operationsRevision" INTEGER NOT NULL DEFAULT 0,
|
||||
"historyFrom" DATETIME,
|
||||
"historyBoundary" TEXT NOT NULL DEFAULT 'unknown',
|
||||
|
||||
PRIMARY KEY ("userId", "revision", "accountId"),
|
||||
CONSTRAINT "TbankAccount_userId_revision_fkey" FOREIGN KEY ("userId", "revision") REFERENCES "TbankConnection" ("userId", "revision") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankPortfolio" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"mappingVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("userId", "revision", "accountId"),
|
||||
CONSTRAINT "TbankPortfolio_userId_revision_accountId_fkey" FOREIGN KEY ("userId", "revision", "accountId") REFERENCES "TbankAccount" ("userId", "revision", "accountId") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankOperation" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"brokerOperationId" TEXT NOT NULL,
|
||||
"occurredAt" DATETIME NOT NULL,
|
||||
"instrumentUid" TEXT,
|
||||
"category" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"mappingVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("userId", "revision", "accountId", "brokerOperationId"),
|
||||
CONSTRAINT "TbankOperation_userId_revision_accountId_fkey" FOREIGN KEY ("userId", "revision", "accountId") REFERENCES "TbankAccount" ("userId", "revision", "accountId") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankOperationCoverage" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"instrumentPredicate" TEXT NOT NULL DEFAULT '',
|
||||
"from" DATETIME NOT NULL,
|
||||
"to" DATETIME NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"asOf" DATETIME NOT NULL,
|
||||
|
||||
PRIMARY KEY ("userId", "revision", "accountId", "instrumentPredicate", "from", "to"),
|
||||
CONSTRAINT "TbankOperationCoverage_userId_revision_accountId_fkey" FOREIGN KEY ("userId", "revision", "accountId") REFERENCES "TbankAccount" ("userId", "revision", "accountId") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankForecast" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"scopeKey" TEXT NOT NULL,
|
||||
"membershipRevision" INTEGER NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"dependencyRevisions" JSONB NOT NULL,
|
||||
"sourceTimestamps" JSONB NOT NULL,
|
||||
"calculatedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("userId", "revision", "scopeKey"),
|
||||
CONSTRAINT "TbankForecast_userId_revision_fkey" FOREIGN KEY ("userId", "revision") REFERENCES "TbankConnection" ("userId", "revision") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankInstrument" (
|
||||
"provider" TEXT NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"figi" TEXT,
|
||||
"ticker" TEXT,
|
||||
"instrumentType" TEXT NOT NULL,
|
||||
"metadata" JSONB NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("provider", "uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankCatalogDiscovery" (
|
||||
"provider" TEXT NOT NULL,
|
||||
"instrumentType" TEXT NOT NULL,
|
||||
"query" TEXT NOT NULL,
|
||||
"instrumentUids" JSONB NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("provider", "instrumentType", "query")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankInstrumentLookup" (
|
||||
"provider" TEXT NOT NULL,
|
||||
"identifierType" TEXT NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"matchedUids" JSONB NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("provider", "identifierType", "identifier")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankInstrumentSource" (
|
||||
"provider" TEXT NOT NULL,
|
||||
"instrumentUid" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"rangeKey" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("provider", "instrumentUid", "source", "kind", "rangeKey"),
|
||||
CONSTRAINT "TbankInstrumentSource_provider_instrumentUid_fkey" FOREIGN KEY ("provider", "instrumentUid") REFERENCES "TbankInstrument" ("provider", "uid") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankCandle" (
|
||||
"provider" TEXT NOT NULL,
|
||||
"instrumentUid" TEXT NOT NULL,
|
||||
"interval" TEXT NOT NULL,
|
||||
"time" DATETIME NOT NULL,
|
||||
"close" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"isComplete" BOOLEAN NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
|
||||
PRIMARY KEY ("provider", "instrumentUid", "interval", "time"),
|
||||
CONSTRAINT "TbankCandle_provider_instrumentUid_fkey" FOREIGN KEY ("provider", "instrumentUid") REFERENCES "TbankInstrument" ("provider", "uid") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankCandleCoverage" (
|
||||
"provider" TEXT NOT NULL,
|
||||
"instrumentUid" TEXT NOT NULL,
|
||||
"interval" TEXT NOT NULL,
|
||||
"from" DATETIME NOT NULL,
|
||||
"to" DATETIME NOT NULL,
|
||||
"isComplete" BOOLEAN NOT NULL,
|
||||
"fetchedAt" DATETIME NOT NULL,
|
||||
"dataRevision" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
PRIMARY KEY ("provider", "instrumentUid", "interval", "from", "to"),
|
||||
CONSTRAINT "TbankCandleCoverage_provider_instrumentUid_fkey" FOREIGN KEY ("provider", "instrumentUid") REFERENCES "TbankInstrument" ("provider", "uid") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankSyncWork" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"scopeKey" TEXT NOT NULL,
|
||||
"intent" JSONB NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"attemptId" TEXT NOT NULL,
|
||||
"processRunId" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL,
|
||||
"progress" JSONB NOT NULL,
|
||||
"errorCode" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "TbankSyncWork_userId_revision_fkey" FOREIGN KEY ("userId", "revision") REFERENCES "TbankConnection" ("userId", "revision") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankSyncStage" (
|
||||
"workId" TEXT NOT NULL,
|
||||
"attemptId" TEXT NOT NULL,
|
||||
"sourceKey" TEXT NOT NULL,
|
||||
"itemKey" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
|
||||
PRIMARY KEY ("workId", "attemptId", "sourceKey", "itemKey"),
|
||||
CONSTRAINT "TbankSyncStage_workId_attemptId_fkey" FOREIGN KEY ("workId", "attemptId") REFERENCES "TbankSyncWork" ("id", "attemptId") ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
CREATE TABLE "new_TbankConnection" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"encryptedToken" TEXT NOT NULL,
|
||||
"connectedAt" DATETIME NOT NULL,
|
||||
"revision" TEXT NOT NULL,
|
||||
"invalidToken" BOOLEAN NOT NULL DEFAULT false,
|
||||
"membershipAt" DATETIME,
|
||||
"membershipRevision" INTEGER NOT NULL DEFAULT 0,
|
||||
CONSTRAINT "TbankConnection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
-- Preserve credentials and assign an opaque revision to each existing connection.
|
||||
INSERT INTO "new_TbankConnection" ("connectedAt", "encryptedToken", "userId", "revision") SELECT "connectedAt", "encryptedToken", "userId", lower(hex(randomblob(16))) FROM "TbankConnection";
|
||||
DROP TABLE "TbankConnection";
|
||||
ALTER TABLE "new_TbankConnection" RENAME TO "TbankConnection";
|
||||
CREATE UNIQUE INDEX "TbankConnection_userId_key" ON "TbankConnection"("userId");
|
||||
CREATE UNIQUE INDEX "TbankConnection_userId_revision_key" ON "TbankConnection"("userId", "revision");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TbankOperation_userId_revision_accountId_occurredAt_brokerOperationId_idx" ON "TbankOperation"("userId", "revision", "accountId", "occurredAt", "brokerOperationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TbankOperation_userId_revision_accountId_instrumentUid_occurredAt_idx" ON "TbankOperation"("userId", "revision", "accountId", "instrumentUid", "occurredAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TbankOperation_userId_revision_accountId_category_occurredAt_idx" ON "TbankOperation"("userId", "revision", "accountId", "category", "occurredAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TbankInstrument_provider_figi_idx" ON "TbankInstrument"("provider", "figi");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TbankInstrument_provider_ticker_instrumentType_idx" ON "TbankInstrument"("provider", "ticker", "instrumentType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TbankSyncWork_userId_revision_state_priority_idx" ON "TbankSyncWork"("userId", "revision", "state", "priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TbankSyncWork_userId_revision_scopeKey_key" ON "TbankSyncWork"("userId", "revision", "scopeKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TbankSyncWork_id_attemptId_key" ON "TbankSyncWork"("id", "attemptId");
|
||||
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -25,7 +25,212 @@ model TbankConnection {
|
||||
userId String @unique
|
||||
encryptedToken String
|
||||
connectedAt DateTime
|
||||
revision String @default(uuid())
|
||||
invalidToken Boolean @default(false)
|
||||
membershipAt DateTime?
|
||||
membershipRevision Int @default(0)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
accounts TbankAccount[]
|
||||
forecasts TbankForecast[]
|
||||
work TbankSyncWork[]
|
||||
|
||||
@@unique([userId, revision])
|
||||
}
|
||||
|
||||
model TbankAccount {
|
||||
userId String
|
||||
revision String
|
||||
accountId String
|
||||
type String
|
||||
name String
|
||||
openedOn String?
|
||||
upstreamOrder Int
|
||||
operationsRevision Int @default(0)
|
||||
historyFrom DateTime?
|
||||
historyBoundary String @default("unknown")
|
||||
connection TbankConnection @relation(fields: [userId, revision], references: [userId, revision], onDelete: Cascade, onUpdate: Restrict)
|
||||
portfolio TbankPortfolio?
|
||||
operations TbankOperation[]
|
||||
coverage TbankOperationCoverage[]
|
||||
|
||||
@@id([userId, revision, accountId])
|
||||
}
|
||||
|
||||
model TbankPortfolio {
|
||||
userId String
|
||||
revision String
|
||||
accountId String
|
||||
payload Json
|
||||
mappingVersion Int @default(1)
|
||||
fetchedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
account TbankAccount @relation(fields: [userId, revision, accountId], references: [userId, revision, accountId], onDelete: Cascade, onUpdate: Restrict)
|
||||
|
||||
@@id([userId, revision, accountId])
|
||||
}
|
||||
|
||||
model TbankOperation {
|
||||
userId String
|
||||
revision String
|
||||
accountId String
|
||||
brokerOperationId String
|
||||
occurredAt DateTime
|
||||
instrumentUid String?
|
||||
category String
|
||||
payload Json
|
||||
mappingVersion Int @default(1)
|
||||
account TbankAccount @relation(fields: [userId, revision, accountId], references: [userId, revision, accountId], onDelete: Cascade, onUpdate: Restrict)
|
||||
|
||||
@@id([userId, revision, accountId, brokerOperationId])
|
||||
@@index([userId, revision, accountId, occurredAt, brokerOperationId])
|
||||
@@index([userId, revision, accountId, instrumentUid, occurredAt])
|
||||
@@index([userId, revision, accountId, category, occurredAt])
|
||||
}
|
||||
|
||||
model TbankOperationCoverage {
|
||||
userId String
|
||||
revision String
|
||||
accountId String
|
||||
instrumentPredicate String @default("")
|
||||
from DateTime
|
||||
to DateTime
|
||||
fetchedAt DateTime
|
||||
asOf DateTime
|
||||
account TbankAccount @relation(fields: [userId, revision, accountId], references: [userId, revision, accountId], onDelete: Cascade, onUpdate: Restrict)
|
||||
|
||||
@@id([userId, revision, accountId, instrumentPredicate, from, to])
|
||||
}
|
||||
|
||||
model TbankForecast {
|
||||
userId String
|
||||
revision String
|
||||
scopeKey String
|
||||
membershipRevision Int
|
||||
payload Json
|
||||
dependencyRevisions Json
|
||||
sourceTimestamps Json
|
||||
calculatedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
connection TbankConnection @relation(fields: [userId, revision], references: [userId, revision], onDelete: Cascade, onUpdate: Restrict)
|
||||
|
||||
@@id([userId, revision, scopeKey])
|
||||
}
|
||||
|
||||
model TbankInstrument {
|
||||
provider String
|
||||
uid String
|
||||
figi String?
|
||||
ticker String?
|
||||
instrumentType String
|
||||
metadata Json
|
||||
fetchedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
sources TbankInstrumentSource[]
|
||||
candles TbankCandle[]
|
||||
candleCoverage TbankCandleCoverage[]
|
||||
|
||||
@@id([provider, uid])
|
||||
@@index([provider, figi])
|
||||
@@index([provider, ticker, instrumentType])
|
||||
}
|
||||
|
||||
model TbankCatalogDiscovery {
|
||||
provider String
|
||||
instrumentType String
|
||||
query String
|
||||
instrumentUids Json
|
||||
fetchedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
|
||||
@@id([provider, instrumentType, query])
|
||||
}
|
||||
|
||||
// Empty and ambiguous successful UID/FIGI/ticker lookups are distinct from failures.
|
||||
model TbankInstrumentLookup {
|
||||
provider String
|
||||
identifierType String
|
||||
identifier String
|
||||
matchedUids Json
|
||||
fetchedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
|
||||
@@id([provider, identifierType, identifier])
|
||||
}
|
||||
|
||||
// Source records contain no holdings, personal estimates or credentials errors.
|
||||
model TbankInstrumentSource {
|
||||
provider String
|
||||
instrumentUid String
|
||||
source String
|
||||
kind String
|
||||
rangeKey String
|
||||
payload Json
|
||||
fetchedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
instrument TbankInstrument @relation(fields: [provider, instrumentUid], references: [provider, uid], onDelete: Cascade)
|
||||
|
||||
@@id([provider, instrumentUid, source, kind, rangeKey])
|
||||
}
|
||||
|
||||
model TbankCandle {
|
||||
provider String
|
||||
instrumentUid String
|
||||
interval String
|
||||
time DateTime
|
||||
close String
|
||||
payload Json
|
||||
isComplete Boolean
|
||||
fetchedAt DateTime
|
||||
instrument TbankInstrument @relation(fields: [provider, instrumentUid], references: [provider, uid], onDelete: Cascade)
|
||||
|
||||
@@id([provider, instrumentUid, interval, time])
|
||||
}
|
||||
|
||||
model TbankCandleCoverage {
|
||||
provider String
|
||||
instrumentUid String
|
||||
interval String
|
||||
from DateTime
|
||||
to DateTime
|
||||
isComplete Boolean
|
||||
fetchedAt DateTime
|
||||
dataRevision Int @default(1)
|
||||
instrument TbankInstrument @relation(fields: [provider, instrumentUid], references: [provider, uid], onDelete: Cascade)
|
||||
|
||||
@@id([provider, instrumentUid, interval, from, to])
|
||||
}
|
||||
|
||||
model TbankSyncWork {
|
||||
id String @id
|
||||
userId String
|
||||
revision String
|
||||
scopeKey String
|
||||
intent Json
|
||||
state String
|
||||
attemptId String
|
||||
processRunId String
|
||||
priority Int
|
||||
progress Json
|
||||
errorCode String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
connection TbankConnection @relation(fields: [userId, revision], references: [userId, revision], onDelete: Cascade, onUpdate: Restrict)
|
||||
staged TbankSyncStage[]
|
||||
|
||||
@@unique([userId, revision, scopeKey])
|
||||
@@index([userId, revision, state, priority])
|
||||
@@unique([id, attemptId])
|
||||
}
|
||||
|
||||
model TbankSyncStage {
|
||||
workId String
|
||||
attemptId String
|
||||
sourceKey String
|
||||
itemKey String
|
||||
payload Json
|
||||
work TbankSyncWork @relation(fields: [workId, attemptId], references: [id, attemptId], onDelete: Cascade, onUpdate: Restrict)
|
||||
|
||||
@@id([workId, attemptId, sourceKey, itemKey])
|
||||
}
|
||||
|
||||
model Session {
|
||||
|
||||
Reference in New Issue
Block a user