@steno-ai/supabase-adapter 0.1.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.
Files changed (53) hide show
  1. package/dist/client.d.ts +7 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +8 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/storage.d.ts +126 -0
  10. package/dist/storage.d.ts.map +1 -0
  11. package/dist/storage.js +990 -0
  12. package/dist/storage.js.map +1 -0
  13. package/package.json +33 -0
  14. package/src/client.d.ts +7 -0
  15. package/src/client.d.ts.map +1 -0
  16. package/src/client.js +8 -0
  17. package/src/client.js.map +1 -0
  18. package/src/client.ts +13 -0
  19. package/src/index.d.ts +3 -0
  20. package/src/index.d.ts.map +1 -0
  21. package/src/index.js +3 -0
  22. package/src/index.js.map +1 -0
  23. package/src/index.ts +2 -0
  24. package/src/migrations/001_extensions.sql +4 -0
  25. package/src/migrations/002_create_tenants.sql +14 -0
  26. package/src/migrations/003_create_api_keys.sql +15 -0
  27. package/src/migrations/004_create_sessions.sql +18 -0
  28. package/src/migrations/005_create_extractions.sql +34 -0
  29. package/src/migrations/006_create_facts.sql +68 -0
  30. package/src/migrations/007_create_entities.sql +26 -0
  31. package/src/migrations/008_create_fact_entities.sql +9 -0
  32. package/src/migrations/009_create_edges.sql +22 -0
  33. package/src/migrations/010_create_triggers.sql +18 -0
  34. package/src/migrations/011_create_memory_accesses.sql +20 -0
  35. package/src/migrations/012_create_usage_records.sql +16 -0
  36. package/src/migrations/013_create_functions.sql +19 -0
  37. package/src/migrations/014_create_rls_policies.sql +50 -0
  38. package/src/migrations/015_create_rpc_functions.sql +109 -0
  39. package/src/migrations/016_alter_source_ref.sql +2 -0
  40. package/src/migrations/017_keyword_search_rpc.sql +75 -0
  41. package/src/migrations/018_graph_traverse_rpc.sql +106 -0
  42. package/src/migrations/019_create_webhooks.sql +22 -0
  43. package/src/migrations/020_compound_search_rpc.sql +100 -0
  44. package/src/migrations/021_match_entities_rpc.sql +39 -0
  45. package/src/migrations/022_get_facts_for_entities_rpc.sql +123 -0
  46. package/src/migrations/023_add_event_date.sql +8 -0
  47. package/src/migrations/024_add_source_chunk.sql +3 -0
  48. package/src/migrations/025_update_edge_types_add_signing_key.sql +14 -0
  49. package/src/storage.d.ts +126 -0
  50. package/src/storage.d.ts.map +1 -0
  51. package/src/storage.js +990 -0
  52. package/src/storage.js.map +1 -0
  53. package/src/storage.ts +1180 -0
