@axiom-lattice/pg-stores 3.3.5 → 3.3.6
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +153 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +160 -78
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/scripts/verify-model-provider-migrations.ts +214 -0
- package/scripts/verify-store.ts +232 -0
- package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +41 -4
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +48 -4
- package/src/__tests__/PostgreSQLConnectionStore.integration.test.ts +60 -1
- package/src/__tests__/PostgreSQLDatabaseConfigStore.test.ts +53 -0
- package/src/__tests__/PostgreSQLMcpServerConfigStore.test.ts +43 -0
- package/src/__tests__/PostgreSQLMetricsServerConfigStore.test.ts +59 -0
- package/src/__tests__/PostgreSQLModelProviderStore.test.ts +58 -0
- package/src/createPgStoreConfig.ts +6 -1
- package/src/migrations/connection_config_migrations.ts +56 -0
- package/src/migrations/model_provider_migrations.ts +74 -0
- package/src/stores/PostgreSQLA2AApiKeyStore.ts +4 -17
- package/src/stores/PostgreSQLChannelInstallationStore.ts +4 -4
- package/src/stores/PostgreSQLDatabaseConfigStore.ts +2 -7
- package/src/stores/PostgreSQLMcpServerConfigStore.ts +9 -19
- package/src/stores/PostgreSQLMetricsServerConfigStore.ts +3 -13
- package/src/stores/PostgreSQLModelProviderStore.ts +128 -25
|
@@ -20,8 +20,12 @@ import { MigrationManager } from '../migrations/migration';
|
|
|
20
20
|
import {
|
|
21
21
|
createModelProvidersTable,
|
|
22
22
|
createModelProviderModelsTable,
|
|
23
|
+
addModelProviderApiStyle,
|
|
24
|
+
addModelProviderModelStale,
|
|
25
|
+
addModelProviderModelExtra,
|
|
26
|
+
addModelProviderModelDefault,
|
|
23
27
|
} from '../migrations/model_provider_migrations';
|
|
24
|
-
import { encrypt,
|
|
28
|
+
import { encrypt, safeDecrypt } from '@axiom-lattice/core';
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* PostgreSQL ModelProviderStore options
|
|
@@ -76,9 +80,25 @@ interface ModelRow {
|
|
|
76
80
|
upstream_created: Date | null;
|
|
77
81
|
enabled: boolean;
|
|
78
82
|
stale: boolean;
|
|
83
|
+
extra: Record<string, unknown> | null;
|
|
84
|
+
is_default: boolean;
|
|
79
85
|
discovered_at: Date;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/**
|
|
89
|
+
* A model's stored config, or undefined when it carries none.
|
|
90
|
+
*
|
|
91
|
+
* The column is `NOT NULL DEFAULT '{}'`, so "no per-model config" arrives as an
|
|
92
|
+
* empty object. The protocol types `extra` as optional and the in-memory store
|
|
93
|
+
* leaves it undefined, so normalising here keeps the two stores
|
|
94
|
+
* interchangeable rather than letting callers tell which one is behind them.
|
|
95
|
+
*/
|
|
96
|
+
function extraOrUndefined(
|
|
97
|
+
value: Record<string, unknown> | null
|
|
98
|
+
): Record<string, unknown> | undefined {
|
|
99
|
+
return value && Object.keys(value).length > 0 ? value : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
/**
|
|
83
103
|
* PostgreSQL implementation of ModelProviderStore
|
|
84
104
|
*
|
|
@@ -113,8 +133,17 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
this.migrationManager = new MigrationManager(this.pool);
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
// Every migration that touches these two tables, not just the ones that
|
|
137
|
+
// create them. This store can migrate itself (standalone `autoMigrate`,
|
|
138
|
+
// without going through `createPgStoreConfig`), and a list that stops at
|
|
139
|
+
// v182 leaves such a database without `stale` or `extra` — the reads below
|
|
140
|
+
// then fail on a missing column rather than returning an empty value.
|
|
141
|
+
this.migrationManager.register(createModelProvidersTable); // v181
|
|
142
|
+
this.migrationManager.register(createModelProviderModelsTable); // v182
|
|
143
|
+
this.migrationManager.register(addModelProviderApiStyle); // v183
|
|
144
|
+
this.migrationManager.register(addModelProviderModelStale); // v184
|
|
145
|
+
this.migrationManager.register(addModelProviderModelExtra); // v191
|
|
146
|
+
this.migrationManager.register(addModelProviderModelDefault); // v192
|
|
118
147
|
|
|
119
148
|
// Auto-migrate by default
|
|
120
149
|
if (options.autoMigrate !== false) {
|
|
@@ -440,7 +469,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
440
469
|
? await this.pool.query<ModelRow>(
|
|
441
470
|
`
|
|
442
471
|
SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
443
|
-
upstream_created, enabled, stale, discovered_at
|
|
472
|
+
upstream_created, enabled, stale, extra, is_default, discovered_at
|
|
444
473
|
FROM lattice_model_provider_models
|
|
445
474
|
WHERE tenant_id = $1 AND provider_id = $2
|
|
446
475
|
ORDER BY model_id ASC
|
|
@@ -450,7 +479,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
450
479
|
: await this.pool.query<ModelRow>(
|
|
451
480
|
`
|
|
452
481
|
SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
453
|
-
upstream_created, enabled, discovered_at
|
|
482
|
+
upstream_created, enabled, stale, extra, is_default, discovered_at
|
|
454
483
|
FROM lattice_model_provider_models
|
|
455
484
|
WHERE tenant_id = $1
|
|
456
485
|
ORDER BY model_id ASC
|
|
@@ -476,7 +505,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
476
505
|
const result = await this.pool.query<ModelRow>(
|
|
477
506
|
`
|
|
478
507
|
SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
479
|
-
upstream_created, enabled, discovered_at
|
|
508
|
+
upstream_created, enabled, stale, extra, is_default, discovered_at
|
|
480
509
|
FROM lattice_model_provider_models
|
|
481
510
|
WHERE tenant_id = $1 AND provider_id = $2 AND model_id = $3
|
|
482
511
|
`,
|
|
@@ -498,8 +527,12 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
498
527
|
* is an independent model.
|
|
499
528
|
*
|
|
500
529
|
* Models the upstream no longer offers are deleted; models that survive are
|
|
501
|
-
* upserted *without* touching `enabled`, so the tenant's
|
|
502
|
-
*
|
|
530
|
+
* upserted *without* touching `enabled` or `extra`, so the tenant's choices
|
|
531
|
+
* survive a re-discovery. New rows take the column defaults.
|
|
532
|
+
*
|
|
533
|
+
* The default model is the exception to the delete: it is kept and flagged
|
|
534
|
+
* `stale` instead, because deleting it would take the tenant's default away
|
|
535
|
+
* with no confirmation.
|
|
503
536
|
*/
|
|
504
537
|
async replaceProviderModels(
|
|
505
538
|
tenantId: string,
|
|
@@ -513,12 +546,17 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
513
546
|
try {
|
|
514
547
|
await client.query('BEGIN');
|
|
515
548
|
|
|
516
|
-
// Read the current
|
|
517
|
-
// explicitly: discovery leaves `enabled` out (the tenant's
|
|
518
|
-
// while an explicit selection supplies it.
|
|
519
|
-
const current = await client.query<{
|
|
549
|
+
// Read the current per-row state first, so the write below can state it
|
|
550
|
+
// explicitly: discovery leaves `enabled` / `extra` out (the tenant's
|
|
551
|
+
// choice is kept) while an explicit selection supplies it.
|
|
552
|
+
const current = await client.query<{
|
|
553
|
+
model_id: string;
|
|
554
|
+
enabled: boolean;
|
|
555
|
+
extra: Record<string, unknown> | null;
|
|
556
|
+
is_default: boolean;
|
|
557
|
+
}>(
|
|
520
558
|
`
|
|
521
|
-
SELECT model_id, enabled FROM lattice_model_provider_models
|
|
559
|
+
SELECT model_id, enabled, extra, is_default FROM lattice_model_provider_models
|
|
522
560
|
WHERE tenant_id = $1 AND provider_id = $2
|
|
523
561
|
`,
|
|
524
562
|
[tenantId, providerId]
|
|
@@ -526,6 +564,12 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
526
564
|
const currentEnabled = new Map(
|
|
527
565
|
current.rows.map((row) => [row.model_id, row.enabled])
|
|
528
566
|
);
|
|
567
|
+
const currentExtra = new Map(
|
|
568
|
+
current.rows.map((row) => [row.model_id, row.extra])
|
|
569
|
+
);
|
|
570
|
+
const currentIsDefault = new Map(
|
|
571
|
+
current.rows.map((row) => [row.model_id, row.is_default])
|
|
572
|
+
);
|
|
529
573
|
|
|
530
574
|
// De-duplicate by model id: a single `ON CONFLICT DO UPDATE` statement
|
|
531
575
|
// cannot touch the same target row twice, and upstreams (and callers)
|
|
@@ -540,7 +584,63 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
540
584
|
(model) => model.enabled ?? currentEnabled.get(model.id) ?? true
|
|
541
585
|
);
|
|
542
586
|
|
|
543
|
-
//
|
|
587
|
+
// Same rule as `enabled`: an omitted `extra` means "leave the stored
|
|
588
|
+
// config alone", so a sync — which carries no per-model config at all —
|
|
589
|
+
// cannot wipe what the tenant set by hand.
|
|
590
|
+
//
|
|
591
|
+
// The fallback is `{}` rather than null: the column is NOT NULL, and
|
|
592
|
+
// writing an explicit null fails the constraint for every model that has
|
|
593
|
+
// no config, which is most of them.
|
|
594
|
+
const effectiveExtra = rows.map(
|
|
595
|
+
(model) => model.extra ?? currentExtra.get(model.id) ?? {}
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Whether this write is naming a default at all.
|
|
599
|
+
//
|
|
600
|
+
// The unique index spans the *tenant*, while this method writes one
|
|
601
|
+
// provider. Two consequences:
|
|
602
|
+
//
|
|
603
|
+
// 1. A swap (A was the default, B now is) would see two true rows
|
|
604
|
+
// mid-statement — PostgreSQL checks a unique index per row, so whether
|
|
605
|
+
// it fails would depend on which row the batch happened to process
|
|
606
|
+
// first. Clearing tenant-wide *before* the upsert makes order
|
|
607
|
+
// irrelevant.
|
|
608
|
+
// 2. Once cleared, an omitted flag must resolve to `false`, not "keep":
|
|
609
|
+
// keeping it would restore the old default alongside the new one.
|
|
610
|
+
const namesDefault = rows.some((model) => model.isDefault === true);
|
|
611
|
+
if (namesDefault) {
|
|
612
|
+
await client.query(
|
|
613
|
+
`UPDATE lattice_model_provider_models SET is_default = FALSE WHERE tenant_id = $1`,
|
|
614
|
+
[tenantId]
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const effectiveIsDefault = rows.map(
|
|
619
|
+
(model) =>
|
|
620
|
+
model.isDefault ??
|
|
621
|
+
(namesDefault ? false : currentIsDefault.get(model.id)) ??
|
|
622
|
+
false
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// Drop only what the upstream no longer offers — except the default.
|
|
626
|
+
//
|
|
627
|
+
// Deleting it would take the tenant's default away with no confirmation,
|
|
628
|
+
// and nothing would put it back. That is not hypothetical: a discovery
|
|
629
|
+
// pass replaces the set wholesale, so an upstream that stops offering the
|
|
630
|
+
// default model would silently delete it. Keep it, flagged, the way the
|
|
631
|
+
// tenant-facing sync does.
|
|
632
|
+
|
|
633
|
+
await client.query(
|
|
634
|
+
`
|
|
635
|
+
UPDATE lattice_model_provider_models
|
|
636
|
+
SET stale = TRUE
|
|
637
|
+
WHERE tenant_id = $1
|
|
638
|
+
AND provider_id = $2
|
|
639
|
+
AND model_id <> ALL($3::text[])
|
|
640
|
+
AND is_default
|
|
641
|
+
`,
|
|
642
|
+
[tenantId, providerId, rows.map((model) => model.id)]
|
|
643
|
+
);
|
|
544
644
|
|
|
545
645
|
await client.query(
|
|
546
646
|
`
|
|
@@ -548,26 +648,28 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
548
648
|
WHERE tenant_id = $1
|
|
549
649
|
AND provider_id = $2
|
|
550
650
|
AND model_id <> ALL($3::text[])
|
|
651
|
+
AND NOT is_default
|
|
551
652
|
`,
|
|
552
653
|
[tenantId, providerId, rows.map((model) => model.id)]
|
|
553
654
|
);
|
|
554
655
|
|
|
555
656
|
if (models.length > 0) {
|
|
556
657
|
// One statement for the whole snapshot rather than one per model.
|
|
557
|
-
// `enabled`
|
|
558
|
-
//
|
|
658
|
+
// `enabled` and `extra` are computed above so that an omitted value
|
|
659
|
+
// resolves to the row's current one rather than being cleared.
|
|
559
660
|
await client.query(
|
|
560
661
|
`
|
|
561
662
|
INSERT INTO lattice_model_provider_models (
|
|
562
663
|
id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
563
|
-
upstream_created, raw, enabled, stale, discovered_at
|
|
664
|
+
upstream_created, raw, enabled, stale, extra, is_default, discovered_at
|
|
564
665
|
)
|
|
565
666
|
SELECT * FROM unnest(
|
|
566
667
|
$1::text[], $2::text[], $3::text[], $4::text[], $5::text[],
|
|
567
|
-
$6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[],
|
|
668
|
+
$6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[],
|
|
669
|
+
$11::jsonb[], $12::boolean[], $13::timestamp[]
|
|
568
670
|
) AS t(
|
|
569
671
|
id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
570
|
-
upstream_created, raw, enabled, stale, discovered_at
|
|
672
|
+
upstream_created, raw, enabled, stale, extra, is_default, discovered_at
|
|
571
673
|
)
|
|
572
674
|
ON CONFLICT (tenant_id, provider_id, model_id) DO UPDATE SET
|
|
573
675
|
display_name = EXCLUDED.display_name,
|
|
@@ -576,6 +678,8 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
576
678
|
raw = EXCLUDED.raw,
|
|
577
679
|
enabled = EXCLUDED.enabled,
|
|
578
680
|
stale = EXCLUDED.stale,
|
|
681
|
+
extra = EXCLUDED.extra,
|
|
682
|
+
is_default = EXCLUDED.is_default,
|
|
579
683
|
discovered_at = EXCLUDED.discovered_at
|
|
580
684
|
`,
|
|
581
685
|
[
|
|
@@ -593,6 +697,8 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
593
697
|
),
|
|
594
698
|
effectiveEnabled,
|
|
595
699
|
rows.map((model) => model.stale ?? false),
|
|
700
|
+
effectiveExtra.map((extra) => JSON.stringify(extra)),
|
|
701
|
+
effectiveIsDefault,
|
|
596
702
|
rows.map(() => new Date().toISOString()),
|
|
597
703
|
]
|
|
598
704
|
);
|
|
@@ -682,12 +788,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
682
788
|
let apiKey: string | undefined;
|
|
683
789
|
|
|
684
790
|
if (row.api_key_enc) {
|
|
685
|
-
|
|
686
|
-
apiKey = decrypt(row.api_key_enc);
|
|
687
|
-
} catch (error) {
|
|
688
|
-
console.error('Failed to decrypt model provider API key:', error);
|
|
689
|
-
throw new Error('Failed to decrypt model provider API key');
|
|
690
|
-
}
|
|
791
|
+
apiKey = safeDecrypt(row.api_key_enc);
|
|
691
792
|
}
|
|
692
793
|
|
|
693
794
|
return {
|
|
@@ -725,6 +826,8 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
|
|
|
725
826
|
upstreamCreatedAt: row.upstream_created || undefined,
|
|
726
827
|
enabled: row.enabled,
|
|
727
828
|
stale: row.stale,
|
|
829
|
+
extra: extraOrUndefined(row.extra),
|
|
830
|
+
isDefault: row.is_default,
|
|
728
831
|
discoveredAt: row.discovered_at,
|
|
729
832
|
};
|
|
730
833
|
}
|