You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: auto-indexing, custom indexes, CA cert injection, and auth token fix (#4)
* fix: restore execute bit on 004_readonly_user.sh init script
macOS Git strips execute permissions on checkout, causing Postgres
container init to fail with "bad interpreter: Permission denied" on
first boot. Records the bit in the index so clones on Mac work correctly.
Bumps patch version to 1.1.1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: inject US Signal corporate CA cert into Docker image
The corporate SSL inspection proxy re-signs outbound HTTPS traffic with
the internal US Signal CA chain, causing Node.js inside the container to
reject Salesforce API calls with UNABLE_TO_GET_ISSUER_CERT. Bakes the
full three-cert chain (edge trust → issue CA → root CA) into the image
via update-ca-certificates and points NODE_EXTRA_CA_CERTS at it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(db): auto-index reference fields and add operator-defined custom indexes
Two indexing features:
1. Any field with sfType 'reference' is automatically indexed at table creation
and when a column is added. Index names are derived from object+field names
and hashed if they exceed Postgres's 63-char identifier limit.
2. sfdb.custom_indexes table stores operator-defined indexes (composite, partial,
or any arbitrary expression) that are applied at startup and whenever a table
is created or recreated. CRUD exposed at /api/indexes.
All index creation uses CREATE INDEX CONCURRENTLY IF NOT EXISTS — no table locks,
idempotent, safe to run against a live database.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: document reference-field auto-indexing and custom indexes feature
- README: add Indexing section with curl examples for the /api/indexes API
- docs/scope.md: update DDL table, add sfdb.custom_indexes to schema reference,
add Indexing section with API reference and behavior description
- CLAUDE.md: add both indexing behaviors to Key Design Decisions
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(ui): add custom indexes card to Settings page
Read-only table showing all registered custom indexes for the active org
with a per-row delete button. Fetches on mount and on org switch.
Also fixes GET /api/indexes to return camelCase keys consistent with
other API routes.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(build): sync package-lock.json version to 1.1.1
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(auth): add --verbose flag to sf org display to get real access tokens
sf org display --json redacts the accessToken unless --verbose is passed.
Without it, tokens.json was storing the literal "[REDACTED]..." string,
causing Bulk API 401 INVALID_AUTH_HEADER errors.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(auth): use sf org auth show-access-token to get real access tokens
sf org display --json now permanently redacts accessToken regardless of
flags, pointing to the new dedicated command. Switch to a two-call approach:
sf org display for instanceUrl/username metadata, then sf org auth
show-access-token for the actual token value.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Seth Sheppard <Seth.Sheppard@setshe-mbp-1.rvp.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -107,7 +107,7 @@ sf-db/
107
107
### Postgres
108
108
- Internal app tables → `sfdb` schema
109
109
- Synced Salesforce data → one schema per registered org named `org_<lowercased orgid>` (e.g. `org_00d5g000001abcdeaa`)
110
-
- All `sfdb.*` per-object/per-field tables (`sync_config`, `field_config`, `field_metadata`, `sync_log`, `sync_lock`) are keyed by `(org_id, ...)` with `ON DELETE CASCADE` from `sfdb.orgs`
110
+
- All `sfdb.*` per-object/per-field tables (`sync_config`, `field_config`, `field_metadata`, `sync_log`, `sync_lock`, `custom_indexes`) are keyed by `(org_id, ...)` with `ON DELETE CASCADE` from `sfdb.orgs`
111
111
- The active UI/sync context is stored in `sfdb.active_org` (single row); the API resolves it from `X-Org-Id` request header first, falling back to that pointer
112
112
- Every synced table must have: `id`, `sf_created_at`, `sf_updated_at`, `sf_deleted_at`, `synced_at`
113
113
- Field names are lowercase snake_case versions of SF API names
@@ -162,3 +162,5 @@ All runtime config (active org alias, sync intervals, enabled objects/fields) li
162
162
-**Config in DB, not `.env`.**`.env` is infrastructure only. Org registry, object selection, field selection, and schedule config all live in `sfdb.orgs` / `sfdb.sync_config` / `sfdb.field_config` / `sfdb.app_config`.
163
163
-**Multi-org by schema.** Every registered org gets its own `org_<orgid>` schema. Removing an org drops the schema and cascades through `sfdb.*` via the FKs on `sfdb.orgs(org_id)`.
164
164
-**Schema name is derived from the immutable Salesforce org id**, not the user-editable alias — aliases can be renamed without affecting where the data lives.
165
+
-**Reference fields are auto-indexed.**`createObjectTable` and `addColumn` automatically issue `CREATE INDEX CONCURRENTLY IF NOT EXISTS` for every `sfType === 'reference'` column. Index names are `idx_ref_<object>_<field>`, md5-hashed if > 63 chars.
166
+
-**Custom indexes live in `sfdb.custom_indexes`.** Operator-defined indexes (composite, partial, etc.) are registered via `POST /api/indexes` and re-applied at startup and on table creation. Deleting a registration does not drop the underlying PG index.
Copy file name to clipboardExpand all lines: README.md
+41Lines changed: 41 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -110,6 +110,47 @@ Queries `SELECT Id FROM <Object>` for the full live ID set, diffs against local
110
110
111
111
Sync is serialized per org via `sfdb.sync_lock`, with one lock row per registered org. Different orgs can sync in parallel; overlapping syncs for the same org are blocked. Stale locks (> 30 min) are automatically reclaimed on startup.
112
112
113
+
## Indexing
114
+
115
+
sfetch maintains two layers of indexes on every synced table.
116
+
117
+
### Automatic — reference fields
118
+
119
+
Any Salesforce field of type `reference` (lookup / master-detail) is indexed automatically when the column is created. No configuration needed — sfetch infers this from the field type.
120
+
121
+
### Custom indexes
122
+
123
+
For queries with composite filters, date ranges, or partial index predicates, register an index via the API:
124
+
125
+
```bash
126
+
curl -s -X POST http://localhost:7743/api/indexes \
|`object_api_name`| yes | Salesforce API name of the object |
140
+
|`index_name`| yes | Postgres index name — max 63 characters |
141
+
|`columns`| yes | Array of column names to index |
142
+
|`where_clause`| no | Raw SQL predicate for a partial index |
143
+
144
+
The index is applied immediately on POST and re-applied every time the API starts or the table is recreated. All index creation uses `CREATE INDEX CONCURRENTLY IF NOT EXISTS` — no table locks.
145
+
146
+
```bash
147
+
# List registered custom indexes for the active org
148
+
curl http://localhost:7743/api/indexes
149
+
150
+
# Remove a registration (does not drop the underlying PG index)
| Field re-enabled (reference) | Auto-index the new column |
285
287
| Field disabled | `ALTER TABLE org_<orgid>.<object> DROP COLUMN <field>` |
286
288
287
-
DDL is idempotent where possible (`IF NOT EXISTS`, `IF EXISTS`).
289
+
DDL is idempotent where possible (`IF NOT EXISTS`, `IF EXISTS`). All index creation uses `CREATE INDEX CONCURRENTLY IF NOT EXISTS` — no table locks.
288
290
289
291
---
290
292
@@ -363,6 +365,55 @@ All per-object/per-field tables include `org_id` and have `ON DELETE CASCADE` fr
363
365
- `org_id` text PRIMARY KEY
364
366
- `locked` boolean, `locked_at` timestamptz, `job_type` text
365
367
368
+
**`sfdb.custom_indexes`** — operator-defined indexes applied to org object tables
369
+
- `id` serial PRIMARY KEY
370
+
- `org_id` text (CASCADE from sfdb.orgs)
371
+
- `object_api_name` text
372
+
- `index_name` text (≤ 63 chars, unique per org)
373
+
- `columns` text[] — columns to index
374
+
- `where_clause` text NULL — optional partial index predicate (raw SQL)
375
+
- Applied at startup and whenever the target table is created or recreated
376
+
- CRUD via `GET/POST/DELETE /api/indexes`
377
+
378
+
---
379
+
380
+
## Indexing
381
+
382
+
sfetch maintains two layers of indexes on org object tables.
383
+
384
+
### Auto-indexes (reference fields)
385
+
386
+
Every Salesforce field of type `reference` (lookup / master-detail) is automatically indexed when its column is created. Reference fields are JOIN columns by definition — indexing them requires no configuration.
387
+
388
+
Index names follow the pattern `idx_ref_<object>_<field>`. If the combined name exceeds Postgres's 63-character identifier limit, the name is replaced with `idx_ref_<8-char md5 hash>`.
389
+
390
+
These indexes are created with `CREATE INDEX CONCURRENTLY IF NOT EXISTS`, so they impose no table lock.
391
+
392
+
### Custom indexes (`sfdb.custom_indexes`)
393
+
394
+
For application-specific query patterns (composite filters, date ranges, status columns) that the auto-rule cannot derive, operators register indexes via the API or directly in the table:
Custom indexes are applied immediately on POST and re-applied at every API startup and whenever the target table is created or recreated. They survive container restarts. Removing an entry via `DELETE /api/indexes/:id` removes the registration but does not drop the underlying Postgres index.
410
+
411
+
| Endpoint | Purpose |
412
+
|---|---|
413
+
|`GET /api/indexes`| List all custom indexes for the active org |
414
+
|`POST /api/indexes`| Register and immediately apply a new index |
415
+
|`DELETE /api/indexes/:id`| Remove a registration |
0 commit comments