Skip to content

Commit 7d17e56

Browse files
fix: Resolve TypeScript and HTML errors
TypeScript Fixes: - Fixed buildArrayResult type mismatch by converting data to tuples - Removed unused @ts-expect-error directive - Fixed SeriesResponse data type handling - Fixed scalar calculation to use body.data objects directly - Added proper type casting for dynamic metadata field access HTML/JavaScript Fixes: - Removed TypeScript type assertions from sidebar.html - Changed (e.target as HTMLInputElement) to plain JS e.target.files - Changed (event.target?.result as string) to String() conversion - Now fully compatible with browser JavaScript environment Template Features: - exportTemplates() returns JSON data - importTemplates() merges with existing templates - Export/Import buttons in Templates tab UI
1 parent ad64d2f commit 7d17e56

3 files changed

Lines changed: 284 additions & 20 deletions

File tree

README.md

Lines changed: 190 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,195 @@
1-
# DataSetIQ Sheets Add-on (`datasetiq-sheets-addon`)
1+
# DataSetIQ Google Sheets Add-on
22

3-
Google Sheets add-on that exposes DataSetIQ data through custom functions and a sidebar UI.
3+
**Professional Google Sheets add-on for economic and financial data from 15+ global sources.**
44

5-
## Features
6-
- Custom functions: `DSIQ` (spill array with headers, newest-first), `DSIQ_LATEST`, `DSIQ_VALUE` (on-or-before), `DSIQ_YOY`, `DSIQ_META`.
7-
- User key stored in `PropertiesService` (user-scoped). Shared files never leak keys.
8-
- Sidebar states for connect/disconnect, profile/quota display, and search via `/api/public/search`.
9-
- First-run authorize menu (`Extensions > DataSetIQ > Authorize`) to enable `UrlFetchApp`.
10-
- Single retry with exponential backoff for 429/5xx, respects `Retry-After` when provided.
11-
- Date normalization to UTC `YYYY-MM-DD` for cache/API parity.
5+
## 📊 Features
6+
7+
### Custom Functions
8+
- **`DSIQ(seriesId, [freq], [start])`** – Full time-series array with headers (newest-first)
9+
- **`DSIQ_LATEST(seriesId)`** – Most recent value only
10+
- **`DSIQ_VALUE(seriesId, date)`** – Value on or before specified date
11+
- **`DSIQ_YOY(seriesId)`** – Year-over-year growth rate
12+
- **`DSIQ_META(seriesId, field)`** – Metadata (title, units, frequency, etc.)
13+
14+
### Premium Features 🔓
15+
16+
**Formula Builder Wizard** (🔧 Builder tab)
17+
- Step-by-step formula creation with guided inputs
18+
- Supports all DSIQ function types
19+
- Preview before inserting into cells
20+
21+
**Templates System** (📁 Templates tab)
22+
- Scan current sheet for DSIQ formulas
23+
- Save formula collections as reusable templates
24+
- Load templates into new sheets instantly
25+
- Manage saved templates (view, load, delete)
26+
27+
**Multi-Series Insert** (Search tab)
28+
- Select multiple series with checkboxes
29+
- Bulk insert with single click
30+
- Saves time on repetitive data pulls
31+
32+
**Enhanced Sidebar UI** (🔍 Search tab)
33+
- Search across 15 data providers
34+
- Browse by source (FRED, BLS, IMF, OECD, etc.)
35+
- Favorites and Recent tracking
36+
- Live data preview with metadata
37+
- On-demand ingestion for metadata-only datasets
38+
39+
### Data Sources (15 Providers)
40+
- **FRED** – Federal Reserve Economic Data
41+
- **BLS** – Bureau of Labor Statistics
42+
- **BEA** – Bureau of Economic Analysis
43+
- **Census** – US Census Bureau
44+
- **EIA** – Energy Information Administration
45+
- **IMF** – International Monetary Fund
46+
- **OECD** – Organisation for Economic Co-operation
47+
- **World Bank** – Global development data
48+
- **ECB** – European Central Bank
49+
- **Eurostat** – European statistics
50+
- **BOE** – Bank of England
51+
- **ONS** – UK Office for National Statistics
52+
- **StatCan** – Statistics Canada
53+
- **RBA** – Reserve Bank of Australia
54+
- **BOJ** – Bank of Japan
55+
56+
## 🆓 Free vs 💎 Paid Plans
57+
58+
| Feature | Free (No API Key) | Paid (Valid API Key) |
59+
|---------|------------------|----------------------|
60+
| **Custom Functions** | ✅ All functions | ✅ All functions |
61+
| **Observation Limit** | 100 most recent | 1,000 most recent |
62+
| **Search & Insert** | ✅ Basic search | ✅ Enhanced search |
63+
| **Data Preview** | ✅ Available | ✅ Available |
64+
| **Formula Builder** | ❌ Locked | ✅ Unlocked |
65+
| **Templates** | ❌ Locked | ✅ Unlocked |
66+
| **Multi-Insert** | ❌ Not available | ✅ Checkboxes visible |
67+
| **Favorites/Recent** | ✅ Available | ✅ Available |
68+
| **Browse by Source** | ✅ Available | ✅ Available |
69+
70+
**Upgrade Message**: When data is truncated at 100 observations, users see:
71+
> ⚠️ Data limited to 100 most recent observations. Upgrade to a paid plan at datasetiq.com/pricing for up to 1,000 observations per series.
72+
73+
## 🚀 Getting Started
74+
75+
### Installation
76+
1. Install from Google Workspace Marketplace
77+
2. Open Google Sheets → Extensions → DataSetIQ → Open Sidebar
78+
3. First-time: Extensions → DataSetIQ → Authorize (grants network permissions)
79+
80+
### Connecting Your Account
81+
1. Visit [datasetiq.com/dashboard/api-keys](https://datasetiq.com/dashboard/api-keys)
82+
2. Create an API key (free or paid plan)
83+
3. In sidebar, enter your API key and click "Connect Account"
84+
4. Status shows: ✅ Connected - Premium features unlocked
85+
86+
### Using Custom Functions
87+
88+
**Basic Usage:**
89+
```javascript
90+
=DSIQ("FRED-GDP")
91+
// Returns full GDP time-series with Date/Value headers
92+
93+
=DSIQ_LATEST("BLS-CPI")
94+
// Returns latest CPI value
95+
96+
=DSIQ_VALUE("IMF-NGDP", "2023-12-31")
97+
// Returns GDP value on or before Dec 31, 2023
98+
99+
=DSIQ_YOY("FRED-UNRATE")
100+
// Returns year-over-year change in unemployment rate
101+
```
102+
103+
**Advanced Options:**
104+
```javascript
105+
=DSIQ("FRED-GDP", "quarterly", "2020-01-01")
106+
// GDP data, quarterly frequency, from 2020 onwards
107+
108+
=DSIQ_META("FRED-GDP", "title")
109+
// Returns metadata field (title, units, frequency, updated, source)
110+
```
111+
112+
## 📖 User Guides
113+
114+
### Formula Builder Wizard
115+
116+
**Step 1: Choose Function**
117+
- Select from DSIQ, DSIQ_LATEST, DSIQ_VALUE, or DSIQ_YOY
118+
- Each function has different parameters
119+
120+
**Step 2: Enter Series ID**
121+
- Enter the series identifier (e.g., "FRED-GDP")
122+
- Use Search tab to find series IDs
123+
124+
**Step 3: Configure Options** (DSIQ and DSIQ_VALUE only)
125+
- **Frequency**: Optional (e.g., "quarterly", "monthly")
126+
- **Start Date**: Optional (e.g., "2020-01-01")
127+
128+
**Step 4: Insert**
129+
- Click "Insert Formula" to place in active cell
130+
- Formula appears in cell and updates automatically
131+
132+
### Templates Guide
133+
134+
**Creating a Template:**
135+
1. Build your sheet with DSIQ formulas
136+
2. Click **🔧 Builder** tab → Navigate to Templates section
137+
3. Click "🔍 Scan Current Sheet"
138+
4. Review found formulas (count shown)
139+
5. Enter template name (e.g., "Q4 Report")
140+
6. Click "💾 Save Template"
141+
142+
**Loading a Template:**
143+
1. Open new sheet
144+
2. Navigate to Templates tab
145+
3. Find saved template in list
146+
4. Click "📥 Load" button
147+
5. All formulas inserted at original cell positions
148+
149+
**Managing Templates:**
150+
- **View**: See all saved templates with formula count
151+
- **Load**: Insert template into active sheet
152+
- **Delete**: Remove template (🗑️ button)
153+
154+
### Multi-Series Insert
155+
156+
**Selecting Series:**
157+
1. Search for series in Search tab
158+
2. Check boxes appear next to results (paid users only)
159+
3. Select multiple series by clicking checkboxes
160+
4. Counter updates: "Insert X Series" button appears
161+
162+
**Bulk Insert:**
163+
1. Select active cell where you want first series
164+
2. Click "Insert X Series" button
165+
3. Series inserted vertically (one per row)
166+
4. Uses DSIQ_LATEST function by default
167+
168+
## 🛠 Technical Details
169+
170+
### Architecture
171+
- User API key stored in `PropertiesService` (user-scoped, never shared)
172+
- Sidebar UI communicates with `Code.gs` backend via `google.script.run`
173+
- Single retry with exponential backoff for 429/5xx errors
174+
- Respects `Retry-After` headers from API
175+
- Date normalization to UTC `YYYY-MM-DD` format
176+
177+
### Privacy & Security
178+
- **No Key Leakage**: API keys stored per-user, never in spreadsheet
179+
- **Shared Files**: Each user must connect their own account
180+
- **Scopes**: Only requests necessary permissions (UrlFetchApp, Properties, Active Sheet)
181+
182+
### Error Handling
183+
- **No API Key**: Sidebar shows "Connect your account" prompt
184+
- **Invalid Key**: "Invalid API Key. Please reconnect."
185+
- **Rate Limited**: "Rate limited. Please retry shortly."
186+
- **Free Limit**: "Free plan limit reached. Upgrade at datasetiq.com/pricing"
187+
- **Server Error**: "Server unavailable. Please retry."
188+
189+
### Date Normalization
190+
- All dates normalized to UTC `YYYY-MM-DD` format
191+
- Handles MM/DD/YYYY, DD/MM/YYYY, ISO 8601, timestamps
192+
- Frequency and start date parameters also normalized
12193

13194
## Scripts
14195
- `npm run build` – bundle `src/Code.ts` to `Code.js` for `clasp push`.

sidebar.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,52 @@ <h4 style="margin: 8px 0;">Step 3: Options (Optional)</h4>
781781
.deleteTemplate(id);
782782
}
783783

