daeda-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/db/config.d.ts +12 -0
- package/dist/db/config.js +53 -0
- package/dist/db/keychain.d.ts +2 -0
- package/dist/db/keychain.js +11 -0
- package/dist/db/schema.d.ts +10 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/sqlite.d.ts +43 -0
- package/dist/db/sqlite.js +280 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -0
- package/dist/sync/csv-loader.d.ts +15 -0
- package/dist/sync/csv-loader.js +450 -0
- package/dist/sync/csv-worker.d.ts +21 -0
- package/dist/sync/csv-worker.js +60 -0
- package/dist/sync/export-api.d.ts +57 -0
- package/dist/sync/export-api.js +355 -0
- package/dist/sync/init-manager.d.ts +5 -0
- package/dist/sync/init-manager.js +274 -0
- package/dist/sync/init-state.d.ts +31 -0
- package/dist/sync/init-state.js +64 -0
- package/dist/sync/seeder.d.ts +1 -0
- package/dist/sync/seeder.js +176 -0
- package/dist/tools/auth.d.ts +26 -0
- package/dist/tools/auth.js +127 -0
- package/dist/tools/query.d.ts +6 -0
- package/dist/tools/query.js +109 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daeda Technologies PTE Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Daeda MCP
|
|
2
|
+
|
|
3
|
+
An MCP server that syncs your HubSpot CRM to a local encrypted database, enabling AI assistants to query your contacts, companies, and deals instantly.
|
|
4
|
+
|
|
5
|
+
## Why Daeda?
|
|
6
|
+
|
|
7
|
+
Querying HubSpot through the API is slow and rate-limited. Daeda solves this by:
|
|
8
|
+
|
|
9
|
+
- **Syncing your entire CRM locally** - Contacts, companies, deals, and all associations
|
|
10
|
+
- **Encrypted storage** - Your data is encrypted at rest using your HubSpot token
|
|
11
|
+
- **Instant queries** - AI assistants can run SQL queries against your local database
|
|
12
|
+
- **Works offline** - Once synced, no internet required for queries
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Full CRM Sync** - Exports all contacts, companies, and deals with all properties
|
|
17
|
+
- **Association Support** - Contact-company, deal-contact, and deal-company relationships
|
|
18
|
+
- **Smart Seeding** - Quick preview of ~1,000 recent deals available in seconds while full sync runs
|
|
19
|
+
- **Resumable Sync** - Interrupted syncs resume automatically on restart
|
|
20
|
+
- **Read-Only Queries** - AI can only SELECT data, never modify your CRM
|
|
21
|
+
|
|
22
|
+
## Use Cases
|
|
23
|
+
|
|
24
|
+
- "Show me all deals closing this month over $50k"
|
|
25
|
+
- "Find contacts at companies in the healthcare industry"
|
|
26
|
+
- "Which deals have no associated contacts?"
|
|
27
|
+
- "List all contacts with a @gmail.com email"
|
|
28
|
+
- "What's the total pipeline value by deal stage?"
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
### HubSpot Private App Setup
|
|
33
|
+
|
|
34
|
+
1. Go to your HubSpot account → Settings → Integrations → Private Apps
|
|
35
|
+
2. Create a new private app
|
|
36
|
+
3. Under "Scopes", enable these permissions:
|
|
37
|
+
- `crm.export` (required for bulk data export)
|
|
38
|
+
- `crm.objects.contacts.read`
|
|
39
|
+
- `crm.objects.companies.read`
|
|
40
|
+
- `crm.objects.deals.read`
|
|
41
|
+
4. Create the app and copy your access token (starts with `pat-`)
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
### npm (recommended)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g daeda-mcp
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### From source
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/daeda-tech/daeda-mcp.git
|
|
55
|
+
cd daeda-mcp
|
|
56
|
+
npm install
|
|
57
|
+
npm run build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
### Claude Desktop
|
|
63
|
+
|
|
64
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"daeda": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "daeda-mcp"],
|
|
72
|
+
"env": {
|
|
73
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Claude Desktop (from source)
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"daeda": {
|
|
86
|
+
"command": "node",
|
|
87
|
+
"args": ["/path/to/daeda-mcp/dist/index.js"],
|
|
88
|
+
"env": {
|
|
89
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Cursor
|
|
97
|
+
|
|
98
|
+
Add to your Cursor MCP settings (`.cursor/mcp.json` in your project or global config):
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"daeda": {
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["-y", "daeda-mcp"],
|
|
106
|
+
"env": {
|
|
107
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Windsurf
|
|
115
|
+
|
|
116
|
+
Add to your Windsurf MCP config (`~/.windsurf/mcp.json`):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"daeda": {
|
|
122
|
+
"command": "npx",
|
|
123
|
+
"args": ["-y", "daeda-mcp"],
|
|
124
|
+
"env": {
|
|
125
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### VS Code with MCP Extension
|
|
133
|
+
|
|
134
|
+
Add to your VS Code settings or MCP extension config:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"mcp.servers": {
|
|
139
|
+
"daeda": {
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["-y", "daeda-mcp"],
|
|
142
|
+
"env": {
|
|
143
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Cline
|
|
151
|
+
|
|
152
|
+
Add to your Cline MCP settings:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"daeda": {
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["-y", "daeda-mcp"],
|
|
160
|
+
"env": {
|
|
161
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Using with Bun (development)
|
|
169
|
+
|
|
170
|
+
If you prefer Bun for faster startup:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"mcpServers": {
|
|
175
|
+
"daeda": {
|
|
176
|
+
"command": "bun",
|
|
177
|
+
"args": ["run", "/path/to/daeda-mcp/src/index.ts"],
|
|
178
|
+
"env": {
|
|
179
|
+
"HS_PRIVATE_TOKEN": "pat-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## How It Works
|
|
187
|
+
|
|
188
|
+
1. **First Launch** - Daeda requests bulk exports from HubSpot's Export API
|
|
189
|
+
2. **Quick Seed** - While exports process, ~1,000 recent deals are fetched via Search API for immediate use
|
|
190
|
+
3. **Full Sync** - Export CSVs are downloaded and streamed into an encrypted SQLite database
|
|
191
|
+
4. **Ready** - AI assistants can now query your full CRM instantly
|
|
192
|
+
|
|
193
|
+
The **quick seed completes in 2-5 minutes**, giving you immediate access to your 1,000 most recent deals and their associated contacts and companies.
|
|
194
|
+
|
|
195
|
+
The **full sync** runs in the background and duration depends on your CRM size:
|
|
196
|
+
- Small CRM (10k records): ~5-10 minutes
|
|
197
|
+
- Medium CRM (100k records): ~30-60 minutes
|
|
198
|
+
- Large CRM (1M+ records): up to 5 hours
|
|
199
|
+
|
|
200
|
+
Progress is shown via the `db_status` tool. You can start querying immediately after the quick seed completes.
|
|
201
|
+
|
|
202
|
+
## Available Tools
|
|
203
|
+
|
|
204
|
+
| Tool | Description |
|
|
205
|
+
|------|-------------|
|
|
206
|
+
| `db_status` | Check sync progress and database health |
|
|
207
|
+
| `get_raw_sql` | Execute SELECT queries against your CRM data |
|
|
208
|
+
|
|
209
|
+
## Database Schema
|
|
210
|
+
|
|
211
|
+
The local database contains:
|
|
212
|
+
|
|
213
|
+
- **contacts** - All contacts with email and full properties as JSON
|
|
214
|
+
- **companies** - All companies with domain and full properties as JSON
|
|
215
|
+
- **deals** - All deals with name and full properties as JSON
|
|
216
|
+
- **contact_company** - Contact to company associations
|
|
217
|
+
- **deal_contact** - Deal to contact associations
|
|
218
|
+
- **deal_company** - Deal to company associations
|
|
219
|
+
|
|
220
|
+
Query any HubSpot property using `json_extract()`:
|
|
221
|
+
|
|
222
|
+
```sql
|
|
223
|
+
SELECT
|
|
224
|
+
json_extract(properties, '$.firstname') as first_name,
|
|
225
|
+
json_extract(properties, '$.lastname') as last_name,
|
|
226
|
+
email
|
|
227
|
+
FROM contacts
|
|
228
|
+
WHERE json_extract(properties, '$.lifecyclestage') = 'customer'
|
|
229
|
+
LIMIT 10
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Data Storage
|
|
233
|
+
|
|
234
|
+
Your CRM data is stored locally at:
|
|
235
|
+
- **macOS/Linux**: `~/.daeda-mcp/data/`
|
|
236
|
+
- **Windows**: `%APPDATA%\daeda-mcp\data\`
|
|
237
|
+
|
|
238
|
+
The database is encrypted using your HubSpot token as the encryption key. If you change tokens, the database will be re-initialized automatically.
|
|
239
|
+
|
|
240
|
+
## Security
|
|
241
|
+
|
|
242
|
+
- All data stays on your machine
|
|
243
|
+
- Database is encrypted at rest
|
|
244
|
+
- Only SELECT queries are allowed
|
|
245
|
+
- Dangerous SQL keywords are blocked
|
|
246
|
+
- Your HubSpot token is never stored (only used for encryption)
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PortalConfig {
|
|
2
|
+
portalId: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
lastSyncAt?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getPortal(portalId: string): PortalConfig | null;
|
|
9
|
+
export declare function listPortals(): PortalConfig[];
|
|
10
|
+
export declare function setPortal(portal: PortalConfig): void;
|
|
11
|
+
export declare function updatePortalLastSync(portalId: string): void;
|
|
12
|
+
export declare function deletePortal(portalId: string): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Portal configuration storage
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DATA_DIR = join(__dirname, '../../data');
|
|
7
|
+
const CONFIG_FILE = join(DATA_DIR, 'config.json');
|
|
8
|
+
function ensureDataDir() {
|
|
9
|
+
if (!existsSync(DATA_DIR)) {
|
|
10
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
ensureDataDir();
|
|
15
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
16
|
+
return { portals: {} };
|
|
17
|
+
}
|
|
18
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
19
|
+
return JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
function saveConfig(config) {
|
|
22
|
+
ensureDataDir();
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
|
+
}
|
|
25
|
+
export function getPortal(portalId) {
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
return config.portals[portalId] || null;
|
|
28
|
+
}
|
|
29
|
+
export function listPortals() {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
return Object.values(config.portals);
|
|
32
|
+
}
|
|
33
|
+
export function setPortal(portal) {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
config.portals[portal.portalId] = portal;
|
|
36
|
+
saveConfig(config);
|
|
37
|
+
}
|
|
38
|
+
export function updatePortalLastSync(portalId) {
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
if (config.portals[portalId]) {
|
|
41
|
+
config.portals[portalId].lastSyncAt = new Date().toISOString();
|
|
42
|
+
saveConfig(config);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function deletePortal(portalId) {
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
if (config.portals[portalId]) {
|
|
48
|
+
delete config.portals[portalId];
|
|
49
|
+
saveConfig(config);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const ENV_VAR_NAME = "HS_PRIVATE_TOKEN";
|
|
2
|
+
export function getHubSpotToken() {
|
|
3
|
+
return process.env[ENV_VAR_NAME] || null;
|
|
4
|
+
}
|
|
5
|
+
export async function getDbEncryptionKey() {
|
|
6
|
+
const key = process.env[ENV_VAR_NAME];
|
|
7
|
+
if (!key) {
|
|
8
|
+
throw new Error(`${ENV_VAR_NAME} environment variable is required for database encryption.`);
|
|
9
|
+
}
|
|
10
|
+
return key;
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const SCHEMA_SQL = "\n-- Contacts table (id, human-readable identifier, last_synced, all properties as JSON)\nCREATE TABLE IF NOT EXISTS contacts (\n id TEXT PRIMARY KEY,\n email TEXT,\n last_synced TEXT,\n properties TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email);\n\n-- Companies table\nCREATE TABLE IF NOT EXISTS companies (\n id TEXT PRIMARY KEY,\n domain TEXT,\n last_synced TEXT,\n properties TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_companies_domain ON companies(domain);\n\n-- Deals table\nCREATE TABLE IF NOT EXISTS deals (\n id TEXT PRIMARY KEY,\n dealname TEXT,\n last_synced TEXT,\n properties TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_deals_dealname ON deals(dealname);\n\n-- Association tables\nCREATE TABLE IF NOT EXISTS contact_company (\n contact_id TEXT,\n company_id TEXT,\n PRIMARY KEY (contact_id, company_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_cc_contact ON contact_company(contact_id);\nCREATE INDEX IF NOT EXISTS idx_cc_company ON contact_company(company_id);\n\nCREATE TABLE IF NOT EXISTS deal_contact (\n deal_id TEXT,\n contact_id TEXT,\n PRIMARY KEY (deal_id, contact_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_dc_deal ON deal_contact(deal_id);\nCREATE INDEX IF NOT EXISTS idx_dc_contact ON deal_contact(contact_id);\n\nCREATE TABLE IF NOT EXISTS deal_company (\n deal_id TEXT,\n company_id TEXT,\n PRIMARY KEY (deal_id, company_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_dco_deal ON deal_company(deal_id);\nCREATE INDEX IF NOT EXISTS idx_dco_company ON deal_company(company_id);\n\n-- Sync metadata table\nCREATE TABLE IF NOT EXISTS sync_metadata (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n";
|
|
2
|
+
export type ObjectType = "contacts" | "companies" | "deals";
|
|
3
|
+
export type AssociationType = "contact_company" | "deal_contact" | "deal_company";
|
|
4
|
+
export interface SyncMetadata {
|
|
5
|
+
initialized_at: string;
|
|
6
|
+
last_synced: string;
|
|
7
|
+
contacts_count: number;
|
|
8
|
+
companies_count: number;
|
|
9
|
+
deals_count: number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const SCHEMA_SQL = `
|
|
2
|
+
-- Contacts table (id, human-readable identifier, last_synced, all properties as JSON)
|
|
3
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
email TEXT,
|
|
6
|
+
last_synced TEXT,
|
|
7
|
+
properties TEXT
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email);
|
|
11
|
+
|
|
12
|
+
-- Companies table
|
|
13
|
+
CREATE TABLE IF NOT EXISTS companies (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
domain TEXT,
|
|
16
|
+
last_synced TEXT,
|
|
17
|
+
properties TEXT
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_companies_domain ON companies(domain);
|
|
21
|
+
|
|
22
|
+
-- Deals table
|
|
23
|
+
CREATE TABLE IF NOT EXISTS deals (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
dealname TEXT,
|
|
26
|
+
last_synced TEXT,
|
|
27
|
+
properties TEXT
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_deals_dealname ON deals(dealname);
|
|
31
|
+
|
|
32
|
+
-- Association tables
|
|
33
|
+
CREATE TABLE IF NOT EXISTS contact_company (
|
|
34
|
+
contact_id TEXT,
|
|
35
|
+
company_id TEXT,
|
|
36
|
+
PRIMARY KEY (contact_id, company_id)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_cc_contact ON contact_company(contact_id);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_cc_company ON contact_company(company_id);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS deal_contact (
|
|
43
|
+
deal_id TEXT,
|
|
44
|
+
contact_id TEXT,
|
|
45
|
+
PRIMARY KEY (deal_id, contact_id)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_dc_deal ON deal_contact(deal_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_dc_contact ON deal_contact(contact_id);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS deal_company (
|
|
52
|
+
deal_id TEXT,
|
|
53
|
+
company_id TEXT,
|
|
54
|
+
PRIMARY KEY (deal_id, company_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_dco_deal ON deal_company(deal_id);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_dco_company ON deal_company(company_id);
|
|
59
|
+
|
|
60
|
+
-- Sync metadata table
|
|
61
|
+
CREATE TABLE IF NOT EXISTS sync_metadata (
|
|
62
|
+
key TEXT PRIMARY KEY,
|
|
63
|
+
value TEXT
|
|
64
|
+
);
|
|
65
|
+
`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Client } from "@libsql/client";
|
|
2
|
+
import { type ObjectType, type AssociationType } from "./schema.js";
|
|
3
|
+
export declare function getDbPath(): string;
|
|
4
|
+
export declare function dbExists(): Promise<boolean>;
|
|
5
|
+
export declare function isDbHealthy(): Promise<boolean>;
|
|
6
|
+
export declare function getDb(): Promise<Client>;
|
|
7
|
+
export declare function closeDb(): Promise<void>;
|
|
8
|
+
export declare function clearTable(table: ObjectType | AssociationType): Promise<void>;
|
|
9
|
+
export declare function insertContact(id: string, email: string | null, properties: Record<string, unknown>): Promise<void>;
|
|
10
|
+
export declare function insertCompany(id: string, domain: string | null, properties: Record<string, unknown>): Promise<void>;
|
|
11
|
+
export declare function insertDeal(id: string, dealname: string | null, properties: Record<string, unknown>): Promise<void>;
|
|
12
|
+
export declare function insertAssociation(associationType: AssociationType, fromId: string, toId: string): Promise<void>;
|
|
13
|
+
export declare function setMetadata(key: string, value: string): Promise<void>;
|
|
14
|
+
export interface ContactRow {
|
|
15
|
+
id: string;
|
|
16
|
+
email: string | null;
|
|
17
|
+
properties: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface CompanyRow {
|
|
20
|
+
id: string;
|
|
21
|
+
domain: string | null;
|
|
22
|
+
properties: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface DealRow {
|
|
25
|
+
id: string;
|
|
26
|
+
dealname: string | null;
|
|
27
|
+
properties: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export interface AssociationRow {
|
|
30
|
+
fromId: string;
|
|
31
|
+
toId: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function batchInsertContacts(rows: ContactRow[]): Promise<void>;
|
|
34
|
+
export declare function batchInsertCompanies(rows: CompanyRow[]): Promise<void>;
|
|
35
|
+
export declare function batchInsertDeals(rows: DealRow[]): Promise<void>;
|
|
36
|
+
export declare function batchInsertAssociations(associationType: AssociationType, rows: AssociationRow[]): Promise<void>;
|
|
37
|
+
export declare function getMetadata(key: string): Promise<string | null>;
|
|
38
|
+
export declare function getAllMetadata(): Promise<Record<string, string>>;
|
|
39
|
+
export declare function getRecordCount(objectType: ObjectType): Promise<number>;
|
|
40
|
+
export declare function executeQuery(sql: string): Promise<{
|
|
41
|
+
columns: string[];
|
|
42
|
+
rows: Record<string, unknown>[];
|
|
43
|
+
}>;
|