package/src/storage.js ADDED
@@ -0,0 +1,990 @@
1
+ // =============================================================================
2
+ // camelCase ↔ snake_case conversion utilities
3
+ // =============================================================================
4
+ /**
5
+ * Convert a single camelCase key to snake_case.
6
+ * e.g. tenantId → tenant_id, validFrom → valid_from
7
+ */
8
+ function camelToSnake(key) {
9
+ return key.replace(/([A-Z])/g, (match) => `_${match.toLowerCase()}`);
10
+ }
11
+ /**
12
+ * Convert a single snake_case key to camelCase.
13
+ * e.g. tenant_id → tenantId, valid_from → validFrom
14
+ */
15
+ function snakeToCamel(key) {
16
+ return key.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
17
+ }
18
+ /**
19
+ * Convert all top-level keys of a plain object from camelCase to snake_case.
20
+ * Nested objects (metadata, config, properties, condition) are preserved as-is.
21
+ * Arrays and null values are preserved.
22
+ */
23
+ export function toSnakeCase(obj) {
24
+ const result = {};
25
+ for (const [key, value] of Object.entries(obj)) {
26
+ result[camelToSnake(key)] = value;
27
+ }
28
+ return result;
29
+ }
30
+ /**
31
+ * Convert all top-level keys of a plain object from snake_case to camelCase.
32
+ * Nested objects (metadata, config, properties, condition) are preserved as-is.
33
+ * Arrays and null values are preserved.
34
+ */
35
+ export function toCamelCase(obj) {
36
+ const result = {};
37
+ for (const [key, value] of Object.entries(obj)) {
38
+ result[snakeToCamel(key)] = value;
39
+ }
40
+ return result;
41
+ }
42
+ // =============================================================================
43
+ // Error helpers
44
+ // =============================================================================
45
+ function throwSupabaseError(method, error) {
46
+ throw new Error(`SupabaseStorageAdapter.${method}() failed: ${error?.message ?? 'unknown error'}`);
47
+ }
48
+ // =============================================================================
49
+ // SupabaseStorageAdapter
50
+ // =============================================================================
51
+ export class SupabaseStorageAdapter {
52
+ client;
53
+ constructor(client) {
54
+ this.client = client;
55
+ }
56
+ async ping() {
57
+ const { error } = await this.client.from('tenants').select('id').limit(1);
58
+ return !error;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Tenants
62
+ // ---------------------------------------------------------------------------
63
+ async createTenant(tenant) {
64
+ const row = toSnakeCase(tenant);
65
+ const { data, error } = await this.client
66
+ .from('tenants')
67
+ .insert(row)
68
+ .select()
69
+ .single();
70
+ if (error)
71
+ throwSupabaseError('createTenant', error);
72
+ return toCamelCase(data);
73
+ }
74
+ async getTenant(id) {
75
+ const { data, error } = await this.client
76
+ .from('tenants')
77
+ .select('*')
78
+ .eq('id', id)
79
+ .maybeSingle();
80
+ if (error)
81
+ throwSupabaseError('getTenant', error);
82
+ if (!data)
83
+ return null;
84
+ return toCamelCase(data);
85
+ }
86
+ async getTenantBySlug(slug) {
87
+ const { data, error } = await this.client
88
+ .from('tenants')
89
+ .select('*')
90
+ .eq('slug', slug)
91
+ .maybeSingle();
92
+ if (error)
93
+ throwSupabaseError('getTenantBySlug', error);
94
+ if (!data)
95
+ return null;
96
+ return toCamelCase(data);
97
+ }
98
+ async updateTenant(id, updates) {
99
+ const row = toSnakeCase(updates);
100
+ const { data, error } = await this.client
101
+ .from('tenants')
102
+ .update(row)
103
+ .eq('id', id)
104
+ .select()
105
+ .single();
106
+ if (error)
107
+ throwSupabaseError('updateTenant', error);
108
+ return toCamelCase(data);
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // API Keys
112
+ // ---------------------------------------------------------------------------
113
+ async createApiKey(apiKey) {
114
+ const row = toSnakeCase(apiKey);
115
+ const { data, error } = await this.client
116
+ .from('api_keys')
117
+ .insert(row)
118
+ .select()
119
+ .single();
120
+ if (error)
121
+ throwSupabaseError('createApiKey', error);
122
+ return toCamelCase(data);
123
+ }
124
+ async getApiKeyByPrefix(prefix) {
125
+ const { data, error } = await this.client
126
+ .from('api_keys')
127
+ .select('*')
128
+ .eq('key_prefix', prefix)
129
+ .eq('active', true)
130
+ .maybeSingle();
131
+ if (error)
132
+ throwSupabaseError('getApiKeyByPrefix', error);
133
+ if (!data)
134
+ return null;
135
+ return toCamelCase(data);
136
+ }
137
+ async getApiKeysForTenant(tenantId) {
138
+ const { data, error } = await this.client
139
+ .from('api_keys')
140
+ .select('*')
141
+ .eq('tenant_id', tenantId);
142
+ if (error)
143
+ throwSupabaseError('getApiKeysForTenant', error);
144
+ return (data ?? []).map((row) => toCamelCase(row));
145
+ }
146
+ async revokeApiKey(tenantId, id) {
147
+ const { error } = await this.client
148
+ .from('api_keys')
149
+ .update({ active: false })
150
+ .eq('id', id)
151
+ .eq('tenant_id', tenantId);
152
+ if (error)
153
+ throwSupabaseError('revokeApiKey', error);
154
+ }
155
+ async updateApiKeyLastUsed(id) {
156
+ const { error } = await this.client
157
+ .from('api_keys')
158
+ .update({ last_used_at: new Date().toISOString() })
159
+ .eq('id', id);
160
+ if (error)
161
+ throwSupabaseError('updateApiKeyLastUsed', error);
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Extractions
165
+ // ---------------------------------------------------------------------------
166
+ async createExtraction(extraction) {
167
+ const row = toSnakeCase({
168
+ ...extraction,
169
+ status: 'queued',
170
+ });
171
+ const { data, error } = await this.client
172
+ .from('extractions')
173
+ .insert(row)
174
+ .select()
175
+ .single();
176
+ if (error)
177
+ throwSupabaseError('createExtraction', error);
178
+ return toCamelCase(data);
179
+ }
180
+ async getExtraction(tenantId, id) {
181
+ const { data, error } = await this.client
182
+ .from('extractions')
183
+ .select('*')
184
+ .eq('tenant_id', tenantId)
185
+ .eq('id', id)
186
+ .maybeSingle();
187
+ if (error)
188
+ throwSupabaseError('getExtraction', error);
189
+ if (!data)
190
+ return null;
191
+ return toCamelCase(data);
192
+ }
193
+ async updateExtraction(tenantId, id, updates) {
194
+ const row = toSnakeCase(updates);
195
+ const { data, error } = await this.client
196
+ .from('extractions')
197
+ .update(row)
198
+ .eq('tenant_id', tenantId)
199
+ .eq('id', id)
200
+ .select()
201
+ .single();
202
+ if (error)
203
+ throwSupabaseError('updateExtraction', error);
204
+ return toCamelCase(data);
205
+ }
206
+ async getExtractionByHash(tenantId, inputHash) {
207
+ const { data, error } = await this.client
208
+ .from('extractions')
209
+ .select('*')
210
+ .eq('tenant_id', tenantId)
211
+ .eq('input_hash', inputHash)
212
+ .maybeSingle();
213
+ if (error)
214
+ throwSupabaseError('getExtractionByHash', error);
215
+ if (!data)
216
+ return null;
217
+ return toCamelCase(data);
218
+ }
219
+ async getExtractionsByTenant(tenantId, options) {
220
+ const { limit, cursor } = options;
221
+ let query = this.client
222
+ .from('extractions')
223
+ .select('*')
224
+ .eq('tenant_id', tenantId)
225
+ .order('created_at', { ascending: false })
226
+ .limit(limit + 1);
227
+ if (cursor) {
228
+ query = query.lt('created_at', cursor);
229
+ }
230
+ const { data, error } = await query;
231
+ if (error)
232
+ throwSupabaseError('getExtractionsByTenant', error);
233
+ const rows = data ?? [];
234
+ const hasMore = rows.length > limit;
235
+ const page = hasMore ? rows.slice(0, limit) : rows;
236
+ const extractions = page.map((row) => toCamelCase(row));
237
+ const nextCursor = hasMore && page.length > 0
238
+ ? page[page.length - 1]['created_at']
239
+ : null;
240
+ return { data: extractions, cursor: nextCursor, hasMore };
241
+ }
242
+ // ---------------------------------------------------------------------------
243
+ // Facts
244
+ // ---------------------------------------------------------------------------
245
+ async createFact(fact) {
246
+ const { embedding, ...rest } = fact;
247
+ const row = toSnakeCase(rest);
248
+ // Version defaults to 1 for new facts
249
+ if (!('version' in row)) {
250
+ row['version'] = 1;
251
+ }
252
+ // Lineage ID is required; the interface guarantees it, but set it explicitly
253
+ if (!row['lineage_id']) {
254
+ row['lineage_id'] = fact.id; // fallback: use fact id as its own lineage
255
+ }
256
+ // Embeddings need special handling — pgvector expects a string like '[0.1,0.2,...]'
257
+ if (embedding !== undefined) {
258
+ row['embedding'] = `[${embedding.join(',')}]`;
259
+ }
260
+ const { data, error } = await this.client
261
+ .from('facts')
262
+ .insert(row)
263
+ .select()
264
+ .single();
265
+ if (error)
266
+ throwSupabaseError('createFact', error);
267
+ return toCamelCase(data);
268
+ }
269
+ async getFact(tenantId, id) {
270
+ const { data, error } = await this.client
271
+ .from('facts')
272
+ .select('*')
273
+ .eq('tenant_id', tenantId)
274
+ .eq('id', id)
275
+ .maybeSingle();
276
+ if (error)
277
+ throwSupabaseError('getFact', error);
278
+ if (!data)
279
+ return null;
280
+ return toCamelCase(data);
281
+ }
282
+ async getFactsByIds(tenantId, ids) {
283
+ if (ids.length === 0)
284
+ return [];
285
+ const { data, error } = await this.client
286
+ .from('facts')
287
+ .select('*')
288
+ .eq('tenant_id', tenantId)
289
+ .in('id', ids);
290
+ if (error)
291
+ throwSupabaseError('getFactsByIds', error);
292
+ return (data ?? []).map((row) => toCamelCase(row));
293
+ }
294
+ async getFactsByLineage(tenantId, lineageId) {
295
+ const { data, error } = await this.client
296
+ .from('facts')
297
+ .select('*')
298
+ .eq('tenant_id', tenantId)
299
+ .eq('lineage_id', lineageId)
300
+ .order('version', { ascending: true });
301
+ if (error)
302
+ throwSupabaseError('getFactsByLineage', error);
303
+ return (data ?? []).map((row) => toCamelCase(row));
304
+ }
305
+ async invalidateFact(tenantId, id) {
306
+ const { error } = await this.client
307
+ .from('facts')
308
+ .update({ valid_until: new Date().toISOString() })
309
+ .eq('tenant_id', tenantId)
310
+ .eq('id', id);
311
+ if (error)
312
+ throwSupabaseError('invalidateFact', error);
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Entities
316
+ // ---------------------------------------------------------------------------
317
+ async createEntity(entity) {
318
+ const { embedding, ...rest } = entity;
319
+ const row = toSnakeCase(rest);
320
+ if (embedding !== undefined) {
321
+ row['embedding'] = `[${embedding.join(',')}]`;
322
+ }
323
+ const { data, error } = await this.client
324
+ .from('entities')
325
+ .insert(row)
326
+ .select()
327
+ .single();
328
+ if (error)
329
+ throwSupabaseError('createEntity', error);
330
+ return toCamelCase(data);
331
+ }
332
+ async getEntity(tenantId, id) {
333
+ const { data, error } = await this.client
334
+ .from('entities')
335
+ .select('*')
336
+ .eq('tenant_id', tenantId)
337
+ .eq('id', id)
338
+ .maybeSingle();
339
+ if (error)
340
+ throwSupabaseError('getEntity', error);
341
+ if (!data)
342
+ return null;
343
+ return toCamelCase(data);
344
+ }
345
+ async findEntityByCanonicalName(tenantId, canonicalName, entityType) {
346
+ const { data, error } = await this.client
347
+ .from('entities')
348
+ .select('*')
349
+ .eq('tenant_id', tenantId)
350
+ .eq('canonical_name', canonicalName)
351
+ .eq('entity_type', entityType)
352
+ .maybeSingle();
353
+ if (error)
354
+ throwSupabaseError('findEntityByCanonicalName', error);
355
+ if (!data)
356
+ return null;
357
+ return toCamelCase(data);
358
+ }
359
+ async findEntitiesByEmbedding(tenantId, embedding, limit, minSimilarity = 0.3) {
360
+ const { data, error } = await this.client.rpc('match_entities', {
361
+ query_embedding: JSON.stringify(embedding),
362
+ match_tenant_id: tenantId,
363
+ match_count: limit,
364
+ min_similarity: minSimilarity,
365
+ });
366
+ if (error)
367
+ throwSupabaseError('findEntitiesByEmbedding', error);
368
+ return (data ?? []).map((row) => ({
369
+ entity: toCamelCase(row),
370
+ similarity: row['similarity'],
371
+ }));
372
+ }
373
+ // ---------------------------------------------------------------------------
374
+ // Fact-Entity junction
375
+ // ---------------------------------------------------------------------------
376
+ async linkFactEntity(factId, entityId, role) {
377
+ const { error } = await this.client.from('fact_entities').insert({
378
+ fact_id: factId,
379
+ entity_id: entityId,
380
+ role,
381
+ });
382
+ if (error)
383
+ throwSupabaseError('linkFactEntity', error);
384
+ }
385
+ // ---------------------------------------------------------------------------
386
+ // Edges
387
+ // ---------------------------------------------------------------------------
388
+ async createEdge(edge) {
389
+ const row = toSnakeCase(edge);
390
+ const { data, error } = await this.client
391
+ .from('edges')
392
+ .insert(row)
393
+ .select()
394
+ .single();
395
+ if (error)
396
+ throwSupabaseError('createEdge', error);
397
+ return toCamelCase(data);
398
+ }
399
+ // ---------------------------------------------------------------------------
400
+ // Vector search
401
+ // ---------------------------------------------------------------------------
402
+ async vectorSearch(options) {
403
+ const { embedding, tenantId, scope, scopeId, limit, minSimilarity } = options;
404
+ // Uses a Postgres function `match_facts` that must be created in migrations.
405
+ // See: packages/db/migrations/match_facts.sql
406
+ const { data, error } = await this.client.rpc('match_facts', {
407
+ query_embedding: `[${embedding.join(',')}]`,
408
+ match_tenant_id: tenantId,
409
+ match_scope: scope,
410
+ match_scope_id: scopeId,
411
+ match_count: limit,
412
+ min_similarity: minSimilarity ?? 0,
413
+ match_as_of: options.asOf?.toISOString() ?? null,
414
+ });
415
+ if (error)
416
+ throwSupabaseError('vectorSearch', error);
417
+ return (data ?? []).map((row) => ({
418
+ fact: toCamelCase(row),
419
+ similarity: row['similarity'],
420
+ }));
421
+ }
422
+ // ---------------------------------------------------------------------------
423
+ // Usage
424
+ // ---------------------------------------------------------------------------
425
+ async incrementUsage(tenantId, tokens, queries, extractions, costUsd) {
426
+ // Call the increment_usage Postgres RPC function which atomically
427
+ // increments existing totals via INSERT ... ON CONFLICT DO UPDATE.
428
+ // This avoids the bug where .upsert() replaces values instead of adding.
429
+ const { error } = await this.client.rpc('increment_usage', {
430
+ p_tenant_id: tenantId,
431
+ p_tokens: tokens,
432
+ p_queries: queries,
433
+ p_extractions: extractions,
434
+ p_cost_usd: costUsd,
435
+ });
436
+ if (error)
437
+ throwSupabaseError('incrementUsage', error);
438
+ }
439
+ async getUsage(tenantId, periodStart) {
440
+ const { data, error } = await this.client
441
+ .from('usage_records')
442
+ .select('*')
443
+ .eq('tenant_id', tenantId)
444
+ .eq('period_start', periodStart.toISOString())
445
+ .maybeSingle();
446
+ if (error)
447
+ throwSupabaseError('getUsage', error);
448
+ if (!data)
449
+ return null;
450
+ return toCamelCase(data);
451
+ }
452
+ async getCurrentUsage(tenantId) {
453
+ const now = new Date();
454
+ const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
455
+ return this.getUsage(tenantId, periodStart);
456
+ }
457
+ // ---------------------------------------------------------------------------
458
+ // Stubs — implemented in Plan 3 (Retrieval Engine)
459
+ // ---------------------------------------------------------------------------
460
+ async getFactsByScope(tenantId, scope, scopeId, options) {
461
+ const { limit, cursor } = options;
462
+ let query = this.client
463
+ .from('facts')
464
+ .select('*')
465
+ .eq('tenant_id', tenantId)
466
+ .eq('scope', scope)
467
+ .eq('scope_id', scopeId)
468
+ .order('created_at', { ascending: false })
469
+ .limit(limit + 1);
470
+ if (cursor) {
471
+ query = query.lt('created_at', cursor);
472
+ }
473
+ const { data, error } = await query;
474
+ if (error)
475
+ throwSupabaseError('getFactsByScope', error);
476
+ const rows = data ?? [];
477
+ const hasMore = rows.length > limit;
478
+ const page = hasMore ? rows.slice(0, limit) : rows;
479
+ const facts = page.map((row) => toCamelCase(row));
480
+ const nextCursor = hasMore && page.length > 0
481
+ ? page[page.length - 1]['created_at']
482
+ : null;
483
+ return { data: facts, cursor: nextCursor, hasMore };
484
+ }
485
+ async purgeFacts(tenantId, scope, scopeId) {
486
+ // First, get the IDs of facts to be purged so we can clean up related tables
487
+ const { data: factRows, error: fetchError } = await this.client
488
+ .from('facts')
489
+ .select('id')
490
+ .eq('tenant_id', tenantId)
491
+ .eq('scope', scope)
492
+ .eq('scope_id', scopeId);
493
+ if (fetchError)
494
+ throwSupabaseError('purgeFacts', fetchError);
495
+ const factIds = (factRows ?? []).map((row) => row['id']);
496
+ if (factIds.length === 0)
497
+ return 0;
498
+ // Delete related fact_entities
499
+ const { error: feError } = await this.client
500
+ .from('fact_entities')
501
+ .delete()
502
+ .in('fact_id', factIds);
503
+ if (feError)
504
+ throwSupabaseError('purgeFacts', feError);
505
+ // Delete related edges (where fact_id references one of these facts)
506
+ const { error: edgeError } = await this.client
507
+ .from('edges')
508
+ .delete()
509
+ .in('fact_id', factIds);
510
+ if (edgeError)
511
+ throwSupabaseError('purgeFacts', edgeError);
512
+ // Delete the facts themselves
513
+ const { error: deleteError } = await this.client
514
+ .from('facts')
515
+ .delete()
516
+ .eq('tenant_id', tenantId)
517
+ .eq('scope', scope)
518
+ .eq('scope_id', scopeId);
519
+ if (deleteError)
520
+ throwSupabaseError('purgeFacts', deleteError);
521
+ // Also delete extraction records for this scope so dedup cache doesn't serve stale results
522
+ const { error: extractionError } = await this.client
523
+ .from('extractions')
524
+ .delete()
525
+ .eq('tenant_id', tenantId)
526
+ .eq('scope', scope)
527
+ .eq('scope_id', scopeId);
528
+ if (extractionError)
529
+ throwSupabaseError('purgeFacts', extractionError);
530
+ return factIds.length;
531
+ }
532
+ async updateDecayScores(tenantId, facts) {
533
+ // Batch update each fact's decay_score (and optionally last_accessed, frequency, importance)
534
+ for (const fact of facts) {
535
+ const updates = {
536
+ decay_score: fact.decayScore,
537
+ };
538
+ if (fact.lastAccessed !== undefined) {
539
+ updates['last_accessed'] = fact.lastAccessed.toISOString();
540
+ }
541
+ if (fact.frequency !== undefined) {
542
+ updates['frequency'] = fact.frequency;
543
+ }
544
+ if (fact.importance !== undefined) {
545
+ updates['importance'] = fact.importance;
546
+ }
547
+ const { error } = await this.client
548
+ .from('facts')
549
+ .update(updates)
550
+ .eq('tenant_id', tenantId)
551
+ .eq('id', fact.id);
552
+ if (error)
553
+ throwSupabaseError('updateDecayScores', error);
554
+ }
555
+ }
556
+ async keywordSearch(options) {
557
+ const { query, tenantId, scope, scopeId, limit, asOf } = options;
558
+ const { data, error } = await this.client.rpc('keyword_search_facts', {
559
+ search_query: query,
560
+ match_tenant_id: tenantId,
561
+ match_scope: scope,
562
+ match_scope_id: scopeId,
563
+ match_count: limit,
564
+ match_as_of: asOf?.toISOString() ?? null,
565
+ });
566
+ if (error)
567
+ throwSupabaseError('keywordSearch', error);
568
+ return (data ?? []).map((row) => {
569
+ const rankScore = row['rank_score'];
570
+ const converted = toCamelCase(row);
571
+ return {
572
+ fact: converted,
573
+ rankScore,
574
+ };
575
+ });
576
+ }
577
+ async compoundSearch(options) {
578
+ const { data, error } = await this.client.rpc('steno_search', {
579
+ query_embedding: `[${options.embedding.join(',')}]`,
580
+ search_query: options.query,
581
+ match_tenant_id: options.tenantId,
582
+ match_scope: options.scope,
583
+ match_scope_id: options.scopeId,
584
+ match_count: options.limit,
585
+ min_similarity: options.minSimilarity ?? 0,
586
+ });
587
+ if (error)
588
+ throwSupabaseError('compoundSearch', error);
589
+ return (data ?? []).map((row) => ({
590
+ source: row['source'],
591
+ fact: toCamelCase(row),
592
+ relevanceScore: row['relevance_score'],
593
+ }));
594
+ }
595
+ async getEntitiesForTenant(tenantId, options) {
596
+ const { limit, cursor } = options;
597
+ let query = this.client
598
+ .from('entities')
599
+ .select('*')
600
+ .eq('tenant_id', tenantId)
601
+ .order('created_at', { ascending: true })
602
+ .limit(limit + 1); // fetch one extra to determine hasMore
603
+ if (cursor) {
604
+ query = query.gt('created_at', cursor);
605
+ }
606
+ const { data, error } = await query;
607
+ if (error)
608
+ throwSupabaseError('getEntitiesForTenant', error);
609
+ const rows = data ?? [];
610
+ const hasMore = rows.length > limit;
611
+ const page = hasMore ? rows.slice(0, limit) : rows;
612
+ const entities = page.map((row) => toCamelCase(row));
613
+ const nextCursor = hasMore && page.length > 0
614
+ ? page[page.length - 1]['created_at']
615
+ : null;
616
+ return { data: entities, cursor: nextCursor, hasMore };
617
+ }
618
+ async getEntitiesForFact(factId) {
619
+ // First, get entity IDs from the junction table
620
+ const { data: junctionRows, error: junctionError } = await this.client
621
+ .from('fact_entities')
622
+ .select('entity_id')
623
+ .eq('fact_id', factId);
624
+ if (junctionError)
625
+ throwSupabaseError('getEntitiesForFact', junctionError);
626
+ if (!junctionRows || junctionRows.length === 0)
627
+ return [];
628
+ const entityIds = junctionRows.map((row) => row['entity_id']);
629
+ // Then fetch the full entity records
630
+ const { data, error } = await this.client
631
+ .from('entities')
632
+ .select('*')
633
+ .in('id', entityIds);
634
+ if (error)
635
+ throwSupabaseError('getEntitiesForFact', error);
636
+ return (data ?? []).map((row) => toCamelCase(row));
637
+ }
638
+ async getFactsForEntity(tenantId, entityId, options) {
639
+ const { limit, cursor } = options;
640
+ // Use a join via PostgREST's !inner syntax to avoid URL-length issues
641
+ // with large IN clauses (408 UUIDs = ~15KB URL, exceeds PostgREST limit)
642
+ let query = this.client
643
+ .from('fact_entities')
644
+ .select('fact_id, facts!inner(*)')
645
+ .eq('entity_id', entityId)
646
+ .eq('facts.tenant_id', tenantId)
647
+ .order('created_at', { ascending: false, referencedTable: 'facts' })
648
+ .limit(limit + 1);
649
+ if (cursor) {
650
+ query = query.lt('facts.created_at', cursor);
651
+ }
652
+ const { data, error } = await query;
653
+ if (error)
654
+ throwSupabaseError('getFactsForEntity', error);
655
+ const rows = data ?? [];
656
+ const hasMore = rows.length > limit;
657
+ const page = hasMore ? rows.slice(0, limit) : rows;
658
+ const facts = page.map((row) => {
659
+ // Join returns { fact_id, facts: { ...fact_columns } }
660
+ const factRow = row['facts'];
661
+ return toCamelCase(factRow);
662
+ });
663
+ const nextCursor = hasMore && page.length > 0
664
+ ? page[page.length - 1]['facts']['created_at']
665
+ : null;
666
+ return { data: facts, cursor: nextCursor, hasMore };
667
+ }
668
+ async getFactsForEntities(tenantId, entityIds, perEntityLimit) {
669
+ if (entityIds.length === 0)
670
+ return [];
671
+ const { data, error } = await this.client.rpc('get_facts_for_entities', {
672
+ match_tenant_id: tenantId,
673
+ entity_ids: entityIds,
674
+ per_entity_limit: perEntityLimit,
675
+ });
676
+ if (error)
677
+ throwSupabaseError('getFactsForEntities', error);
678
+ return (data ?? []).map((row) => {
679
+ const entityId = row['entity_id'];
680
+ // Build fact from the row (all fact columns are returned)
681
+ const factRow = { ...row };
682
+ delete factRow['entity_id'];
683
+ factRow['id'] = factRow['fact_id'];
684
+ delete factRow['fact_id'];
685
+ return {
686
+ entityId,
687
+ fact: toCamelCase(factRow),
688
+ };
689
+ });
690
+ }
691
+ async getEdgesForEntity(tenantId, entityId) {
692
+ const { data, error } = await this.client
693
+ .from('edges')
694
+ .select('*')
695
+ .eq('tenant_id', tenantId)
696
+ .or(`source_id.eq.${entityId},target_id.eq.${entityId}`);
697
+ if (error)
698
+ throwSupabaseError('getEdgesForEntity', error);
699
+ return (data ?? []).map((row) => toCamelCase(row));
700
+ }
701
+ async graphTraversal(options) {
702
+ const { data, error } = await this.client.rpc('graph_traverse', {
703
+ match_tenant_id: options.tenantId,
704
+ seed_entity_ids: options.entityIds,
705
+ max_depth: options.maxDepth,
706
+ max_entities: options.maxEntities,
707
+ match_as_of: options.asOf?.toISOString() ?? null,
708
+ });
709
+ if (error)
710
+ throwSupabaseError('graphTraversal', error);
711
+ const rows = (data ?? []);
712
+ // Deduplicate entities by id
713
+ const entityMap = new Map();
714
+ const edgeMap = new Map();
715
+ for (const row of rows) {
716
+ const entityId = row['entity_id'];
717
+ if (!entityMap.has(entityId)) {
718
+ entityMap.set(entityId, {
719
+ id: entityId,
720
+ tenantId: options.tenantId,
721
+ name: row['entity_name'],
722
+ entityType: row['entity_type'],
723
+ canonicalName: row['canonical_name'],
724
+ properties: row['properties'] ?? {},
725
+ embeddingModel: null,
726
+ embeddingDim: null,
727
+ mergeTargetId: null,
728
+ createdAt: new Date(),
729
+ updatedAt: new Date(),
730
+ });
731
+ }
732
+ // Filter out null edges (seed entities at depth 0 have null edge fields)
733
+ const edgeId = row['edge_id'];
734
+ if (edgeId && !edgeMap.has(edgeId)) {
735
+ edgeMap.set(edgeId, {
736
+ id: edgeId,
737
+ tenantId: options.tenantId,
738
+ sourceId: row['edge_source_id'],
739
+ targetId: row['edge_target_id'],
740
+ relation: row['edge_relation'],
741
+ edgeType: row['edge_type'],
742
+ weight: row['edge_weight'] ?? 1.0,
743
+ validFrom: row['edge_valid_from'] ? new Date(row['edge_valid_from']) : new Date(),
744
+ validUntil: row['edge_valid_until'] ? new Date(row['edge_valid_until']) : null,
745
+ factId: null,
746
+ confidence: row['edge_confidence'] ?? 1.0,
747
+ metadata: {},
748
+ createdAt: new Date(),
749
+ });
750
+ }
751
+ }
752
+ return {
753
+ entities: Array.from(entityMap.values()),
754
+ edges: Array.from(edgeMap.values()),
755
+ };
756
+ }
757
+ async createTrigger(trigger) {
758
+ const row = toSnakeCase(trigger);
759
+ const { data, error } = await this.client
760
+ .from('triggers')
761
+ .insert(row)
762
+ .select()
763
+ .single();
764
+ if (error)
765
+ throwSupabaseError('createTrigger', error);
766
+ return toCamelCase(data);
767
+ }
768
+ async getTrigger(tenantId, id) {
769
+ const { data, error } = await this.client
770
+ .from('triggers')
771
+ .select('*')
772
+ .eq('tenant_id', tenantId)
773
+ .eq('id', id)
774
+ .maybeSingle();
775
+ if (error)
776
+ throwSupabaseError('getTrigger', error);
777
+ if (!data)
778
+ return null;
779
+ return toCamelCase(data);
780
+ }
781
+ async getActiveTriggers(tenantId, scope, scopeId) {
782
+ const { data, error } = await this.client
783
+ .from('triggers')
784
+ .select('*')
785
+ .eq('tenant_id', tenantId)
786
+ .eq('scope', scope)
787
+ .eq('scope_id', scopeId)
788
+ .eq('active', true)
789
+ .order('priority', { ascending: false });
790
+ if (error)
791
+ throwSupabaseError('getActiveTriggers', error);
792
+ return (data ?? []).map((row) => toCamelCase(row));
793
+ }
794
+ async updateTrigger(tenantId, id, updates) {
795
+ const row = toSnakeCase(updates);
796
+ const { data, error } = await this.client
797
+ .from('triggers')
798
+ .update(row)
799
+ .eq('tenant_id', tenantId)
800
+ .eq('id', id)
801
+ .select()
802
+ .single();
803
+ if (error)
804
+ throwSupabaseError('updateTrigger', error);
805
+ return toCamelCase(data);
806
+ }
807
+ async deleteTrigger(tenantId, id) {
808
+ const { error } = await this.client
809
+ .from('triggers')
810
+ .delete()
811
+ .eq('tenant_id', tenantId)
812
+ .eq('id', id);
813
+ if (error)
814
+ throwSupabaseError('deleteTrigger', error);
815
+ }
816
+ async incrementTriggerFired(tenantId, id) {
817
+ const { error } = await this.client.rpc('increment_trigger_fired', {
818
+ p_tenant_id: tenantId,
819
+ p_trigger_id: id,
820
+ });
821
+ if (error)
822
+ throwSupabaseError('incrementTriggerFired', error);
823
+ }
824
+ async createMemoryAccess(access) {
825
+ const row = toSnakeCase(access);
826
+ const { data, error } = await this.client
827
+ .from('memory_accesses')
828
+ .insert(row)
829
+ .select()
830
+ .single();
831
+ if (error)
832
+ throwSupabaseError('createMemoryAccess', error);
833
+ return toCamelCase(data);
834
+ }
835
+ async updateFeedback(tenantId, factId, feedback) {
836
+ // Update the MOST RECENT memory access for this fact
837
+ const { data: accessRows, error: findError } = await this.client
838
+ .from('memory_accesses')
839
+ .select('id')
840
+ .eq('tenant_id', tenantId)
841
+ .eq('fact_id', factId)
842
+ .order('accessed_at', { ascending: false })
843
+ .limit(1);
844
+ if (findError)
845
+ throwSupabaseError('updateFeedback', findError);
846
+ if (!accessRows || accessRows.length === 0)
847
+ return;
848
+ const accessId = accessRows[0]['id'];
849
+ const { error } = await this.client
850
+ .from('memory_accesses')
851
+ .update({
852
+ was_useful: feedback.wasUseful,
853
+ feedback_type: feedback.feedbackType,
854
+ feedback_detail: feedback.feedbackDetail ?? null,
855
+ was_corrected: feedback.wasCorrected ?? false,
856
+ })
857
+ .eq('id', accessId);
858
+ if (error)
859
+ throwSupabaseError('updateFeedback', error);
860
+ }
861
+ async createSession(session) {
862
+ const row = toSnakeCase(session);
863
+ const { data, error } = await this.client
864
+ .from('sessions')
865
+ .insert(row)
866
+ .select()
867
+ .single();
868
+ if (error)
869
+ throwSupabaseError('createSession', error);
870
+ return toCamelCase(data);
871
+ }
872
+ async getSession(tenantId, id) {
873
+ const { data, error } = await this.client
874
+ .from('sessions')
875
+ .select('*')
876
+ .eq('tenant_id', tenantId)
877
+ .eq('id', id)
878
+ .maybeSingle();
879
+ if (error)
880
+ throwSupabaseError('getSession', error);
881
+ if (!data)
882
+ return null;
883
+ return toCamelCase(data);
884
+ }
885
+ async endSession(tenantId, id, summary, topics) {
886
+ const updates = {
887
+ ended_at: new Date().toISOString(),
888
+ };
889
+ if (summary !== undefined)
890
+ updates['summary'] = summary;
891
+ if (topics !== undefined)
892
+ updates['topics'] = topics;
893
+ const { data, error } = await this.client
894
+ .from('sessions')
895
+ .update(updates)
896
+ .eq('tenant_id', tenantId)
897
+ .eq('id', id)
898
+ .select()
899
+ .single();
900
+ if (error)
901
+ throwSupabaseError('endSession', error);
902
+ return toCamelCase(data);
903
+ }
904
+ async getSessionsByScope(tenantId, scope, scopeId, options) {
905
+ const { limit, cursor } = options;
906
+ let query = this.client
907
+ .from('sessions')
908
+ .select('*')
909
+ .eq('tenant_id', tenantId)
910
+ .eq('scope', scope)
911
+ .eq('scope_id', scopeId)
912
+ .order('started_at', { ascending: false })
913
+ .limit(limit + 1);
914
+ if (cursor) {
915
+ query = query.lt('started_at', cursor);
916
+ }
917
+ const { data, error } = await query;
918
+ if (error)
919
+ throwSupabaseError('getSessionsByScope', error);
920
+ const rows = data ?? [];
921
+ const hasMore = rows.length > limit;
922
+ const page = hasMore ? rows.slice(0, limit) : rows;
923
+ const sessions = page.map((row) => toCamelCase(row));
924
+ const nextCursor = hasMore && page.length > 0
925
+ ? page[page.length - 1]['started_at']
926
+ : null;
927
+ return { data: sessions, cursor: nextCursor, hasMore };
928
+ }
929
+ // ---------------------------------------------------------------------------
930
+ // Webhooks
931
+ // ---------------------------------------------------------------------------
932
+ async createWebhook(webhook) {
933
+ // CreateWebhook has `secret` but we don't store it separately — signingKey holds the raw
934
+ // secret for HMAC signing, and secretHash holds the hashed version.
935
+ // Strip `secret` before inserting; it's not a DB column.
936
+ const { secret: _secret, ...rest } = webhook;
937
+ const row = toSnakeCase(rest);
938
+ const { data, error } = await this.client
939
+ .from('webhooks')
940
+ .insert(row)
941
+ .select()
942
+ .single();
943
+ if (error)
944
+ throwSupabaseError('createWebhook', error);
945
+ return toCamelCase(data);
946
+ }
947
+ async getWebhook(tenantId, id) {
948
+ const { data, error } = await this.client
949
+ .from('webhooks')
950
+ .select('*')
951
+ .eq('tenant_id', tenantId)
952
+ .eq('id', id)
953
+ .maybeSingle();
954
+ if (error)
955
+ throwSupabaseError('getWebhook', error);
956
+ if (!data)
957
+ return null;
958
+ return toCamelCase(data);
959
+ }
960
+ async getWebhooksForTenant(tenantId) {
961
+ const { data, error } = await this.client
962
+ .from('webhooks')
963
+ .select('*')
964
+ .eq('tenant_id', tenantId);
965
+ if (error)
966
+ throwSupabaseError('getWebhooksForTenant', error);
967
+ return (data ?? []).map((row) => toCamelCase(row));
968
+ }
969
+ async getWebhooksByEvent(tenantId, event) {
970
+ const { data, error } = await this.client
971
+ .from('webhooks')
972
+ .select('*')
973
+ .eq('tenant_id', tenantId)
974
+ .eq('active', true)
975
+ .contains('events', [event]);
976
+ if (error)
977
+ throwSupabaseError('getWebhooksByEvent', error);
978
+ return (data ?? []).map((row) => toCamelCase(row));
979
+ }
980
+ async deleteWebhook(tenantId, id) {
981
+ const { error } = await this.client
982
+ .from('webhooks')
983
+ .delete()
984
+ .eq('tenant_id', tenantId)
985
+ .eq('id', id);
986
+ if (error)
987
+ throwSupabaseError('deleteWebhook', error);
988
+ }
989
+ }
990
+ //# sourceMappingURL=storage.js.map