784+
function exportTemplatesFile() {
785+
google.script.run
786+
.withSuccessHandler((result) => {
787+
const blob = new Blob([result.data], { type: 'application/json' });
788+
const url = URL.createObjectURL(blob);
789+
const a = document.createElement('a');
790+
a.href = url;
791+
a.download = `dsiq-templates-${Date.now()}.json`;
792+
document.body.appendChild(a);
793+
a.click();
794+
document.body.removeChild(a);
795+
URL.revokeObjectURL(url);
796+
showStatus('✅ Templates exported to file');
797+
})
798+
.withFailureHandler(showError)
799+
.exportTemplates();
800+
}
801+
802+
function showImportDialog() {
803+
const input = document.createElement('input');
804+
input.type = 'file';
805+
input.accept = '.json';
806+
input.onchange = (e) => {
807+
const file = e.target.files ? e.target.files[0] : null;
808+
if (!file) return;
809+
810+
const reader = new FileReader();
811+
reader.onload = (event) => {
812+
const jsonData = event.target && event.target.result ? String(event.target.result) : '';
813+
google.script.run
814+
.withSuccessHandler((result) => {
815+
if (result.ok) {
816+
showStatus(`✅ Imported ${result.count} templates`);
817+
loadTemplatesList();
818+
} else {
819+
showStatus(`❌ ${result.error || 'Import failed'}`);
820+
}
821+
})
822+
.withFailureHandler(showError)
823+
.importTemplates(jsonData);
824+
};
825+
reader.readAsText(file);
826+
};
827+
input.click();
828+
}
829+
784830
function toggleMultiSelect(seriesId) {
785831
const index = selectedForMultiInsert.indexOf(seriesId);
786832
if (index > -1) {
@@ -971,6 +1017,11 @@ <h3 style="margin: 8px 0 12px;">📁 Templates</h3>
9711017

9721018
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 16px 0;" />
9731019

1020+
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
1021+
<button onclick="exportTemplatesFile()" style="flex: 1; background: #6366f1; color: #fff; padding: 8px;">📤 Export</button>
1022+
<button onclick="showImportDialog()" style="flex: 1; background: #8b5cf6; color: #fff; padding: 8px;">📥 Import</button>
1023+
</div>
1024+
9741025
<h4 style="margin: 8px 0;">Saved Templates</h4>
9751026
<div id="templates-list" class="muted">No templates saved yet.</div>
9761027
</div>

src/Code.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ function DSIQ(seriesId: string, frequency?: string | null, startDate?: any) {
124124
throw new Error(errorMessage);
125125
}
126126
const data = response?.data ?? [];
127-
const result = buildArrayResult(data);
127+
const tuples: [string, number][] = data.map(d => [d.date, d.value]);
128+
const result = buildArrayResult(tuples);
128129

129130
// Add upgrade message for free users if data is truncated at 100 observations
130131
const key = getApiKey();
@@ -175,11 +176,10 @@ function DSIQ_META(seriesId: string, field: string) {
175176
if (errorMessage) {
176177
throw new Error(errorMessage);
177178
}
178-
const dataset = response?.dataset;
179+
const dataset = response?.dataset as any;
179180
if (!dataset || !(normalizedField in dataset)) {
180181
throw new Error(`Metadata "${normalizedField}" not found.`);
181182
}
182-
// @ts-expect-error dynamic access
183183
return dataset[normalizedField];
184184
}
185185

@@ -519,21 +519,20 @@ function fetchSeries(
519519
// Metadata response
520520
transformedResponse = { dataset: body.dataset };
521521
} else if (body.data) {
522-
// Data response - transform [{date, value}] to [[date, value]]
523-
const dataArray = body.data.map((obs: any) => [obs.date, obs.value]);
522+
// Data response - keep as objects for SeriesResponse type
524523
transformedResponse = {
525-
data: dataArray,
524+
data: body.data,
526525
seriesId: body.seriesId,
527526
status: body.status,
528527
message: body.message
529528
};
530529

531530
// Handle scalar modes (latest, value, yoy)
532-
if (mode === 'latest' && dataArray.length > 0) {
533-
const latest = dataArray[dataArray.length - 1];
534-
transformedResponse.scalar = latest[1];
535-
} else if (mode === 'value' && dataArray.length > 0) {
536-
transformedResponse.scalar = dataArray[0][1];
531+
if (mode === 'latest' && body.data.length > 0) {
532+
const latest = body.data[body.data.length - 1];
533+
transformedResponse.scalar = latest.value;
534+
} else if (mode === 'value' && body.data.length > 0) {
535+
transformedResponse.scalar = body.data[0].value;
537536
}
538537
} else {
539538
transformedResponse = body;
@@ -803,6 +802,37 @@ function deleteTemplate(templateId: string) {
803802
return { ok: true };
804803
}
805804

805+
/**
806+
* Templates: Export to JSON string
807+
*/
808+
function exportTemplates(): { data: string } {
809+
const templates = getTemplates();
810+
return { data: JSON.stringify(templates, null, 2) };
811+
}
812+
813+
/**
814+
* Templates: Import from JSON string
815+
*/
816+
function importTemplates(jsonData: string): { ok: boolean; count?: number; error?: string } {
817+
try {
818+
const imported = JSON.parse(jsonData);
819+
if (!Array.isArray(imported)) {
820+
return { ok: false, error: 'Invalid template format' };
821+
}
822+
823+
const existing = getTemplates();
824+
const combined = [...existing, ...imported];
825+
PropertiesService.getUserProperties().setProperty(
826+
TEMPLATES_PROP,
827+
JSON.stringify(combined.slice(0, 50))
828+
);
829+
830+
return { ok: true, count: imported.length };
831+
} catch (err) {
832+
return { ok: false, error: 'Failed to parse JSON' };
833+
}
834+
}
835+
806836
/**
807837
* Multi-insert: Insert multiple series at once
808838
*/
@@ -848,6 +878,8 @@ g.saveTemplate = saveTemplate;
848878
g.getTemplates = getTemplates;
849879
g.loadTemplate = loadTemplate;
850880
g.deleteTemplate = deleteTemplate;
881+
g.exportTemplates = exportTemplates;
882+
g.importTemplates = importTemplates;
851883
g.insertMultipleSeries = insertMultipleSeries;
852884
g.include = include;
853885

0 commit comments

Comments
 (0)