sheetlink 0.1.15 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sheetlink",
3
- "version": "0.1.15",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for SheetLink — sync your bank transactions to any destination",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,11 +4,52 @@
4
4
  * Writes a flat snapshot of transactions to a CSV file.
5
5
  * Each run OVERWRITES the file — no append, no dedup.
6
6
  * Users who want history should use Postgres or SQLite.
7
+ *
8
+ * Full schema matches the Google Sheets extension and Excel add-in (35 transaction columns).
9
+ * Use --slim to write the legacy 14-column subset instead.
7
10
  */
8
11
 
9
12
  import fs from 'fs';
10
13
 
11
- const HEADERS = [
14
+ const HEADERS_FULL = [
15
+ 'transaction_id',
16
+ 'account_id',
17
+ 'persistent_account_id',
18
+ 'account_name',
19
+ 'account_mask',
20
+ 'date',
21
+ 'authorized_date',
22
+ 'datetime',
23
+ 'authorized_datetime',
24
+ 'description_raw',
25
+ 'merchant_name',
26
+ 'merchant_entity_id',
27
+ 'amount',
28
+ 'iso_currency_code',
29
+ 'unofficial_currency_code',
30
+ 'pending',
31
+ 'pending_transaction_id',
32
+ 'check_number',
33
+ 'category_primary',
34
+ 'category_detailed',
35
+ 'payment_channel',
36
+ 'transaction_type',
37
+ 'transaction_code',
38
+ 'location_address',
39
+ 'location_city',
40
+ 'location_region',
41
+ 'location_postal_code',
42
+ 'location_country',
43
+ 'location_lat',
44
+ 'location_lon',
45
+ 'website',
46
+ 'logo_url',
47
+ 'source_institution',
48
+ 'category',
49
+ 'synced_at',
50
+ ];
51
+
52
+ const HEADERS_SLIM = [
12
53
  'date',
13
54
  'name',
14
55
  'amount',
@@ -16,12 +57,12 @@ const HEADERS = [
16
57
  'account_id',
17
58
  'account_name',
18
59
  'account_mask',
19
- 'institution_name',
60
+ 'source_institution',
20
61
  'pending',
21
62
  'payment_channel',
22
63
  'merchant_name',
23
- 'primary_category',
24
- 'detailed_category',
64
+ 'category_primary',
65
+ 'category_detailed',
25
66
  'transaction_id',
26
67
  ];
27
68
 
@@ -34,12 +75,64 @@ function escape(val) {
34
75
  return s;
35
76
  }
36
77
 
37
- function formatRow(txn) {
38
- return HEADERS.map(h => escape(txn[h])).join(',');
78
+ function flattenTransaction(tx) {
79
+ const loc = tx.location || {};
80
+ const pfc = tx.personal_finance_category || {};
81
+ const category = Array.isArray(tx.plaid_category) ? tx.plaid_category.join(', ') : (tx.category || null);
82
+ return {
83
+ transaction_id: tx.transaction_id,
84
+ account_id: tx.account_id,
85
+ persistent_account_id: tx.persistent_account_id || null,
86
+ account_name: tx.account_name,
87
+ account_mask: tx.account_mask,
88
+ date: tx.date,
89
+ authorized_date: tx.authorized_date || null,
90
+ datetime: tx.datetime || null,
91
+ authorized_datetime: tx.authorized_datetime || null,
92
+ description_raw: tx.description_raw || tx.name,
93
+ merchant_name: tx.merchant_name || null,
94
+ merchant_entity_id: tx.merchant_entity_id || null,
95
+ amount: tx.amount,
96
+ iso_currency_code: tx.iso_currency_code || null,
97
+ unofficial_currency_code: tx.unofficial_currency_code || null,
98
+ pending: tx.pending,
99
+ pending_transaction_id: tx.pending_transaction_id || null,
100
+ check_number: tx.check_number || null,
101
+ category_primary: pfc.primary || tx.category_primary || null,
102
+ category_detailed: pfc.detailed || tx.category_detailed || null,
103
+ payment_channel: tx.payment_channel || null,
104
+ transaction_type: tx.transaction_type || null,
105
+ transaction_code: tx.transaction_code || null,
106
+ location_address: loc.address || null,
107
+ location_city: loc.city || null,
108
+ location_region: loc.region || null,
109
+ location_postal_code: loc.postal_code || null,
110
+ location_country: loc.country || null,
111
+ location_lat: loc.lat ?? null,
112
+ location_lon: loc.lon ?? null,
113
+ website: tx.website || null,
114
+ logo_url: tx.logo_url || null,
115
+ source_institution: tx.source_institution || tx.institution_name || null,
116
+ category,
117
+ synced_at: new Date().toISOString(),
118
+ };
39
119
  }
40
120
 
41
- export function writeCsv(transactions, filePath = './sheetlink-transactions.csv') {
42
- const lines = [HEADERS.join(','), ...transactions.map(formatRow)];
43
- fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf8');
121
+ export function writeCsv(transactions, filePath = './sheetlink-transactions.csv', { slim = false } = {}) {
122
+ const headers = slim ? HEADERS_SLIM : HEADERS_FULL;
123
+ const rows = transactions.map(tx => {
124
+ const flat = slim
125
+ ? {
126
+ ...tx,
127
+ name: tx.description_raw || tx.name,
128
+ source_institution: tx.source_institution || tx.institution_name || null,
129
+ category: Array.isArray(tx.plaid_category) ? tx.plaid_category.join(', ') : (tx.category || null),
130
+ category_primary: (tx.personal_finance_category?.primary) || tx.category_primary || null,
131
+ category_detailed: (tx.personal_finance_category?.detailed) || tx.category_detailed || null,
132
+ }
133
+ : flattenTransaction(tx);
134
+ return headers.map(h => escape(flat[h])).join(',');
135
+ });
136
+ fs.writeFileSync(filePath, [headers.join(','), ...rows].join('\n') + '\n', 'utf8');
44
137
  console.log(`Wrote ${transactions.length} transactions to ${filePath}`);
45
138
  }
@@ -3,44 +3,116 @@
3
3
  *
4
4
  * Creates sheetlink_transactions and sheetlink_accounts tables if they don't exist.
5
5
  * Upserts on primary key — safe to run repeatedly with no duplicates.
6
+ *
7
+ * Full schema matches the Google Sheets extension and Excel add-in (35 transaction columns).
8
+ * Use --slim to write the legacy 14-column subset instead.
6
9
  */
7
10
 
8
- const CREATE_TRANSACTIONS = `
11
+ // Full schema — matches TRANSACTIONS_HEADERS_FULL in extension + Excel
12
+ const CREATE_TRANSACTIONS_FULL = `
13
+ CREATE TABLE IF NOT EXISTS sheetlink_transactions (
14
+ transaction_id TEXT PRIMARY KEY,
15
+ account_id TEXT,
16
+ persistent_account_id TEXT,
17
+ account_name TEXT,
18
+ account_mask TEXT,
19
+ date DATE NOT NULL,
20
+ authorized_date DATE,
21
+ datetime TEXT,
22
+ authorized_datetime TEXT,
23
+ description_raw TEXT,
24
+ merchant_name TEXT,
25
+ merchant_entity_id TEXT,
26
+ amount DECIMAL(10,2),
27
+ iso_currency_code TEXT,
28
+ unofficial_currency_code TEXT,
29
+ pending BOOLEAN,
30
+ pending_transaction_id TEXT,
31
+ check_number TEXT,
32
+ category_primary TEXT,
33
+ category_detailed TEXT,
34
+ payment_channel TEXT,
35
+ transaction_type TEXT,
36
+ transaction_code TEXT,
37
+ location_address TEXT,
38
+ location_city TEXT,
39
+ location_region TEXT,
40
+ location_postal_code TEXT,
41
+ location_country TEXT,
42
+ location_lat DECIMAL(10,7),
43
+ location_lon DECIMAL(10,7),
44
+ website TEXT,
45
+ logo_url TEXT,
46
+ source_institution TEXT,
47
+ category TEXT,
48
+ synced_at TIMESTAMP DEFAULT NOW()
49
+ )`;
50
+
51
+ // Slim schema — legacy 14-column subset for --slim flag
52
+ const CREATE_TRANSACTIONS_SLIM = `
9
53
  CREATE TABLE IF NOT EXISTS sheetlink_transactions (
10
- transaction_id TEXT PRIMARY KEY,
11
- date DATE NOT NULL,
12
- name TEXT,
13
- amount DECIMAL(10,2),
14
- category TEXT,
15
- account_id TEXT,
16
- account_name TEXT,
17
- account_mask TEXT,
18
- institution_name TEXT,
19
- pending BOOLEAN,
20
- payment_channel TEXT,
21
- merchant_name TEXT,
22
- primary_category TEXT,
23
- detailed_category TEXT,
24
- synced_at TIMESTAMP DEFAULT NOW()
54
+ transaction_id TEXT PRIMARY KEY,
55
+ date DATE NOT NULL,
56
+ name TEXT,
57
+ amount DECIMAL(10,2),
58
+ category TEXT,
59
+ account_id TEXT,
60
+ account_name TEXT,
61
+ account_mask TEXT,
62
+ source_institution TEXT,
63
+ pending BOOLEAN,
64
+ payment_channel TEXT,
65
+ merchant_name TEXT,
66
+ category_primary TEXT,
67
+ category_detailed TEXT,
68
+ synced_at TIMESTAMP DEFAULT NOW()
25
69
  )`;
26
70
 
27
71
  const CREATE_ACCOUNTS = `
28
72
  CREATE TABLE IF NOT EXISTS sheetlink_accounts (
29
- account_id TEXT PRIMARY KEY,
30
- name TEXT,
31
- mask TEXT,
32
- type TEXT,
33
- subtype TEXT,
34
- balance_current DECIMAL(10,2),
35
- balance_available DECIMAL(10,2),
36
- institution_name TEXT,
37
- last_synced_at TIMESTAMP DEFAULT NOW()
73
+ account_id TEXT PRIMARY KEY,
74
+ persistent_account_id TEXT,
75
+ name TEXT,
76
+ official_name TEXT,
77
+ mask TEXT,
78
+ type TEXT,
79
+ subtype TEXT,
80
+ current_balance DECIMAL(10,2),
81
+ available_balance DECIMAL(10,2),
82
+ iso_currency_code TEXT,
83
+ institution TEXT,
84
+ last_synced_at TIMESTAMP DEFAULT NOW()
38
85
  )`;
39
86
 
40
- const UPSERT_TRANSACTION = `
87
+ const UPSERT_TRANSACTION_FULL = `
88
+ INSERT INTO sheetlink_transactions
89
+ (transaction_id, account_id, persistent_account_id, account_name, account_mask,
90
+ date, authorized_date, datetime, authorized_datetime,
91
+ description_raw, merchant_name, merchant_entity_id,
92
+ amount, iso_currency_code, unofficial_currency_code,
93
+ pending, pending_transaction_id, check_number,
94
+ category_primary, category_detailed, payment_channel,
95
+ transaction_type, transaction_code,
96
+ location_address, location_city, location_region, location_postal_code, location_country,
97
+ location_lat, location_lon,
98
+ website, logo_url, source_institution, category, synced_at)
99
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,NOW())
100
+ ON CONFLICT (transaction_id) DO UPDATE SET
101
+ date = EXCLUDED.date,
102
+ authorized_date = EXCLUDED.authorized_date,
103
+ description_raw = EXCLUDED.description_raw,
104
+ amount = EXCLUDED.amount,
105
+ pending = EXCLUDED.pending,
106
+ pending_transaction_id = EXCLUDED.pending_transaction_id,
107
+ category_primary = EXCLUDED.category_primary,
108
+ category_detailed = EXCLUDED.category_detailed,
109
+ category = EXCLUDED.category,
110
+ synced_at = NOW()`;
111
+
112
+ const UPSERT_TRANSACTION_SLIM = `
41
113
  INSERT INTO sheetlink_transactions
42
114
  (transaction_id, date, name, amount, category, account_id, account_name, account_mask,
43
- institution_name, pending, payment_channel, merchant_name, primary_category, detailed_category, synced_at)
115
+ source_institution, pending, payment_channel, merchant_name, category_primary, category_detailed, synced_at)
44
116
  VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,NOW())
45
117
  ON CONFLICT (transaction_id) DO UPDATE SET
46
118
  date = EXCLUDED.date,
@@ -52,16 +124,16 @@ ON CONFLICT (transaction_id) DO UPDATE SET
52
124
 
53
125
  const UPSERT_ACCOUNT = `
54
126
  INSERT INTO sheetlink_accounts
55
- (account_id, name, mask, type, subtype, balance_current, balance_available, institution_name, last_synced_at)
56
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW())
127
+ (account_id, persistent_account_id, name, official_name, mask, type, subtype,
128
+ current_balance, available_balance, iso_currency_code, institution, last_synced_at)
129
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,NOW())
57
130
  ON CONFLICT (account_id) DO UPDATE SET
58
131
  name = EXCLUDED.name,
59
- balance_current = EXCLUDED.balance_current,
60
- balance_available = EXCLUDED.balance_available,
132
+ current_balance = EXCLUDED.current_balance,
133
+ available_balance = EXCLUDED.available_balance,
61
134
  last_synced_at = NOW()`;
62
135
 
63
- export async function writePostgres(transactions, accounts, connectionString) {
64
- // Dynamic import so users without pg installed don't hit an error on other commands
136
+ export async function writePostgres(transactions, accounts, connectionString, { slim = false } = {}) {
65
137
  const { default: pg } = await import('pg').then(m => ({ default: m.default || m }));
66
138
  const { Client } = pg;
67
139
 
@@ -69,24 +141,76 @@ export async function writePostgres(transactions, accounts, connectionString) {
69
141
  await client.connect();
70
142
 
71
143
  try {
72
- await client.query(CREATE_TRANSACTIONS);
144
+ await client.query(slim ? CREATE_TRANSACTIONS_SLIM : CREATE_TRANSACTIONS_FULL);
73
145
  await client.query(CREATE_ACCOUNTS);
74
146
 
75
147
  for (const tx of transactions) {
76
- await client.query(UPSERT_TRANSACTION, [
77
- tx.transaction_id, tx.date, tx.name, tx.amount, tx.category,
78
- tx.account_id, tx.account_name, tx.account_mask,
79
- tx.institution_name || tx.source_institution,
80
- tx.pending, tx.payment_channel, tx.merchant_name,
81
- tx.primary_category, tx.detailed_category,
82
- ]);
148
+ const loc = tx.location || {};
149
+ const pfc = tx.personal_finance_category || {};
150
+ const category = Array.isArray(tx.plaid_category) ? tx.plaid_category.join(', ') : (tx.category || null);
151
+ const source_institution = tx.source_institution || tx.institution_name || null;
152
+
153
+ if (slim) {
154
+ await client.query(UPSERT_TRANSACTION_SLIM, [
155
+ tx.transaction_id, tx.date, tx.description_raw || tx.name, tx.amount,
156
+ category, tx.account_id, tx.account_name, tx.account_mask,
157
+ source_institution,
158
+ tx.pending, tx.payment_channel, tx.merchant_name,
159
+ pfc.primary || tx.category_primary || null,
160
+ pfc.detailed || tx.category_detailed || null,
161
+ ]);
162
+ } else {
163
+ await client.query(UPSERT_TRANSACTION_FULL, [
164
+ tx.transaction_id,
165
+ tx.account_id,
166
+ tx.persistent_account_id || null,
167
+ tx.account_name,
168
+ tx.account_mask,
169
+ tx.date,
170
+ tx.authorized_date || null,
171
+ tx.datetime || null,
172
+ tx.authorized_datetime || null,
173
+ tx.description_raw || tx.name,
174
+ tx.merchant_name || null,
175
+ tx.merchant_entity_id || null,
176
+ tx.amount,
177
+ tx.iso_currency_code || null,
178
+ tx.unofficial_currency_code || null,
179
+ tx.pending,
180
+ tx.pending_transaction_id || null,
181
+ tx.check_number || null,
182
+ pfc.primary || tx.category_primary || null,
183
+ pfc.detailed || tx.category_detailed || null,
184
+ tx.payment_channel || null,
185
+ tx.transaction_type || null,
186
+ tx.transaction_code || null,
187
+ loc.address || null,
188
+ loc.city || null,
189
+ loc.region || null,
190
+ loc.postal_code || null,
191
+ loc.country || null,
192
+ loc.lat ?? null,
193
+ loc.lon ?? null,
194
+ tx.website || null,
195
+ tx.logo_url || null,
196
+ source_institution,
197
+ category,
198
+ ]);
199
+ }
83
200
  }
84
201
 
85
202
  for (const acc of accounts) {
86
203
  await client.query(UPSERT_ACCOUNT, [
87
- acc.account_id, acc.name, acc.mask, acc.type, acc.subtype,
88
- acc.balances?.current ?? null,
89
- acc.balances?.available ?? null,
204
+ acc.account_id,
205
+ acc.persistent_account_id || null,
206
+ acc.name,
207
+ acc.official_name || null,
208
+ acc.mask,
209
+ acc.type,
210
+ acc.subtype,
211
+ acc.balances?.current ?? acc.current_balance ?? null,
212
+ acc.balances?.available ?? acc.available_balance ?? null,
213
+ acc.iso_currency_code || null,
90
214
  acc.institution || acc.institution_name || null,
91
215
  ]);
92
216
  }
@@ -2,44 +2,109 @@
2
2
  * sqlite.js - SQLite upsert adapter (MAX tier only)
3
3
  *
4
4
  * Same schema as Postgres. Upserts on primary key — safe to run repeatedly.
5
+ *
6
+ * Full schema matches the Google Sheets extension and Excel add-in (35 transaction columns).
7
+ * Use --slim to write the legacy 14-column subset instead.
5
8
  */
6
9
 
7
- const CREATE_TRANSACTIONS = `
10
+ const CREATE_TRANSACTIONS_FULL = `
8
11
  CREATE TABLE IF NOT EXISTS sheetlink_transactions (
9
- transaction_id TEXT PRIMARY KEY,
10
- date TEXT NOT NULL,
11
- name TEXT,
12
- amount REAL,
13
- category TEXT,
14
- account_id TEXT,
15
- account_name TEXT,
16
- account_mask TEXT,
17
- institution_name TEXT,
18
- pending INTEGER,
19
- payment_channel TEXT,
20
- merchant_name TEXT,
21
- primary_category TEXT,
22
- detailed_category TEXT,
23
- synced_at TEXT DEFAULT (datetime('now'))
12
+ transaction_id TEXT PRIMARY KEY,
13
+ account_id TEXT,
14
+ persistent_account_id TEXT,
15
+ account_name TEXT,
16
+ account_mask TEXT,
17
+ date TEXT NOT NULL,
18
+ authorized_date TEXT,
19
+ datetime TEXT,
20
+ authorized_datetime TEXT,
21
+ description_raw TEXT,
22
+ merchant_name TEXT,
23
+ merchant_entity_id TEXT,
24
+ amount REAL,
25
+ iso_currency_code TEXT,
26
+ unofficial_currency_code TEXT,
27
+ pending INTEGER,
28
+ pending_transaction_id TEXT,
29
+ check_number TEXT,
30
+ category_primary TEXT,
31
+ category_detailed TEXT,
32
+ payment_channel TEXT,
33
+ transaction_type TEXT,
34
+ transaction_code TEXT,
35
+ location_address TEXT,
36
+ location_city TEXT,
37
+ location_region TEXT,
38
+ location_postal_code TEXT,
39
+ location_country TEXT,
40
+ location_lat REAL,
41
+ location_lon REAL,
42
+ website TEXT,
43
+ logo_url TEXT,
44
+ source_institution TEXT,
45
+ category TEXT,
46
+ synced_at TEXT DEFAULT (datetime('now'))
47
+ )`;
48
+
49
+ const CREATE_TRANSACTIONS_SLIM = `
50
+ CREATE TABLE IF NOT EXISTS sheetlink_transactions (
51
+ transaction_id TEXT PRIMARY KEY,
52
+ date TEXT NOT NULL,
53
+ name TEXT,
54
+ amount REAL,
55
+ category TEXT,
56
+ account_id TEXT,
57
+ account_name TEXT,
58
+ account_mask TEXT,
59
+ source_institution TEXT,
60
+ pending INTEGER,
61
+ payment_channel TEXT,
62
+ merchant_name TEXT,
63
+ category_primary TEXT,
64
+ category_detailed TEXT,
65
+ synced_at TEXT DEFAULT (datetime('now'))
24
66
  )`;
25
67
 
26
68
  const CREATE_ACCOUNTS = `
27
69
  CREATE TABLE IF NOT EXISTS sheetlink_accounts (
28
- account_id TEXT PRIMARY KEY,
29
- name TEXT,
30
- mask TEXT,
31
- type TEXT,
32
- subtype TEXT,
33
- balance_current REAL,
34
- balance_available REAL,
35
- institution_name TEXT,
36
- last_synced_at TEXT DEFAULT (datetime('now'))
70
+ account_id TEXT PRIMARY KEY,
71
+ persistent_account_id TEXT,
72
+ name TEXT,
73
+ official_name TEXT,
74
+ mask TEXT,
75
+ type TEXT,
76
+ subtype TEXT,
77
+ current_balance REAL,
78
+ available_balance REAL,
79
+ iso_currency_code TEXT,
80
+ institution TEXT,
81
+ last_synced_at TEXT DEFAULT (datetime('now'))
37
82
  )`;
38
83
 
39
- const UPSERT_TRANSACTION = `
84
+ const UPSERT_TRANSACTION_FULL = `
85
+ INSERT INTO sheetlink_transactions
86
+ (transaction_id, account_id, persistent_account_id, account_name, account_mask,
87
+ date, authorized_date, datetime, authorized_datetime,
88
+ description_raw, merchant_name, merchant_entity_id,
89
+ amount, iso_currency_code, unofficial_currency_code,
90
+ pending, pending_transaction_id, check_number,
91
+ category_primary, category_detailed, payment_channel,
92
+ transaction_type, transaction_code,
93
+ location_address, location_city, location_region, location_postal_code, location_country,
94
+ location_lat, location_lon,
95
+ website, logo_url, source_institution, category, synced_at)
96
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
97
+ ON CONFLICT(transaction_id) DO UPDATE SET
98
+ date=excluded.date, authorized_date=excluded.authorized_date,
99
+ description_raw=excluded.description_raw, amount=excluded.amount, pending=excluded.pending,
100
+ pending_transaction_id=excluded.pending_transaction_id,
101
+ category_primary=excluded.category_primary, category_detailed=excluded.category_detailed,
102
+ category=excluded.category, synced_at=datetime('now')`;
103
+
104
+ const UPSERT_TRANSACTION_SLIM = `
40
105
  INSERT INTO sheetlink_transactions
41
106
  (transaction_id, date, name, amount, category, account_id, account_name, account_mask,
42
- institution_name, pending, payment_channel, merchant_name, primary_category, detailed_category, synced_at)
107
+ source_institution, pending, payment_channel, merchant_name, category_primary, category_detailed, synced_at)
43
108
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
44
109
  ON CONFLICT(transaction_id) DO UPDATE SET
45
110
  date=excluded.date, name=excluded.name, amount=excluded.amount,
@@ -47,41 +112,93 @@ ON CONFLICT(transaction_id) DO UPDATE SET
47
112
 
48
113
  const UPSERT_ACCOUNT = `
49
114
  INSERT INTO sheetlink_accounts
50
- (account_id, name, mask, type, subtype, balance_current, balance_available, institution_name, last_synced_at)
51
- VALUES (?,?,?,?,?,?,?,?,datetime('now'))
115
+ (account_id, persistent_account_id, name, official_name, mask, type, subtype,
116
+ current_balance, available_balance, iso_currency_code, institution, last_synced_at)
117
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
52
118
  ON CONFLICT(account_id) DO UPDATE SET
53
- name=excluded.name, balance_current=excluded.balance_current,
54
- balance_available=excluded.balance_available, last_synced_at=datetime('now')`;
119
+ name=excluded.name, current_balance=excluded.current_balance,
120
+ available_balance=excluded.available_balance, last_synced_at=datetime('now')`;
55
121
 
56
- export async function writeSQLite(transactions, accounts, dbPath) {
57
- // Dynamic import so users without better-sqlite3 don't hit an error on other commands
122
+ export async function writeSQLite(transactions, accounts, dbPath, { slim = false } = {}) {
58
123
  const Database = (await import('better-sqlite3')).default;
59
124
 
60
125
  const db = new Database(dbPath);
61
- db.exec(CREATE_TRANSACTIONS);
126
+ db.exec(slim ? CREATE_TRANSACTIONS_SLIM : CREATE_TRANSACTIONS_FULL);
62
127
  db.exec(CREATE_ACCOUNTS);
63
128
 
64
- const insertTx = db.prepare(UPSERT_TRANSACTION);
129
+ const insertTx = db.prepare(slim ? UPSERT_TRANSACTION_SLIM : UPSERT_TRANSACTION_FULL);
65
130
  const insertAcc = db.prepare(UPSERT_ACCOUNT);
66
131
 
67
132
  const txBatch = db.transaction((txns) => {
68
133
  for (const tx of txns) {
69
- insertTx.run(
70
- tx.transaction_id, tx.date, tx.name, tx.amount, tx.category,
71
- tx.account_id, tx.account_name, tx.account_mask,
72
- tx.institution_name || tx.source_institution,
73
- tx.pending ? 1 : 0, tx.payment_channel, tx.merchant_name,
74
- tx.primary_category, tx.detailed_category,
75
- );
134
+ const loc = tx.location || {};
135
+ const pfc = tx.personal_finance_category || {};
136
+ const category = Array.isArray(tx.plaid_category) ? tx.plaid_category.join(', ') : (tx.category || null);
137
+ const source_institution = tx.source_institution || tx.institution_name || null;
138
+
139
+ if (slim) {
140
+ insertTx.run(
141
+ tx.transaction_id, tx.date, tx.description_raw || tx.name, tx.amount,
142
+ category, tx.account_id, tx.account_name, tx.account_mask,
143
+ source_institution,
144
+ tx.pending ? 1 : 0, tx.payment_channel, tx.merchant_name,
145
+ pfc.primary || tx.category_primary || null,
146
+ pfc.detailed || tx.category_detailed || null,
147
+ );
148
+ } else {
149
+ insertTx.run(
150
+ tx.transaction_id,
151
+ tx.account_id,
152
+ tx.persistent_account_id || null,
153
+ tx.account_name,
154
+ tx.account_mask,
155
+ tx.date,
156
+ tx.authorized_date || null,
157
+ tx.datetime || null,
158
+ tx.authorized_datetime || null,
159
+ tx.description_raw || tx.name,
160
+ tx.merchant_name || null,
161
+ tx.merchant_entity_id || null,
162
+ tx.amount,
163
+ tx.iso_currency_code || null,
164
+ tx.unofficial_currency_code || null,
165
+ tx.pending ? 1 : 0,
166
+ tx.pending_transaction_id || null,
167
+ tx.check_number || null,
168
+ pfc.primary || tx.category_primary || null,
169
+ pfc.detailed || tx.category_detailed || null,
170
+ tx.payment_channel || null,
171
+ tx.transaction_type || null,
172
+ tx.transaction_code || null,
173
+ loc.address || null,
174
+ loc.city || null,
175
+ loc.region || null,
176
+ loc.postal_code || null,
177
+ loc.country || null,
178
+ loc.lat ?? null,
179
+ loc.lon ?? null,
180
+ tx.website || null,
181
+ tx.logo_url || null,
182
+ source_institution,
183
+ category,
184
+ );
185
+ }
76
186
  }
77
187
  });
78
188
 
79
189
  const accBatch = db.transaction((accs) => {
80
190
  for (const acc of accs) {
81
191
  insertAcc.run(
82
- acc.account_id, acc.name, acc.mask, acc.type, acc.subtype,
83
- acc.balances?.current ?? null,
84
- acc.balances?.available ?? null,
192
+ acc.account_id,
193
+ acc.persistent_account_id || null,
194
+ acc.name,
195
+ acc.official_name || null,
196
+ acc.mask,
197
+ acc.type,
198
+ acc.subtype,
199
+ acc.balances?.current ?? acc.current_balance ?? null,
200
+ acc.balances?.available ?? acc.available_balance ?? null,
201
+ acc.iso_currency_code || null,
85
202
  acc.institution || acc.institution_name || null,
86
203
  );
87
204
  }
@@ -8,6 +8,9 @@
8
8
  * csv [--file path] - Snapshot CSV, overwrites each run (PRO+)
9
9
  * postgres://... - Upsert to Postgres (MAX only)
10
10
  * sqlite:///path/to/db - Upsert to SQLite (MAX only)
11
+ *
12
+ * Flags:
13
+ * --slim - Write legacy 14-column schema instead of full 34-column schema
11
14
  */
12
15
 
13
16
  import { listItems, syncItem } from '../api.js';
@@ -17,9 +20,30 @@ import { writeCsv } from '../adapters/csv.js';
17
20
  import { writePostgres } from '../adapters/postgres.js';
18
21
  import { writeSQLite } from '../adapters/sqlite.js';
19
22
 
23
+ /**
24
+ * Enrich transactions with account_name and account_mask from the accounts array.
25
+ * The API returns accounts and transactions separately; transactions only carry account_id.
26
+ */
27
+ function enrichTransactions(transactions, accounts) {
28
+ const accountMap = {};
29
+ for (const acc of accounts) {
30
+ accountMap[acc.account_id] = acc;
31
+ }
32
+ return transactions.map(tx => {
33
+ const acc = accountMap[tx.account_id];
34
+ return {
35
+ ...tx,
36
+ account_name: acc?.name ?? tx.account_name ?? null,
37
+ account_mask: acc?.mask ?? tx.account_mask ?? null,
38
+ persistent_account_id: acc?.persistent_account_id ?? tx.persistent_account_id ?? null,
39
+ };
40
+ });
41
+ }
42
+
20
43
  export async function cmdSync(options) {
21
44
  const output = options.output || getDefaultOutput();
22
45
  const itemId = options.item || null;
46
+ const slim = !!options.slim;
23
47
 
24
48
  // Collect items to sync
25
49
  let itemIds;
@@ -52,11 +76,12 @@ export async function cmdSync(options) {
52
76
  }, 80);
53
77
  try {
54
78
  const result = await syncItem(id);
55
- allTransactions.push(...(result.transactions || []));
79
+ const enriched = enrichTransactions(result.transactions || [], result.accounts || []);
80
+ allTransactions.push(...enriched);
56
81
  allAccounts.push(...(result.accounts || []));
57
- results.push({ item_id: id, ...result });
82
+ results.push({ item_id: id, ...result, transactions: enriched });
58
83
  clearInterval(spinner);
59
- process.stderr.write(`\r✓ Synced ${id} — ${result.transactions?.length ?? 0} transactions\n`);
84
+ process.stderr.write(`\r✓ Synced ${id} — ${enriched.length} transactions\n`);
60
85
  } catch (e) {
61
86
  clearInterval(spinner);
62
87
  if (e.code === 'ITEM_LOGIN_REQUIRED') {
@@ -82,19 +107,18 @@ export async function cmdSync(options) {
82
107
  }
83
108
 
84
109
  if (output === 'csv') {
85
- writeCsv(allTransactions, options.file);
110
+ writeCsv(allTransactions, options.file, { slim });
86
111
  return;
87
112
  }
88
113
 
89
114
  if (output.startsWith('postgres://') || output.startsWith('postgresql://')) {
90
- await writePostgres(allTransactions, allAccounts, output);
115
+ await writePostgres(allTransactions, allAccounts, output, { slim });
91
116
  return;
92
117
  }
93
118
 
94
119
  if (output.startsWith('sqlite://')) {
95
- // sqlite:///absolute/path → /absolute/path, sqlite://relative/path → relative/path
96
120
  const dbPath = output.replace(/^sqlite:\/\//, '') || './sheetlink.db';
97
- writeSQLite(allTransactions, allAccounts, dbPath);
121
+ writeSQLite(allTransactions, allAccounts, dbPath, { slim });
98
122
  return;
99
123
  }
100
124
 
package/src/index.js CHANGED
@@ -42,6 +42,7 @@ program
42
42
  .option('--output <dest>', 'Output destination: json (default), csv, postgres://..., sqlite:///path')
43
43
  .option('--file <path>', 'File path for CSV output (default: ./sheetlink-transactions.csv)')
44
44
  .option('--item <item_id>', 'Sync a specific item only (default: all connected banks)')
45
+ .option('--slim', 'Write 14-column schema instead of full 34-column schema')
45
46
  .addHelpText('after', `
46
47
  Examples:
47
48
  sheetlink sync JSON to stdout (pipeable)
@@ -50,6 +51,7 @@ Examples:
50
51
  sheetlink sync --output csv --file ~/finances.csv CSV to custom path
51
52
  sheetlink sync --output postgres://localhost/mydb Upsert to Postgres (MAX only)
52
53
  sheetlink sync --output sqlite:///~/finance.db Upsert to SQLite (MAX only)
54
+ sheetlink sync --output postgres://localhost/mydb --slim Legacy 14-column schema
53
55
  sheetlink sync --item VBX93wmRY4Iy... Sync one bank only
54
56
  `)
55
57
  .action(cmdSync);