@stigmer/server 3.24.0 → 3.26.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/dist/authorization/authorizer.d.ts +10 -4
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +11 -4
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/model/index.d.ts +4 -4
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +2 -0
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/oauth_app.d.ts.map +1 -1
- package/dist/authorization/model/oauth_app.js +3 -2
- package/dist/authorization/model/oauth_app.js.map +1 -1
- package/dist/authorization/model/platform_client.d.ts +2 -0
- package/dist/authorization/model/platform_client.d.ts.map +1 -0
- package/dist/authorization/model/platform_client.js +31 -0
- package/dist/authorization/model/platform_client.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +99 -13
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/list-indexes.d.ts +3 -0
- package/dist/boot/list-indexes.d.ts.map +1 -0
- package/dist/boot/list-indexes.js +24 -0
- package/dist/boot/list-indexes.js.map +1 -0
- package/dist/boot/route-shadowing.d.ts +25 -0
- package/dist/boot/route-shadowing.d.ts.map +1 -0
- package/dist/boot/route-shadowing.js +17 -0
- package/dist/boot/route-shadowing.js.map +1 -0
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +6 -8
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/list-index.d.ts +2 -0
- package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
- package/dist/domain/agentexecution/list-index.js +23 -0
- package/dist/domain/agentexecution/list-index.js.map +1 -0
- package/dist/domain/agentexecution/steps.d.ts +17 -33
- package/dist/domain/agentexecution/steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/steps.js +94 -95
- package/dist/domain/agentexecution/steps.js.map +1 -1
- package/dist/domain/agentexecution/usage.d.ts.map +1 -1
- package/dist/domain/agentexecution/usage.js +37 -14
- package/dist/domain/agentexecution/usage.js.map +1 -1
- package/dist/domain/artifact/controller.d.ts.map +1 -1
- package/dist/domain/artifact/controller.js +27 -15
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/artifact/list-index.d.ts +2 -0
- package/dist/domain/artifact/list-index.d.ts.map +1 -0
- package/dist/domain/artifact/list-index.js +21 -0
- package/dist/domain/artifact/list-index.js.map +1 -0
- package/dist/domain/iampolicy/membership.d.ts +6 -1
- package/dist/domain/iampolicy/membership.d.ts.map +1 -1
- package/dist/domain/iampolicy/membership.js +8 -2
- package/dist/domain/iampolicy/membership.js.map +1 -1
- package/dist/domain/identityaccount/constants.d.ts +13 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -1
- package/dist/domain/identityaccount/constants.js +17 -0
- package/dist/domain/identityaccount/constants.js.map +1 -1
- package/dist/domain/identityaccount/controller.d.ts +3 -1
- package/dist/domain/identityaccount/controller.d.ts.map +1 -1
- package/dist/domain/identityaccount/controller.js +15 -6
- package/dist/domain/identityaccount/controller.js.map +1 -1
- package/dist/domain/identityaccount/operator.d.ts.map +1 -1
- package/dist/domain/identityaccount/operator.js +1 -0
- package/dist/domain/identityaccount/operator.js.map +1 -1
- package/dist/domain/identityaccount/provisioning.d.ts +18 -0
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
- package/dist/domain/identityaccount/provisioning.js +1 -0
- package/dist/domain/identityaccount/provisioning.js.map +1 -1
- package/dist/domain/identityaccount/resource-store.js +9 -5
- package/dist/domain/identityaccount/resource-store.js.map +1 -1
- package/dist/domain/identityaccount/steps.d.ts +11 -2
- package/dist/domain/identityaccount/steps.d.ts.map +1 -1
- package/dist/domain/identityaccount/steps.js +42 -4
- package/dist/domain/identityaccount/steps.js.map +1 -1
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
- package/dist/domain/identityaccount/store-contract.js +33 -0
- package/dist/domain/identityaccount/store-contract.js.map +1 -1
- package/dist/domain/identityaccount/store.d.ts +7 -5
- package/dist/domain/identityaccount/store.d.ts.map +1 -1
- package/dist/domain/identityaccount/store.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +10 -0
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/platformclient/constants.d.ts +60 -0
- package/dist/domain/platformclient/constants.d.ts.map +1 -0
- package/dist/domain/platformclient/constants.js +78 -0
- package/dist/domain/platformclient/constants.js.map +1 -0
- package/dist/domain/platformclient/controller.d.ts +59 -0
- package/dist/domain/platformclient/controller.d.ts.map +1 -0
- package/dist/domain/platformclient/controller.js +191 -0
- package/dist/domain/platformclient/controller.js.map +1 -0
- package/dist/domain/platformclient/credentials.d.ts +13 -0
- package/dist/domain/platformclient/credentials.d.ts.map +1 -0
- package/dist/domain/platformclient/credentials.js +56 -0
- package/dist/domain/platformclient/credentials.js.map +1 -0
- package/dist/domain/platformclient/mint.d.ts +23 -0
- package/dist/domain/platformclient/mint.d.ts.map +1 -0
- package/dist/domain/platformclient/mint.js +254 -0
- package/dist/domain/platformclient/mint.js.map +1 -0
- package/dist/domain/platformclient/origin-guard.d.ts +9 -0
- package/dist/domain/platformclient/origin-guard.d.ts.map +1 -0
- package/dist/domain/platformclient/origin-guard.js +81 -0
- package/dist/domain/platformclient/origin-guard.js.map +1 -0
- package/dist/domain/platformclient/resource-store.d.ts +4 -0
- package/dist/domain/platformclient/resource-store.d.ts.map +1 -0
- package/dist/domain/platformclient/resource-store.js +117 -0
- package/dist/domain/platformclient/resource-store.js.map +1 -0
- package/dist/domain/platformclient/steps.d.ts +51 -0
- package/dist/domain/platformclient/steps.d.ts.map +1 -0
- package/dist/domain/platformclient/steps.js +339 -0
- package/dist/domain/platformclient/steps.js.map +1 -0
- package/dist/domain/platformclient/store-contract.d.ts +8 -0
- package/dist/domain/platformclient/store-contract.d.ts.map +1 -0
- package/dist/domain/platformclient/store-contract.js +208 -0
- package/dist/domain/platformclient/store-contract.js.map +1 -0
- package/dist/domain/platformclient/store.d.ts +58 -0
- package/dist/domain/platformclient/store.d.ts.map +1 -0
- package/dist/domain/platformclient/store.js +13 -0
- package/dist/domain/platformclient/store.js.map +1 -0
- package/dist/domain/platformclient/system-managed.d.ts +18 -0
- package/dist/domain/platformclient/system-managed.d.ts.map +1 -0
- package/dist/domain/platformclient/system-managed.js +80 -0
- package/dist/domain/platformclient/system-managed.js.map +1 -0
- package/dist/domain/platformclient/token-controller.d.ts +11 -0
- package/dist/domain/platformclient/token-controller.d.ts.map +1 -0
- package/dist/domain/platformclient/token-controller.js +32 -0
- package/dist/domain/platformclient/token-controller.js.map +1 -0
- package/dist/domain/platformclient/verifier.d.ts +10 -0
- package/dist/domain/platformclient/verifier.d.ts.map +1 -0
- package/dist/domain/platformclient/verifier.js +76 -0
- package/dist/domain/platformclient/verifier.js.map +1 -0
- package/dist/domain/session/list-index.d.ts +4 -0
- package/dist/domain/session/list-index.d.ts.map +1 -0
- package/dist/domain/session/list-index.js +26 -0
- package/dist/domain/session/list-index.js.map +1 -0
- package/dist/domain/session/steps.d.ts +7 -8
- package/dist/domain/session/steps.d.ts.map +1 -1
- package/dist/domain/session/steps.js +76 -78
- package/dist/domain/session/steps.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js +53 -55
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
- package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
- package/dist/domain/workflowexecution/execution-filter.js +11 -5
- package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
- package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
- package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
- package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
- package/dist/domain/workflowexecution/list-index.d.ts +2 -0
- package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
- package/dist/domain/workflowexecution/list-index.js +21 -0
- package/dist/domain/workflowexecution/list-index.js.map +1 -0
- package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
- package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
- package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
- package/dist/domain/workflowexecution/queries.d.ts +30 -4
- package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
- package/dist/domain/workflowexecution/queries.js +76 -22
- package/dist/domain/workflowexecution/queries.js.map +1 -1
- package/dist/encryption/key-manager.d.ts +20 -3
- package/dist/encryption/key-manager.d.ts.map +1 -1
- package/dist/encryption/key-manager.js +64 -42
- package/dist/encryption/key-manager.js.map +1 -1
- package/dist/extensions/caller-guards.d.ts +8 -3
- package/dist/extensions/caller-guards.d.ts.map +1 -1
- package/dist/extensions/drivers.d.ts +37 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/guest-token-minting.d.ts +26 -0
- package/dist/extensions/guest-token-minting.d.ts.map +1 -0
- package/dist/extensions/guest-token-minting.js +2 -0
- package/dist/extensions/guest-token-minting.js.map +1 -0
- package/dist/extensions/identity.d.ts +6 -4
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +9 -12
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/extensions/registry.d.ts +30 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +34 -1
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +14 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +24 -0
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/pipeline/steps/build-update-state.d.ts +11 -1
- package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
- package/dist/pipeline/steps/build-update-state.js +5 -2
- package/dist/pipeline/steps/build-update-state.js.map +1 -1
- package/dist/pipeline/steps/list-page.d.ts +63 -0
- package/dist/pipeline/steps/list-page.d.ts.map +1 -0
- package/dist/pipeline/steps/list-page.js +187 -0
- package/dist/pipeline/steps/list-page.js.map +1 -0
- package/dist/platformtoken/envelope.d.ts +100 -0
- package/dist/platformtoken/envelope.d.ts.map +1 -0
- package/dist/platformtoken/envelope.js +184 -0
- package/dist/platformtoken/envelope.js.map +1 -0
- package/dist/platformtoken/key-ring.d.ts +85 -0
- package/dist/platformtoken/key-ring.d.ts.map +1 -0
- package/dist/platformtoken/key-ring.js +164 -0
- package/dist/platformtoken/key-ring.js.map +1 -0
- package/dist/query/activity/handler.d.ts +6 -7
- package/dist/query/activity/handler.d.ts.map +1 -1
- package/dist/query/activity/handler.js +42 -59
- package/dist/query/activity/handler.js.map +1 -1
- package/dist/store/interface.d.ts +29 -0
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/list-index.d.ts +123 -0
- package/dist/store/list-index.d.ts.map +1 -0
- package/dist/store/list-index.js +277 -0
- package/dist/store/list-index.js.map +1 -0
- package/dist/store/logger.d.ts +3 -0
- package/dist/store/logger.d.ts.map +1 -1
- package/dist/store/logger.js +5 -1
- package/dist/store/logger.js.map +1 -1
- package/dist/store/postgres/migrations.d.ts +10 -6
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +64 -6
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts +26 -5
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +377 -22
- package/dist/store/postgres/store.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +11 -5
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +50 -4
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts +18 -3
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +321 -15
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/README.md +1 -1
- package/src/authorization/__tests__/authorizer.test.ts +6 -2
- package/src/authorization/__tests__/drivers.ts +6 -1
- package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
- package/src/authorization/authorizer.ts +11 -4
- package/src/authorization/model/__tests__/registry.test.ts +8 -1
- package/src/authorization/model/index.ts +6 -4
- package/src/authorization/model/oauth_app.ts +3 -2
- package/src/authorization/model/platform_client.ts +43 -0
- package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
- package/src/boot/__tests__/list-indexes.test.ts +65 -0
- package/src/boot/__tests__/route-shadowing.test.ts +67 -0
- package/src/boot/compose.ts +113 -18
- package/src/boot/list-indexes.ts +25 -0
- package/src/boot/route-shadowing.ts +47 -0
- package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
- package/src/domain/agentexecution/controller.ts +8 -10
- package/src/domain/agentexecution/list-index.ts +24 -0
- package/src/domain/agentexecution/steps.ts +118 -115
- package/src/domain/agentexecution/usage.ts +38 -17
- package/src/domain/artifact/controller.ts +28 -18
- package/src/domain/artifact/list-index.ts +22 -0
- package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
- package/src/domain/iampolicy/membership.ts +8 -2
- package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
- package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
- package/src/domain/identityaccount/constants.ts +20 -0
- package/src/domain/identityaccount/controller.ts +16 -4
- package/src/domain/identityaccount/operator.ts +1 -0
- package/src/domain/identityaccount/provisioning.ts +17 -0
- package/src/domain/identityaccount/resource-store.ts +9 -5
- package/src/domain/identityaccount/steps.ts +58 -3
- package/src/domain/identityaccount/store-contract.ts +51 -0
- package/src/domain/identityaccount/store.ts +7 -5
- package/src/domain/platform/__tests__/platform.test.ts +7 -1
- package/src/domain/platform/controller.ts +12 -1
- package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
- package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
- package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
- package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
- package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
- package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
- package/src/domain/platformclient/constants.ts +120 -0
- package/src/domain/platformclient/controller.ts +449 -0
- package/src/domain/platformclient/credentials.ts +68 -0
- package/src/domain/platformclient/mint.ts +412 -0
- package/src/domain/platformclient/origin-guard.ts +117 -0
- package/src/domain/platformclient/resource-store.ts +150 -0
- package/src/domain/platformclient/steps.ts +472 -0
- package/src/domain/platformclient/store-contract.ts +318 -0
- package/src/domain/platformclient/store.ts +63 -0
- package/src/domain/platformclient/system-managed.ts +115 -0
- package/src/domain/platformclient/token-controller.ts +49 -0
- package/src/domain/platformclient/verifier.ts +106 -0
- package/src/domain/session/list-index.ts +28 -0
- package/src/domain/session/steps.ts +108 -123
- package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
- package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
- package/src/domain/workflowexecution/controller.ts +54 -82
- package/src/domain/workflowexecution/execution-filter.ts +20 -7
- package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
- package/src/domain/workflowexecution/list-index.ts +22 -0
- package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
- package/src/domain/workflowexecution/queries.ts +134 -23
- package/src/encryption/key-manager.ts +87 -44
- package/src/extensions/__tests__/extension-composition.test.ts +3 -2
- package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
- package/src/extensions/__tests__/registry.test.ts +4 -1
- package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
- package/src/extensions/caller-guards.ts +8 -3
- package/src/extensions/drivers.ts +37 -0
- package/src/extensions/guest-token-minting.ts +32 -0
- package/src/extensions/identity.ts +6 -4
- package/src/extensions/list-read-scope.ts +9 -12
- package/src/extensions/registry.ts +77 -3
- package/src/index.ts +65 -1
- package/src/pipeline/interceptors/auth.ts +27 -0
- package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
- package/src/pipeline/steps/__tests__/references.test.ts +2 -0
- package/src/pipeline/steps/build-update-state.ts +5 -2
- package/src/pipeline/steps/list-page.ts +284 -0
- package/src/platformtoken/__tests__/envelope.test.ts +277 -0
- package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
- package/src/platformtoken/envelope.ts +276 -0
- package/src/platformtoken/key-ring.ts +266 -0
- package/src/query/activity/handler.ts +57 -78
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
- package/src/store/README.md +51 -32
- package/src/store/__tests__/list-index.measure.test.ts +293 -0
- package/src/store/__tests__/list-index.test.ts +328 -0
- package/src/store/__tests__/store-contract.ts +346 -3
- package/src/store/interface.ts +39 -0
- package/src/store/list-index.ts +465 -0
- package/src/store/logger.ts +8 -1
- package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
- package/src/store/postgres/__tests__/migrations.test.ts +27 -19
- package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
- package/src/store/postgres/migrations.ts +69 -7
- package/src/store/postgres/store.ts +485 -29
- package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
- package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
- package/src/store/sqlite/__tests__/support.ts +12 -3
- package/src/store/sqlite/migrations.ts +57 -6
- package/src/store/sqlite/store.ts +462 -21
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
|
|
2
2
|
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
3
|
-
import type { AuditRecord, BootstrapStateStore, OAuthGrantStore, PendingOAuthStateStore, RawResourceDocument, ScheduleRunRecord, SearchIndexEntry, SearchIndexQuery, SearchIndexQueryResult, SignalDedupeStore, Store, WorkflowExecutionEventRecord } from "../interface.js";
|
|
3
|
+
import type { AuditRecord, BootstrapStateStore, OAuthGrantStore, PendingOAuthStateStore, RawResourceDocument, ScheduleRunRecord, SearchIndexEntry, SearchIndexQuery, SearchIndexQueryResult, SignalDedupeStore, Store, StoreOpenOptions, WorkflowExecutionEventRecord } from "../interface.js";
|
|
4
|
+
import type { ListIndexDeclaration, ListIndexQuery, ListIndexRow } from "../list-index.js";
|
|
4
5
|
import type { StoreLogger } from "../logger.js";
|
|
5
6
|
export declare class PostgresStore implements Store {
|
|
6
7
|
readonly bootstrapState: BootstrapStateStore;
|
|
@@ -9,18 +10,23 @@ export declare class PostgresStore implements Store {
|
|
|
9
10
|
readonly pendingOAuthStates: PendingOAuthStateStore;
|
|
10
11
|
private pool;
|
|
11
12
|
private readonly logger;
|
|
13
|
+
private readonly listIndexes;
|
|
12
14
|
private constructor();
|
|
13
15
|
/**
|
|
14
16
|
* Connects to databaseUrl, runs migrations (advisory-locked, so
|
|
15
|
-
* concurrent boots serialize),
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* concurrent boots serialize), reconciles the list index (derives the
|
|
18
|
+
* facts of every unproven row of a declared kind; compare-and-set, so
|
|
19
|
+
* replicas opening together cannot conflict), and returns the ready
|
|
20
|
+
* store. Any failure here is a loud boot throw — never a degraded server
|
|
21
|
+
* (the same posture as SqliteStore.open; the composition root does not
|
|
22
|
+
* catch it).
|
|
18
23
|
*/
|
|
19
|
-
static open(databaseUrl: string, logger?: StoreLogger): Promise<PostgresStore>;
|
|
24
|
+
static open(databaseUrl: string, logger?: StoreLogger, options?: StoreOpenOptions): Promise<PostgresStore>;
|
|
20
25
|
saveResource<Desc extends DescMessage>(kind: ApiResourceKind, id: string, schema: Desc, msg: MessageShape<Desc>): Promise<void>;
|
|
21
26
|
getResource<Desc extends DescMessage>(kind: ApiResourceKind, id: string, schema: Desc): Promise<MessageShape<Desc>>;
|
|
22
27
|
updateResource<Desc extends DescMessage>(kind: ApiResourceKind, id: string, schema: Desc, modify: (msg: MessageShape<Desc>) => void): Promise<MessageShape<Desc>>;
|
|
23
28
|
listResources(kind: ApiResourceKind): Promise<Uint8Array[]>;
|
|
29
|
+
queryResources<K extends string>(declaration: ListIndexDeclaration<K>, query: ListIndexQuery<K>): Promise<ListIndexRow[]>;
|
|
24
30
|
deleteResource(kind: ApiResourceKind, id: string): Promise<void>;
|
|
25
31
|
findByField<Desc extends DescMessage>(kind: ApiResourceKind, fieldPath: string, value: string, schema: Desc): Promise<MessageShape<Desc>>;
|
|
26
32
|
findAllByField(kind: ApiResourceKind, fieldPath: string, value: string): Promise<Uint8Array[]>;
|
|
@@ -29,6 +35,21 @@ export declare class PostgresStore implements Store {
|
|
|
29
35
|
replaceResourceDataIfUnchanged(kind: ApiResourceKind, id: string, expectedData: Uint8Array, newData: Uint8Array): Promise<boolean>;
|
|
30
36
|
deleteResourcesByKind(kind: ApiResourceKind): Promise<number>;
|
|
31
37
|
deleteResourcesByIdPrefix(kind: ApiResourceKind, idPrefix: string): Promise<number>;
|
|
38
|
+
/**
|
|
39
|
+
* An unproven row's facts, derived from its bytes, and the row repaired
|
|
40
|
+
* when those bytes are still the stored ones — the compare-and-set on
|
|
41
|
+
* `data` means a newer write is never overwritten, and two replicas
|
|
42
|
+
* repairing one row write the same facts. Undefined when the bytes do
|
|
43
|
+
* not decode; the row stays unproven and is skipped, as a lane skips it.
|
|
44
|
+
*/
|
|
45
|
+
private deriveAndRepair;
|
|
46
|
+
/**
|
|
47
|
+
* Derives every unproven row of each declared kind, in id-keyed batches,
|
|
48
|
+
* and stamps every other kind's unstamped rows (they carry no facts), so
|
|
49
|
+
* the unproven set is empty when the server starts serving. Reads stay
|
|
50
|
+
* exact without it; it keeps them cheap.
|
|
51
|
+
*/
|
|
52
|
+
private reconcileListIndexes;
|
|
32
53
|
saveAudit<Desc extends DescMessage>(kind: ApiResourceKind, resourceId: string, schema: Desc, msg: MessageShape<Desc>, versionHash: string, tag: string): Promise<void>;
|
|
33
54
|
getAuditByHash<Desc extends DescMessage>(kind: ApiResourceKind, resourceId: string, versionHash: string, schema: Desc): Promise<MessageShape<Desc>>;
|
|
34
55
|
getAuditByTag<Desc extends DescMessage>(kind: ApiResourceKind, resourceId: string, tag: string, schema: Desc): Promise<MessageShape<Desc>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/store/postgres/store.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AAQ3H,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EAGnB,eAAe,EAEf,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAEhB,gBAAgB,EAChB,sBAAsB,EAGtB,iBAAiB,EACjB,KAAK,EACL,4BAA4B,EAC7B,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/store/postgres/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
37
37
|
import pg from "pg";
|
|
38
38
|
import { AuditNotFoundError, DELIVERED_SIGNAL_DEDUPE_TTL_MS, PENDING_OAUTH_STATE_TTL_MS, ResourceNotFoundError, } from "../interface.js";
|
|
39
|
+
import { ListIndexRegistry, assertListIndexLimit, listIndexFactsOf, matchesListIndexQuery, mergeListIndexRows, sameListKeyRows, } from "../list-index.js";
|
|
39
40
|
import { NOOP_STORE_LOGGER } from "../logger.js";
|
|
40
41
|
import { apiResourceKindName, filterRowsByLabel, scanForFieldMatch, } from "../proto-fields.js";
|
|
41
42
|
import { rfc3339Seconds } from "../rfc3339.js";
|
|
@@ -48,6 +49,46 @@ import { normalizeTsRankScore, renderTsQueryExpression } from "./tsquery.js";
|
|
|
48
49
|
* silently returns wrong membership).
|
|
49
50
|
*/
|
|
50
51
|
const TEXT_SEARCH_CONFIG = "english";
|
|
52
|
+
/**
|
|
53
|
+
* The one upsert every resource write goes through: the row, its
|
|
54
|
+
* `updated_at`, and the list index's facts stamped with the SAME `now()`
|
|
55
|
+
* (constant within a transaction), so a later writer that bumps
|
|
56
|
+
* `updated_at` without knowing the index leaves the row unproven. An
|
|
57
|
+
* undeclared kind writes NULL facts and is stamped all the same, which
|
|
58
|
+
* keeps it out of the unproven partial index.
|
|
59
|
+
*/
|
|
60
|
+
const UPSERT_RESOURCE_SQL = `
|
|
61
|
+
INSERT INTO resources
|
|
62
|
+
(kind, id, data, updated_at, list_org, list_created_at, list_index_revision, list_indexed_at)
|
|
63
|
+
VALUES ($1, $2, $3, now(), $4, $5, $6, now())
|
|
64
|
+
ON CONFLICT (kind, id) DO UPDATE SET
|
|
65
|
+
data = excluded.data,
|
|
66
|
+
updated_at = excluded.updated_at,
|
|
67
|
+
list_org = excluded.list_org,
|
|
68
|
+
list_created_at = excluded.list_created_at,
|
|
69
|
+
list_index_revision = excluded.list_index_revision,
|
|
70
|
+
list_indexed_at = excluded.list_indexed_at`;
|
|
71
|
+
/**
|
|
72
|
+
* The rows of kind `$1` whose facts are not proven current under revision
|
|
73
|
+
* `$2` (list-index.ts), each arm one index range that fetches a row only
|
|
74
|
+
* when it matches: the stamp arm repeats the partial index's predicate
|
|
75
|
+
* verbatim (the only form the planner matches it in), the revision arms
|
|
76
|
+
* are three ranges of the revision index. A single OR over the revision
|
|
77
|
+
* is answered, without statistics, by visiting every heap row of the kind
|
|
78
|
+
* (measured), which grows with the kind; these arms are empty in steady
|
|
79
|
+
* state. They overlap (an unstamped row has no revision either), so the
|
|
80
|
+
* caller keeps one row per id.
|
|
81
|
+
*/
|
|
82
|
+
const UNPROVEN_ROWS_SQL = `
|
|
83
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_indexed_at IS DISTINCT FROM updated_at
|
|
84
|
+
UNION ALL
|
|
85
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_index_revision IS NULL
|
|
86
|
+
UNION ALL
|
|
87
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_index_revision < $2
|
|
88
|
+
UNION ALL
|
|
89
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_index_revision > $2`;
|
|
90
|
+
/** How many unproven rows the reconciliation at open decodes per round trip. */
|
|
91
|
+
const RECONCILE_BATCH = 500;
|
|
51
92
|
export class PostgresStore {
|
|
52
93
|
bootstrapState;
|
|
53
94
|
signalDedupe;
|
|
@@ -55,9 +96,11 @@ export class PostgresStore {
|
|
|
55
96
|
pendingOAuthStates;
|
|
56
97
|
pool;
|
|
57
98
|
logger;
|
|
58
|
-
|
|
99
|
+
listIndexes;
|
|
100
|
+
constructor(pool, logger, listIndexes) {
|
|
59
101
|
this.pool = pool;
|
|
60
102
|
this.logger = logger;
|
|
103
|
+
this.listIndexes = listIndexes;
|
|
61
104
|
this.bootstrapState = new PostgresBootstrapStateStore(() => this.open());
|
|
62
105
|
this.signalDedupe = new PostgresSignalDedupeStore(() => this.open(), logger);
|
|
63
106
|
this.oauthGrants = new PostgresOAuthGrantStore(() => this.open());
|
|
@@ -65,11 +108,15 @@ export class PostgresStore {
|
|
|
65
108
|
}
|
|
66
109
|
/**
|
|
67
110
|
* Connects to databaseUrl, runs migrations (advisory-locked, so
|
|
68
|
-
* concurrent boots serialize),
|
|
69
|
-
*
|
|
70
|
-
*
|
|
111
|
+
* concurrent boots serialize), reconciles the list index (derives the
|
|
112
|
+
* facts of every unproven row of a declared kind; compare-and-set, so
|
|
113
|
+
* replicas opening together cannot conflict), and returns the ready
|
|
114
|
+
* store. Any failure here is a loud boot throw — never a degraded server
|
|
115
|
+
* (the same posture as SqliteStore.open; the composition root does not
|
|
116
|
+
* catch it).
|
|
71
117
|
*/
|
|
72
|
-
static async open(databaseUrl, logger = NOOP_STORE_LOGGER) {
|
|
118
|
+
static async open(databaseUrl, logger = NOOP_STORE_LOGGER, options = {}) {
|
|
119
|
+
const listIndexes = new ListIndexRegistry(options.listIndexes ?? []);
|
|
73
120
|
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
74
121
|
// An idle pooled connection dropping (server restart, network blip)
|
|
75
122
|
// emits 'error' on the pool; without a listener that is a process
|
|
@@ -91,15 +138,45 @@ export class PostgresStore {
|
|
|
91
138
|
throw error;
|
|
92
139
|
}
|
|
93
140
|
client.release();
|
|
94
|
-
|
|
141
|
+
const store = new PostgresStore(pool, logger, listIndexes);
|
|
142
|
+
try {
|
|
143
|
+
await store.reconcileListIndexes();
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
await store.close();
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
return store;
|
|
95
150
|
}
|
|
96
151
|
// ---------------------------------------------------------------------------
|
|
97
152
|
// Resource operations
|
|
98
153
|
// ---------------------------------------------------------------------------
|
|
99
154
|
async saveResource(kind, id, schema, msg) {
|
|
100
|
-
const data = toBinary(schema, msg);
|
|
101
|
-
|
|
102
|
-
|
|
155
|
+
const data = Buffer.from(toBinary(schema, msg));
|
|
156
|
+
const kindName = apiResourceKindName(kind);
|
|
157
|
+
const facts = this.listIndexes.factsOf(kind, msg);
|
|
158
|
+
if (facts === undefined) {
|
|
159
|
+
await this.open().query(UPSERT_RESOURCE_SQL, [
|
|
160
|
+
kindName,
|
|
161
|
+
id,
|
|
162
|
+
data,
|
|
163
|
+
null,
|
|
164
|
+
null,
|
|
165
|
+
null,
|
|
166
|
+
]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await this.withTransaction(async (client) => {
|
|
170
|
+
await client.query(UPSERT_RESOURCE_SQL, [
|
|
171
|
+
kindName,
|
|
172
|
+
id,
|
|
173
|
+
data,
|
|
174
|
+
facts.org,
|
|
175
|
+
facts.createdAt,
|
|
176
|
+
facts.revision,
|
|
177
|
+
]);
|
|
178
|
+
await replaceListKeys(client, kindName, id, facts);
|
|
179
|
+
});
|
|
103
180
|
}
|
|
104
181
|
async getResource(kind, id, schema) {
|
|
105
182
|
const kindName = apiResourceKindName(kind);
|
|
@@ -116,15 +193,34 @@ export class PostgresStore {
|
|
|
116
193
|
// per-resource atomicity contract. `modify` is synchronous by contract
|
|
117
194
|
// (interface.ts), so no caller I/O can extend the lock's hold time.
|
|
118
195
|
return this.withTransaction(async (client) => {
|
|
119
|
-
const result = await client.query(`SELECT data
|
|
196
|
+
const result = await client.query(`SELECT data, list_indexed_at IS NOT DISTINCT FROM updated_at AS stamped, list_index_revision
|
|
197
|
+
FROM resources WHERE kind = $1 AND id = $2 FOR UPDATE`, [kindName, id]);
|
|
120
198
|
const row = result.rows[0];
|
|
121
199
|
if (row === undefined) {
|
|
122
200
|
throw new ResourceNotFoundError(`${kindName}/${id}`);
|
|
123
201
|
}
|
|
124
202
|
const msg = fromBinary(schema, row.data);
|
|
203
|
+
const before = this.listIndexes.factsOf(kind, msg);
|
|
125
204
|
modify(msg);
|
|
205
|
+
const after = this.listIndexes.factsOf(kind, msg);
|
|
126
206
|
const data = toBinary(schema, msg);
|
|
127
|
-
await client.query(`UPDATE resources SET data = $3, updated_at = now()
|
|
207
|
+
await client.query(`UPDATE resources SET data = $3, updated_at = now(),
|
|
208
|
+
list_org = $4, list_created_at = $5, list_index_revision = $6, list_indexed_at = now()
|
|
209
|
+
WHERE kind = $1 AND id = $2`, [
|
|
210
|
+
kindName,
|
|
211
|
+
id,
|
|
212
|
+
Buffer.from(data),
|
|
213
|
+
after?.org ?? null,
|
|
214
|
+
after?.createdAt ?? null,
|
|
215
|
+
after?.revision ?? null,
|
|
216
|
+
]);
|
|
217
|
+
if (after !== undefined &&
|
|
218
|
+
(!row.stamped ||
|
|
219
|
+
row.list_index_revision !== after.revision ||
|
|
220
|
+
before === undefined ||
|
|
221
|
+
!sameListKeyRows(before, after))) {
|
|
222
|
+
await replaceListKeys(client, kindName, id, after);
|
|
223
|
+
}
|
|
128
224
|
return msg;
|
|
129
225
|
});
|
|
130
226
|
}
|
|
@@ -132,8 +228,53 @@ export class PostgresStore {
|
|
|
132
228
|
const result = await this.open().query(`SELECT data FROM resources WHERE kind = $1`, [apiResourceKindName(kind)]);
|
|
133
229
|
return result.rows.map((row) => row.data);
|
|
134
230
|
}
|
|
231
|
+
async queryResources(declaration, query) {
|
|
232
|
+
this.listIndexes.require(declaration);
|
|
233
|
+
assertListIndexLimit(query.limit);
|
|
234
|
+
if (query.anyKey !== undefined && query.anyKey.length === 0) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const kindName = apiResourceKindName(declaration.kind);
|
|
238
|
+
// A key read is driven from the key table, ordered by that index's own
|
|
239
|
+
// columns, so the plan is one index range whatever the planner's
|
|
240
|
+
// statistics say (a freshly written table has none); several keys are
|
|
241
|
+
// one such read each, merged below. Without a key the organization
|
|
242
|
+
// index is the range.
|
|
243
|
+
const provenRows = [];
|
|
244
|
+
const reads = query.anyKey === undefined
|
|
245
|
+
? [provenReadSql(kindName, declaration.revision, query, undefined)]
|
|
246
|
+
: query.anyKey.map((key) => provenReadSql(kindName, declaration.revision, query, key));
|
|
247
|
+
for (const read of reads) {
|
|
248
|
+
const result = await this.open().query(read.sql, read.params);
|
|
249
|
+
for (const row of result.rows) {
|
|
250
|
+
provenRows.push({
|
|
251
|
+
id: row.id,
|
|
252
|
+
data: row.data,
|
|
253
|
+
cursor: { createdAt: row.created_at, id: row.id },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const unproven = await this.open().query(UNPROVEN_ROWS_SQL, [
|
|
258
|
+
kindName,
|
|
259
|
+
declaration.revision,
|
|
260
|
+
]);
|
|
261
|
+
const unprovenRows = [];
|
|
262
|
+
for (const row of oneRowPerId(unproven.rows)) {
|
|
263
|
+
const facts = await this.deriveAndRepair(declaration, row.id, row.data);
|
|
264
|
+
if (facts !== undefined && matchesListIndexQuery(row.id, facts, query)) {
|
|
265
|
+
unprovenRows.push({
|
|
266
|
+
id: row.id,
|
|
267
|
+
data: row.data,
|
|
268
|
+
cursor: { createdAt: facts.createdAt, id: row.id },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return mergeListIndexRows(provenRows, unprovenRows, query.limit);
|
|
273
|
+
}
|
|
135
274
|
async deleteResource(kind, id) {
|
|
136
|
-
|
|
275
|
+
const kindName = apiResourceKindName(kind);
|
|
276
|
+
await this.open().query(`DELETE FROM resources WHERE kind = $1 AND id = $2`, [kindName, id]);
|
|
277
|
+
await this.open().query(`DELETE FROM resource_list_keys WHERE kind = $1 AND id = $2`, [kindName, id]);
|
|
137
278
|
}
|
|
138
279
|
async findByField(kind, fieldPath, value, schema) {
|
|
139
280
|
const kindName = apiResourceKindName(kind);
|
|
@@ -168,26 +309,156 @@ export class PostgresStore {
|
|
|
168
309
|
async replaceResourceDataIfUnchanged(kind, id, expectedData, newData) {
|
|
169
310
|
// BYTEA equality in the WHERE clause makes the compare-and-swap one
|
|
170
311
|
// atomic statement: zero rows changed means the row moved on (or was
|
|
171
|
-
// deleted) since the read — a lost swap, never an upsert.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
312
|
+
// deleted) since the read — a lost swap, never an upsert. The new bytes'
|
|
313
|
+
// list facts ride the same statement; bytes a declared kind's schema
|
|
314
|
+
// cannot decode leave the row unproven (stamp NULL), for a read to skip.
|
|
315
|
+
const kindName = apiResourceKindName(kind);
|
|
316
|
+
const declaration = this.listIndexes.declarationOf(kind);
|
|
317
|
+
const facts = declaration === undefined
|
|
318
|
+
? undefined
|
|
319
|
+
: factsOfBytes(declaration, newData);
|
|
320
|
+
const swap = async (client) => {
|
|
321
|
+
const result = await client.query(`UPDATE resources SET data = $1, updated_at = now(),
|
|
322
|
+
list_org = $5, list_created_at = $6, list_index_revision = $7,
|
|
323
|
+
list_indexed_at = CASE WHEN $8::boolean THEN now() ELSE NULL END
|
|
324
|
+
WHERE kind = $2 AND id = $3 AND data = $4`, [
|
|
325
|
+
Buffer.from(newData),
|
|
326
|
+
kindName,
|
|
327
|
+
id,
|
|
328
|
+
Buffer.from(expectedData),
|
|
329
|
+
facts?.org ?? null,
|
|
330
|
+
facts?.createdAt ?? null,
|
|
331
|
+
facts?.revision ?? null,
|
|
332
|
+
declaration === undefined || facts !== undefined,
|
|
333
|
+
]);
|
|
334
|
+
return result.rowCount === 1;
|
|
335
|
+
};
|
|
336
|
+
if (facts === undefined) {
|
|
337
|
+
return swap(this.open());
|
|
338
|
+
}
|
|
339
|
+
return this.withTransaction(async (client) => {
|
|
340
|
+
const swapped = await swap(client);
|
|
341
|
+
if (swapped) {
|
|
342
|
+
await replaceListKeys(client, kindName, id, facts);
|
|
343
|
+
}
|
|
344
|
+
return swapped;
|
|
345
|
+
});
|
|
179
346
|
}
|
|
180
347
|
async deleteResourcesByKind(kind) {
|
|
181
|
-
const
|
|
348
|
+
const kindName = apiResourceKindName(kind);
|
|
349
|
+
const result = await this.open().query(`DELETE FROM resources WHERE kind = $1`, [kindName]);
|
|
350
|
+
await this.open().query(`DELETE FROM resource_list_keys WHERE kind = $1`, [
|
|
351
|
+
kindName,
|
|
352
|
+
]);
|
|
182
353
|
return result.rowCount ?? 0;
|
|
183
354
|
}
|
|
184
355
|
async deleteResourcesByIdPrefix(kind, idPrefix) {
|
|
185
356
|
// LIKE with the wildcard characters of the PREFIX escaped — sqlite's
|
|
186
357
|
// GLOB has no LIKE-metacharacter hazard, this port must not either.
|
|
187
|
-
const
|
|
358
|
+
const params = [
|
|
359
|
+
apiResourceKindName(kind),
|
|
360
|
+
`${escapeLikePattern(idPrefix)}%`,
|
|
361
|
+
];
|
|
362
|
+
const result = await this.open().query(`DELETE FROM resources WHERE kind = $1 AND id LIKE $2 ESCAPE '\\'`, params);
|
|
363
|
+
await this.open().query(`DELETE FROM resource_list_keys WHERE kind = $1 AND id LIKE $2 ESCAPE '\\'`, params);
|
|
188
364
|
return result.rowCount ?? 0;
|
|
189
365
|
}
|
|
190
366
|
// ---------------------------------------------------------------------------
|
|
367
|
+
// The list index's repair (list-index.ts)
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
/**
|
|
370
|
+
* An unproven row's facts, derived from its bytes, and the row repaired
|
|
371
|
+
* when those bytes are still the stored ones — the compare-and-set on
|
|
372
|
+
* `data` means a newer write is never overwritten, and two replicas
|
|
373
|
+
* repairing one row write the same facts. Undefined when the bytes do
|
|
374
|
+
* not decode; the row stays unproven and is skipped, as a lane skips it.
|
|
375
|
+
*/
|
|
376
|
+
async deriveAndRepair(declaration, id, data) {
|
|
377
|
+
const kindName = apiResourceKindName(declaration.kind);
|
|
378
|
+
const facts = factsOfBytes(declaration, data);
|
|
379
|
+
if (facts === undefined) {
|
|
380
|
+
this.logger.warn("list index: an unproven row does not decode, skipping", {
|
|
381
|
+
kind: kindName,
|
|
382
|
+
id,
|
|
383
|
+
});
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
await this.withTransaction(async (client) => {
|
|
388
|
+
const result = await client.query(`UPDATE resources SET list_org = $4, list_created_at = $5,
|
|
389
|
+
list_index_revision = $6, list_indexed_at = updated_at
|
|
390
|
+
WHERE kind = $1 AND id = $2 AND data = $3`, [
|
|
391
|
+
kindName,
|
|
392
|
+
id,
|
|
393
|
+
Buffer.from(data),
|
|
394
|
+
facts.org,
|
|
395
|
+
facts.createdAt,
|
|
396
|
+
facts.revision,
|
|
397
|
+
]);
|
|
398
|
+
if (result.rowCount === 1) {
|
|
399
|
+
await replaceListKeys(client, kindName, id, facts);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
// The facts are right whether or not they were written back; a
|
|
405
|
+
// failed repair only leaves the row for the next read to repair.
|
|
406
|
+
this.logger.warn("list index: repairing a row failed", {
|
|
407
|
+
kind: kindName,
|
|
408
|
+
id,
|
|
409
|
+
error: error instanceof Error ? error.message : String(error),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return facts;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Derives every unproven row of each declared kind, in id-keyed batches,
|
|
416
|
+
* and stamps every other kind's unstamped rows (they carry no facts), so
|
|
417
|
+
* the unproven set is empty when the server starts serving. Reads stay
|
|
418
|
+
* exact without it; it keeps them cheap.
|
|
419
|
+
*/
|
|
420
|
+
async reconcileListIndexes() {
|
|
421
|
+
const declarations = this.listIndexes.declarations();
|
|
422
|
+
let reconciled = false;
|
|
423
|
+
for (const declaration of declarations) {
|
|
424
|
+
const kindName = apiResourceKindName(declaration.kind);
|
|
425
|
+
let after = "";
|
|
426
|
+
let derived = 0;
|
|
427
|
+
for (;;) {
|
|
428
|
+
const batch = await this.open().query(`SELECT id, data FROM (${UNPROVEN_ROWS_SQL}) unproven WHERE id > $3
|
|
429
|
+
ORDER BY id LIMIT $4`, [kindName, declaration.revision, after, RECONCILE_BATCH]);
|
|
430
|
+
// Duplicates from overlapping arms sit side by side in id order and
|
|
431
|
+
// the next batch starts after the last id, so none is repaired twice
|
|
432
|
+
// across batches and the count still ends the loop.
|
|
433
|
+
const rows = batch.rows;
|
|
434
|
+
for (const row of oneRowPerId(rows)) {
|
|
435
|
+
if ((await this.deriveAndRepair(declaration, row.id, row.data)) !==
|
|
436
|
+
undefined) {
|
|
437
|
+
derived += 1;
|
|
438
|
+
}
|
|
439
|
+
after = row.id;
|
|
440
|
+
}
|
|
441
|
+
if (rows.length < RECONCILE_BATCH) {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (derived > 0) {
|
|
446
|
+
this.logger.info("list index reconciled", {
|
|
447
|
+
kind: kindName,
|
|
448
|
+
rows: derived,
|
|
449
|
+
});
|
|
450
|
+
reconciled = true;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (reconciled) {
|
|
454
|
+
// Every derived row's list columns changed at once; the planner's
|
|
455
|
+
// statistics for them are refreshed before the first read plans.
|
|
456
|
+
await this.open().query(`ANALYZE resources, resource_list_keys`);
|
|
457
|
+
}
|
|
458
|
+
await this.open().query(`UPDATE resources SET list_indexed_at = updated_at
|
|
459
|
+
WHERE list_indexed_at IS DISTINCT FROM updated_at AND NOT (kind = ANY($1::text[]))`, [declarations.map((d) => apiResourceKindName(d.kind))]);
|
|
460
|
+
}
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
191
462
|
// Audit operations
|
|
192
463
|
// ---------------------------------------------------------------------------
|
|
193
464
|
async saveAudit(kind, resourceId, schema, msg, versionHash, tag) {
|
|
@@ -858,6 +1129,90 @@ class PostgresPendingOAuthStateStore {
|
|
|
858
1129
|
return result.rowCount ?? 0;
|
|
859
1130
|
}
|
|
860
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* A row's key rows made equal to its facts: one row per key the row
|
|
1134
|
+
* names, the creation instant copied in so a parent's rows are one index
|
|
1135
|
+
* range. Runs on the caller's transaction, beside the row write it
|
|
1136
|
+
* belongs to.
|
|
1137
|
+
*/
|
|
1138
|
+
async function replaceListKeys(client, kindName, id, facts) {
|
|
1139
|
+
await client.query(`DELETE FROM resource_list_keys WHERE kind = $1 AND id = $2`, [kindName, id]);
|
|
1140
|
+
if (facts.keys.length === 0) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const params = [kindName, id, facts.createdAt];
|
|
1144
|
+
const tuples = facts.keys.map((entry) => {
|
|
1145
|
+
params.push(entry.key, entry.value);
|
|
1146
|
+
return `($1, $2, $${params.length - 1}, $${params.length}, $3)`;
|
|
1147
|
+
});
|
|
1148
|
+
await client.query(`INSERT INTO resource_list_keys (kind, id, key, value, created_at) VALUES ${tuples.join(", ")}`, params);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* One proven read of `queryResources`: through one key's index range when
|
|
1152
|
+
* `key` is given (the key table drives, its copied creation instant is the
|
|
1153
|
+
* order), else through the organization index. Only rows whose facts are
|
|
1154
|
+
* proven current are returned; the unproven arm answers for the rest.
|
|
1155
|
+
*/
|
|
1156
|
+
function provenReadSql(kindName, revision, query, key) {
|
|
1157
|
+
const params = [kindName, revision];
|
|
1158
|
+
const param = (value) => {
|
|
1159
|
+
params.push(value);
|
|
1160
|
+
return `$${params.length}`;
|
|
1161
|
+
};
|
|
1162
|
+
const created = key === undefined ? "r.list_created_at" : "k.created_at";
|
|
1163
|
+
const id = key === undefined ? `r.id COLLATE "C"` : `k.id COLLATE "C"`;
|
|
1164
|
+
const where = [
|
|
1165
|
+
`r.list_indexed_at = r.updated_at`,
|
|
1166
|
+
`r.list_index_revision = $2`,
|
|
1167
|
+
];
|
|
1168
|
+
let from = `resources r`;
|
|
1169
|
+
if (key === undefined) {
|
|
1170
|
+
where.unshift(`r.kind = $1`);
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
// The row behind each key is fetched by primary key and nothing else:
|
|
1174
|
+
// `OFFSET 0` keeps the subquery from being flattened, so a planner
|
|
1175
|
+
// without statistics cannot reach it through the revision index
|
|
1176
|
+
// instead (measured: a scan of the kind per key row).
|
|
1177
|
+
from = `resource_list_keys k CROSS JOIN LATERAL (
|
|
1178
|
+
SELECT id, data, updated_at, list_indexed_at, list_index_revision, list_org
|
|
1179
|
+
FROM resources WHERE kind = k.kind AND id = k.id OFFSET 0
|
|
1180
|
+
) r`;
|
|
1181
|
+
where.unshift(`k.kind = $1`, `k.key = ${param(key.name)}`, `k.value = ${param(key.value)}`);
|
|
1182
|
+
}
|
|
1183
|
+
if (query.org !== undefined && query.org !== "") {
|
|
1184
|
+
where.push(`r.list_org = ${param(query.org)}`);
|
|
1185
|
+
}
|
|
1186
|
+
if (query.createdAtOrAfter !== undefined) {
|
|
1187
|
+
where.push(`(${created} >= ${param(query.createdAtOrAfter)} OR ${created} = '')`);
|
|
1188
|
+
}
|
|
1189
|
+
if (query.after !== undefined) {
|
|
1190
|
+
const afterCreated = param(query.after.createdAt);
|
|
1191
|
+
const afterId = param(query.after.id);
|
|
1192
|
+
where.push(`(${created} < ${afterCreated} OR (${created} = ${afterCreated} AND ${id} < ${afterId}))`);
|
|
1193
|
+
}
|
|
1194
|
+
const limit = query.limit === undefined ? "" : ` LIMIT ${param(query.limit)}`;
|
|
1195
|
+
return {
|
|
1196
|
+
sql: `SELECT r.id, r.data, ${created} AS created_at FROM ${from}
|
|
1197
|
+
WHERE ${where.join(" AND ")}
|
|
1198
|
+
ORDER BY ${created} DESC, ${id} DESC${limit}`,
|
|
1199
|
+
params,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
/** Keeps the first row per id; the unproven arms overlap. */
|
|
1203
|
+
function oneRowPerId(rows) {
|
|
1204
|
+
const seen = new Set();
|
|
1205
|
+
return rows.filter((row) => seen.has(row.id) ? false : (seen.add(row.id), true));
|
|
1206
|
+
}
|
|
1207
|
+
/** A declared kind's facts from stored bytes; undefined when they do not decode. */
|
|
1208
|
+
function factsOfBytes(declaration, data) {
|
|
1209
|
+
try {
|
|
1210
|
+
return listIndexFactsOf(declaration, fromBinary(declaration.schema, data));
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
861
1216
|
/** Escapes LIKE metacharacters so a prefix matches literally. */
|
|
862
1217
|
function escapeLikePattern(value) {
|
|
863
1218
|
return value
|