@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
|
@@ -64,8 +64,23 @@ import type {
|
|
|
64
64
|
SignalDedupeStatus,
|
|
65
65
|
SignalDedupeStore,
|
|
66
66
|
Store,
|
|
67
|
+
StoreOpenOptions,
|
|
67
68
|
WorkflowExecutionEventRecord,
|
|
68
69
|
} from "../interface.js";
|
|
70
|
+
import {
|
|
71
|
+
ListIndexRegistry,
|
|
72
|
+
assertListIndexLimit,
|
|
73
|
+
listIndexFactsOf,
|
|
74
|
+
matchesListIndexQuery,
|
|
75
|
+
mergeListIndexRows,
|
|
76
|
+
sameListKeyRows,
|
|
77
|
+
} from "../list-index.js";
|
|
78
|
+
import type {
|
|
79
|
+
ListIndexDeclaration,
|
|
80
|
+
ListIndexFacts,
|
|
81
|
+
ListIndexQuery,
|
|
82
|
+
ListIndexRow,
|
|
83
|
+
} from "../list-index.js";
|
|
69
84
|
import { NOOP_STORE_LOGGER } from "../logger.js";
|
|
70
85
|
import type { StoreLogger } from "../logger.js";
|
|
71
86
|
import {
|
|
@@ -85,6 +100,49 @@ import { normalizeTsRankScore, renderTsQueryExpression } from "./tsquery.js";
|
|
|
85
100
|
*/
|
|
86
101
|
const TEXT_SEARCH_CONFIG = "english";
|
|
87
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The one upsert every resource write goes through: the row, its
|
|
105
|
+
* `updated_at`, and the list index's facts stamped with the SAME `now()`
|
|
106
|
+
* (constant within a transaction), so a later writer that bumps
|
|
107
|
+
* `updated_at` without knowing the index leaves the row unproven. An
|
|
108
|
+
* undeclared kind writes NULL facts and is stamped all the same, which
|
|
109
|
+
* keeps it out of the unproven partial index.
|
|
110
|
+
*/
|
|
111
|
+
const UPSERT_RESOURCE_SQL = `
|
|
112
|
+
INSERT INTO resources
|
|
113
|
+
(kind, id, data, updated_at, list_org, list_created_at, list_index_revision, list_indexed_at)
|
|
114
|
+
VALUES ($1, $2, $3, now(), $4, $5, $6, now())
|
|
115
|
+
ON CONFLICT (kind, id) DO UPDATE SET
|
|
116
|
+
data = excluded.data,
|
|
117
|
+
updated_at = excluded.updated_at,
|
|
118
|
+
list_org = excluded.list_org,
|
|
119
|
+
list_created_at = excluded.list_created_at,
|
|
120
|
+
list_index_revision = excluded.list_index_revision,
|
|
121
|
+
list_indexed_at = excluded.list_indexed_at`;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The rows of kind `$1` whose facts are not proven current under revision
|
|
125
|
+
* `$2` (list-index.ts), each arm one index range that fetches a row only
|
|
126
|
+
* when it matches: the stamp arm repeats the partial index's predicate
|
|
127
|
+
* verbatim (the only form the planner matches it in), the revision arms
|
|
128
|
+
* are three ranges of the revision index. A single OR over the revision
|
|
129
|
+
* is answered, without statistics, by visiting every heap row of the kind
|
|
130
|
+
* (measured), which grows with the kind; these arms are empty in steady
|
|
131
|
+
* state. They overlap (an unstamped row has no revision either), so the
|
|
132
|
+
* caller keeps one row per id.
|
|
133
|
+
*/
|
|
134
|
+
const UNPROVEN_ROWS_SQL = `
|
|
135
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_indexed_at IS DISTINCT FROM updated_at
|
|
136
|
+
UNION ALL
|
|
137
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_index_revision IS NULL
|
|
138
|
+
UNION ALL
|
|
139
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_index_revision < $2
|
|
140
|
+
UNION ALL
|
|
141
|
+
SELECT id, data FROM resources WHERE kind = $1 AND list_index_revision > $2`;
|
|
142
|
+
|
|
143
|
+
/** How many unproven rows the reconciliation at open decodes per round trip. */
|
|
144
|
+
const RECONCILE_BATCH = 500;
|
|
145
|
+
|
|
88
146
|
export class PostgresStore implements Store {
|
|
89
147
|
readonly bootstrapState: BootstrapStateStore;
|
|
90
148
|
readonly signalDedupe: SignalDedupeStore;
|
|
@@ -93,10 +151,16 @@ export class PostgresStore implements Store {
|
|
|
93
151
|
|
|
94
152
|
private pool: Pool | undefined;
|
|
95
153
|
private readonly logger: StoreLogger;
|
|
154
|
+
private readonly listIndexes: ListIndexRegistry;
|
|
96
155
|
|
|
97
|
-
private constructor(
|
|
156
|
+
private constructor(
|
|
157
|
+
pool: Pool,
|
|
158
|
+
logger: StoreLogger,
|
|
159
|
+
listIndexes: ListIndexRegistry,
|
|
160
|
+
) {
|
|
98
161
|
this.pool = pool;
|
|
99
162
|
this.logger = logger;
|
|
163
|
+
this.listIndexes = listIndexes;
|
|
100
164
|
this.bootstrapState = new PostgresBootstrapStateStore(() => this.open());
|
|
101
165
|
this.signalDedupe = new PostgresSignalDedupeStore(
|
|
102
166
|
() => this.open(),
|
|
@@ -110,14 +174,19 @@ export class PostgresStore implements Store {
|
|
|
110
174
|
|
|
111
175
|
/**
|
|
112
176
|
* Connects to databaseUrl, runs migrations (advisory-locked, so
|
|
113
|
-
* concurrent boots serialize),
|
|
114
|
-
*
|
|
115
|
-
*
|
|
177
|
+
* concurrent boots serialize), reconciles the list index (derives the
|
|
178
|
+
* facts of every unproven row of a declared kind; compare-and-set, so
|
|
179
|
+
* replicas opening together cannot conflict), and returns the ready
|
|
180
|
+
* store. Any failure here is a loud boot throw — never a degraded server
|
|
181
|
+
* (the same posture as SqliteStore.open; the composition root does not
|
|
182
|
+
* catch it).
|
|
116
183
|
*/
|
|
117
184
|
static async open(
|
|
118
185
|
databaseUrl: string,
|
|
119
186
|
logger: StoreLogger = NOOP_STORE_LOGGER,
|
|
187
|
+
options: StoreOpenOptions = {},
|
|
120
188
|
): Promise<PostgresStore> {
|
|
189
|
+
const listIndexes = new ListIndexRegistry(options.listIndexes ?? []);
|
|
121
190
|
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
122
191
|
// An idle pooled connection dropping (server restart, network blip)
|
|
123
192
|
// emits 'error' on the pool; without a listener that is a process
|
|
@@ -140,7 +209,14 @@ export class PostgresStore implements Store {
|
|
|
140
209
|
}
|
|
141
210
|
client.release();
|
|
142
211
|
|
|
143
|
-
|
|
212
|
+
const store = new PostgresStore(pool, logger, listIndexes);
|
|
213
|
+
try {
|
|
214
|
+
await store.reconcileListIndexes();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
await store.close();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
return store;
|
|
144
220
|
}
|
|
145
221
|
|
|
146
222
|
// ---------------------------------------------------------------------------
|
|
@@ -153,12 +229,31 @@ export class PostgresStore implements Store {
|
|
|
153
229
|
schema: Desc,
|
|
154
230
|
msg: MessageShape<Desc>,
|
|
155
231
|
): Promise<void> {
|
|
156
|
-
const data = toBinary(schema, msg);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
232
|
+
const data = Buffer.from(toBinary(schema, msg));
|
|
233
|
+
const kindName = apiResourceKindName(kind);
|
|
234
|
+
const facts = this.listIndexes.factsOf(kind, msg);
|
|
235
|
+
if (facts === undefined) {
|
|
236
|
+
await this.open().query(UPSERT_RESOURCE_SQL, [
|
|
237
|
+
kindName,
|
|
238
|
+
id,
|
|
239
|
+
data,
|
|
240
|
+
null,
|
|
241
|
+
null,
|
|
242
|
+
null,
|
|
243
|
+
]);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await this.withTransaction(async (client) => {
|
|
247
|
+
await client.query(UPSERT_RESOURCE_SQL, [
|
|
248
|
+
kindName,
|
|
249
|
+
id,
|
|
250
|
+
data,
|
|
251
|
+
facts.org,
|
|
252
|
+
facts.createdAt,
|
|
253
|
+
facts.revision,
|
|
254
|
+
]);
|
|
255
|
+
await replaceListKeys(client, kindName, id, facts);
|
|
256
|
+
});
|
|
162
257
|
}
|
|
163
258
|
|
|
164
259
|
async getResource<Desc extends DescMessage>(
|
|
@@ -191,22 +286,49 @@ export class PostgresStore implements Store {
|
|
|
191
286
|
// (interface.ts), so no caller I/O can extend the lock's hold time.
|
|
192
287
|
return this.withTransaction(async (client) => {
|
|
193
288
|
const result = await client.query(
|
|
194
|
-
`SELECT data
|
|
289
|
+
`SELECT data, list_indexed_at IS NOT DISTINCT FROM updated_at AS stamped, list_index_revision
|
|
290
|
+
FROM resources WHERE kind = $1 AND id = $2 FOR UPDATE`,
|
|
195
291
|
[kindName, id],
|
|
196
292
|
);
|
|
197
|
-
const row = result.rows[0] as
|
|
293
|
+
const row = result.rows[0] as
|
|
294
|
+
| {
|
|
295
|
+
data: Uint8Array;
|
|
296
|
+
stamped: boolean;
|
|
297
|
+
list_index_revision: number | null;
|
|
298
|
+
}
|
|
299
|
+
| undefined;
|
|
198
300
|
if (row === undefined) {
|
|
199
301
|
throw new ResourceNotFoundError(`${kindName}/${id}`);
|
|
200
302
|
}
|
|
201
303
|
|
|
202
304
|
const msg = fromBinary(schema, row.data);
|
|
305
|
+
const before = this.listIndexes.factsOf(kind, msg);
|
|
203
306
|
modify(msg);
|
|
307
|
+
const after = this.listIndexes.factsOf(kind, msg);
|
|
204
308
|
|
|
205
309
|
const data = toBinary(schema, msg);
|
|
206
310
|
await client.query(
|
|
207
|
-
`UPDATE resources SET data = $3, updated_at = now()
|
|
208
|
-
|
|
311
|
+
`UPDATE resources SET data = $3, updated_at = now(),
|
|
312
|
+
list_org = $4, list_created_at = $5, list_index_revision = $6, list_indexed_at = now()
|
|
313
|
+
WHERE kind = $1 AND id = $2`,
|
|
314
|
+
[
|
|
315
|
+
kindName,
|
|
316
|
+
id,
|
|
317
|
+
Buffer.from(data),
|
|
318
|
+
after?.org ?? null,
|
|
319
|
+
after?.createdAt ?? null,
|
|
320
|
+
after?.revision ?? null,
|
|
321
|
+
],
|
|
209
322
|
);
|
|
323
|
+
if (
|
|
324
|
+
after !== undefined &&
|
|
325
|
+
(!row.stamped ||
|
|
326
|
+
row.list_index_revision !== after.revision ||
|
|
327
|
+
before === undefined ||
|
|
328
|
+
!sameListKeyRows(before, after))
|
|
329
|
+
) {
|
|
330
|
+
await replaceListKeys(client, kindName, id, after);
|
|
331
|
+
}
|
|
210
332
|
return msg;
|
|
211
333
|
});
|
|
212
334
|
}
|
|
@@ -219,10 +341,73 @@ export class PostgresStore implements Store {
|
|
|
219
341
|
return (result.rows as Array<{ data: Uint8Array }>).map((row) => row.data);
|
|
220
342
|
}
|
|
221
343
|
|
|
344
|
+
async queryResources<K extends string>(
|
|
345
|
+
declaration: ListIndexDeclaration<K>,
|
|
346
|
+
query: ListIndexQuery<K>,
|
|
347
|
+
): Promise<ListIndexRow[]> {
|
|
348
|
+
this.listIndexes.require(declaration);
|
|
349
|
+
assertListIndexLimit(query.limit);
|
|
350
|
+
if (query.anyKey !== undefined && query.anyKey.length === 0) {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
const kindName = apiResourceKindName(declaration.kind);
|
|
354
|
+
|
|
355
|
+
// A key read is driven from the key table, ordered by that index's own
|
|
356
|
+
// columns, so the plan is one index range whatever the planner's
|
|
357
|
+
// statistics say (a freshly written table has none); several keys are
|
|
358
|
+
// one such read each, merged below. Without a key the organization
|
|
359
|
+
// index is the range.
|
|
360
|
+
const provenRows: ListIndexRow[] = [];
|
|
361
|
+
const reads =
|
|
362
|
+
query.anyKey === undefined
|
|
363
|
+
? [provenReadSql(kindName, declaration.revision, query, undefined)]
|
|
364
|
+
: query.anyKey.map((key) =>
|
|
365
|
+
provenReadSql(kindName, declaration.revision, query, key),
|
|
366
|
+
);
|
|
367
|
+
for (const read of reads) {
|
|
368
|
+
const result = await this.open().query(read.sql, read.params);
|
|
369
|
+
for (const row of result.rows as Array<{
|
|
370
|
+
id: string;
|
|
371
|
+
data: Uint8Array;
|
|
372
|
+
created_at: string;
|
|
373
|
+
}>) {
|
|
374
|
+
provenRows.push({
|
|
375
|
+
id: row.id,
|
|
376
|
+
data: row.data,
|
|
377
|
+
cursor: { createdAt: row.created_at, id: row.id },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const unproven = await this.open().query(UNPROVEN_ROWS_SQL, [
|
|
383
|
+
kindName,
|
|
384
|
+
declaration.revision,
|
|
385
|
+
]);
|
|
386
|
+
const unprovenRows: ListIndexRow[] = [];
|
|
387
|
+
for (const row of oneRowPerId(
|
|
388
|
+
unproven.rows as Array<{ id: string; data: Uint8Array }>,
|
|
389
|
+
)) {
|
|
390
|
+
const facts = await this.deriveAndRepair(declaration, row.id, row.data);
|
|
391
|
+
if (facts !== undefined && matchesListIndexQuery(row.id, facts, query)) {
|
|
392
|
+
unprovenRows.push({
|
|
393
|
+
id: row.id,
|
|
394
|
+
data: row.data,
|
|
395
|
+
cursor: { createdAt: facts.createdAt, id: row.id },
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return mergeListIndexRows(provenRows, unprovenRows, query.limit);
|
|
400
|
+
}
|
|
401
|
+
|
|
222
402
|
async deleteResource(kind: ApiResourceKind, id: string): Promise<void> {
|
|
403
|
+
const kindName = apiResourceKindName(kind);
|
|
223
404
|
await this.open().query(
|
|
224
405
|
`DELETE FROM resources WHERE kind = $1 AND id = $2`,
|
|
225
|
-
[
|
|
406
|
+
[kindName, id],
|
|
407
|
+
);
|
|
408
|
+
await this.open().query(
|
|
409
|
+
`DELETE FROM resource_list_keys WHERE kind = $1 AND id = $2`,
|
|
410
|
+
[kindName, id],
|
|
226
411
|
);
|
|
227
412
|
}
|
|
228
413
|
|
|
@@ -291,24 +476,55 @@ export class PostgresStore implements Store {
|
|
|
291
476
|
): Promise<boolean> {
|
|
292
477
|
// BYTEA equality in the WHERE clause makes the compare-and-swap one
|
|
293
478
|
// atomic statement: zero rows changed means the row moved on (or was
|
|
294
|
-
// deleted) since the read — a lost swap, never an upsert.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
)
|
|
304
|
-
|
|
479
|
+
// deleted) since the read — a lost swap, never an upsert. The new bytes'
|
|
480
|
+
// list facts ride the same statement; bytes a declared kind's schema
|
|
481
|
+
// cannot decode leave the row unproven (stamp NULL), for a read to skip.
|
|
482
|
+
const kindName = apiResourceKindName(kind);
|
|
483
|
+
const declaration = this.listIndexes.declarationOf(kind);
|
|
484
|
+
const facts =
|
|
485
|
+
declaration === undefined
|
|
486
|
+
? undefined
|
|
487
|
+
: factsOfBytes(declaration, newData);
|
|
488
|
+
const swap = async (client: Pool | PoolClient): Promise<boolean> => {
|
|
489
|
+
const result = await client.query(
|
|
490
|
+
`UPDATE resources SET data = $1, updated_at = now(),
|
|
491
|
+
list_org = $5, list_created_at = $6, list_index_revision = $7,
|
|
492
|
+
list_indexed_at = CASE WHEN $8::boolean THEN now() ELSE NULL END
|
|
493
|
+
WHERE kind = $2 AND id = $3 AND data = $4`,
|
|
494
|
+
[
|
|
495
|
+
Buffer.from(newData),
|
|
496
|
+
kindName,
|
|
497
|
+
id,
|
|
498
|
+
Buffer.from(expectedData),
|
|
499
|
+
facts?.org ?? null,
|
|
500
|
+
facts?.createdAt ?? null,
|
|
501
|
+
facts?.revision ?? null,
|
|
502
|
+
declaration === undefined || facts !== undefined,
|
|
503
|
+
],
|
|
504
|
+
);
|
|
505
|
+
return result.rowCount === 1;
|
|
506
|
+
};
|
|
507
|
+
if (facts === undefined) {
|
|
508
|
+
return swap(this.open());
|
|
509
|
+
}
|
|
510
|
+
return this.withTransaction(async (client) => {
|
|
511
|
+
const swapped = await swap(client);
|
|
512
|
+
if (swapped) {
|
|
513
|
+
await replaceListKeys(client, kindName, id, facts);
|
|
514
|
+
}
|
|
515
|
+
return swapped;
|
|
516
|
+
});
|
|
305
517
|
}
|
|
306
518
|
|
|
307
519
|
async deleteResourcesByKind(kind: ApiResourceKind): Promise<number> {
|
|
520
|
+
const kindName = apiResourceKindName(kind);
|
|
308
521
|
const result = await this.open().query(
|
|
309
522
|
`DELETE FROM resources WHERE kind = $1`,
|
|
310
|
-
[
|
|
523
|
+
[kindName],
|
|
311
524
|
);
|
|
525
|
+
await this.open().query(`DELETE FROM resource_list_keys WHERE kind = $1`, [
|
|
526
|
+
kindName,
|
|
527
|
+
]);
|
|
312
528
|
return result.rowCount ?? 0;
|
|
313
529
|
}
|
|
314
530
|
|
|
@@ -318,13 +534,136 @@ export class PostgresStore implements Store {
|
|
|
318
534
|
): Promise<number> {
|
|
319
535
|
// LIKE with the wildcard characters of the PREFIX escaped — sqlite's
|
|
320
536
|
// GLOB has no LIKE-metacharacter hazard, this port must not either.
|
|
537
|
+
const params = [
|
|
538
|
+
apiResourceKindName(kind),
|
|
539
|
+
`${escapeLikePattern(idPrefix)}%`,
|
|
540
|
+
];
|
|
321
541
|
const result = await this.open().query(
|
|
322
542
|
`DELETE FROM resources WHERE kind = $1 AND id LIKE $2 ESCAPE '\\'`,
|
|
323
|
-
|
|
543
|
+
params,
|
|
544
|
+
);
|
|
545
|
+
await this.open().query(
|
|
546
|
+
`DELETE FROM resource_list_keys WHERE kind = $1 AND id LIKE $2 ESCAPE '\\'`,
|
|
547
|
+
params,
|
|
324
548
|
);
|
|
325
549
|
return result.rowCount ?? 0;
|
|
326
550
|
}
|
|
327
551
|
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// The list index's repair (list-index.ts)
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* An unproven row's facts, derived from its bytes, and the row repaired
|
|
558
|
+
* when those bytes are still the stored ones — the compare-and-set on
|
|
559
|
+
* `data` means a newer write is never overwritten, and two replicas
|
|
560
|
+
* repairing one row write the same facts. Undefined when the bytes do
|
|
561
|
+
* not decode; the row stays unproven and is skipped, as a lane skips it.
|
|
562
|
+
*/
|
|
563
|
+
private async deriveAndRepair(
|
|
564
|
+
declaration: ListIndexDeclaration,
|
|
565
|
+
id: string,
|
|
566
|
+
data: Uint8Array,
|
|
567
|
+
): Promise<ListIndexFacts | undefined> {
|
|
568
|
+
const kindName = apiResourceKindName(declaration.kind);
|
|
569
|
+
const facts = factsOfBytes(declaration, data);
|
|
570
|
+
if (facts === undefined) {
|
|
571
|
+
this.logger.warn(
|
|
572
|
+
"list index: an unproven row does not decode, skipping",
|
|
573
|
+
{
|
|
574
|
+
kind: kindName,
|
|
575
|
+
id,
|
|
576
|
+
},
|
|
577
|
+
);
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
await this.withTransaction(async (client) => {
|
|
582
|
+
const result = await client.query(
|
|
583
|
+
`UPDATE resources SET list_org = $4, list_created_at = $5,
|
|
584
|
+
list_index_revision = $6, list_indexed_at = updated_at
|
|
585
|
+
WHERE kind = $1 AND id = $2 AND data = $3`,
|
|
586
|
+
[
|
|
587
|
+
kindName,
|
|
588
|
+
id,
|
|
589
|
+
Buffer.from(data),
|
|
590
|
+
facts.org,
|
|
591
|
+
facts.createdAt,
|
|
592
|
+
facts.revision,
|
|
593
|
+
],
|
|
594
|
+
);
|
|
595
|
+
if (result.rowCount === 1) {
|
|
596
|
+
await replaceListKeys(client, kindName, id, facts);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
// The facts are right whether or not they were written back; a
|
|
601
|
+
// failed repair only leaves the row for the next read to repair.
|
|
602
|
+
this.logger.warn("list index: repairing a row failed", {
|
|
603
|
+
kind: kindName,
|
|
604
|
+
id,
|
|
605
|
+
error: error instanceof Error ? error.message : String(error),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return facts;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Derives every unproven row of each declared kind, in id-keyed batches,
|
|
613
|
+
* and stamps every other kind's unstamped rows (they carry no facts), so
|
|
614
|
+
* the unproven set is empty when the server starts serving. Reads stay
|
|
615
|
+
* exact without it; it keeps them cheap.
|
|
616
|
+
*/
|
|
617
|
+
private async reconcileListIndexes(): Promise<void> {
|
|
618
|
+
const declarations = this.listIndexes.declarations();
|
|
619
|
+
let reconciled = false;
|
|
620
|
+
for (const declaration of declarations) {
|
|
621
|
+
const kindName = apiResourceKindName(declaration.kind);
|
|
622
|
+
let after = "";
|
|
623
|
+
let derived = 0;
|
|
624
|
+
for (;;) {
|
|
625
|
+
const batch = await this.open().query(
|
|
626
|
+
`SELECT id, data FROM (${UNPROVEN_ROWS_SQL}) unproven WHERE id > $3
|
|
627
|
+
ORDER BY id LIMIT $4`,
|
|
628
|
+
[kindName, declaration.revision, after, RECONCILE_BATCH],
|
|
629
|
+
);
|
|
630
|
+
// Duplicates from overlapping arms sit side by side in id order and
|
|
631
|
+
// the next batch starts after the last id, so none is repaired twice
|
|
632
|
+
// across batches and the count still ends the loop.
|
|
633
|
+
const rows = batch.rows as Array<{ id: string; data: Uint8Array }>;
|
|
634
|
+
for (const row of oneRowPerId(rows)) {
|
|
635
|
+
if (
|
|
636
|
+
(await this.deriveAndRepair(declaration, row.id, row.data)) !==
|
|
637
|
+
undefined
|
|
638
|
+
) {
|
|
639
|
+
derived += 1;
|
|
640
|
+
}
|
|
641
|
+
after = row.id;
|
|
642
|
+
}
|
|
643
|
+
if (rows.length < RECONCILE_BATCH) {
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (derived > 0) {
|
|
648
|
+
this.logger.info("list index reconciled", {
|
|
649
|
+
kind: kindName,
|
|
650
|
+
rows: derived,
|
|
651
|
+
});
|
|
652
|
+
reconciled = true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (reconciled) {
|
|
656
|
+
// Every derived row's list columns changed at once; the planner's
|
|
657
|
+
// statistics for them are refreshed before the first read plans.
|
|
658
|
+
await this.open().query(`ANALYZE resources, resource_list_keys`);
|
|
659
|
+
}
|
|
660
|
+
await this.open().query(
|
|
661
|
+
`UPDATE resources SET list_indexed_at = updated_at
|
|
662
|
+
WHERE list_indexed_at IS DISTINCT FROM updated_at AND NOT (kind = ANY($1::text[]))`,
|
|
663
|
+
[declarations.map((d) => apiResourceKindName(d.kind))],
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
328
667
|
// ---------------------------------------------------------------------------
|
|
329
668
|
// Audit operations
|
|
330
669
|
// ---------------------------------------------------------------------------
|
|
@@ -1380,6 +1719,123 @@ class PostgresPendingOAuthStateStore implements PendingOAuthStateStore {
|
|
|
1380
1719
|
}
|
|
1381
1720
|
}
|
|
1382
1721
|
|
|
1722
|
+
/**
|
|
1723
|
+
* A row's key rows made equal to its facts: one row per key the row
|
|
1724
|
+
* names, the creation instant copied in so a parent's rows are one index
|
|
1725
|
+
* range. Runs on the caller's transaction, beside the row write it
|
|
1726
|
+
* belongs to.
|
|
1727
|
+
*/
|
|
1728
|
+
async function replaceListKeys(
|
|
1729
|
+
client: PoolClient,
|
|
1730
|
+
kindName: string,
|
|
1731
|
+
id: string,
|
|
1732
|
+
facts: ListIndexFacts,
|
|
1733
|
+
): Promise<void> {
|
|
1734
|
+
await client.query(
|
|
1735
|
+
`DELETE FROM resource_list_keys WHERE kind = $1 AND id = $2`,
|
|
1736
|
+
[kindName, id],
|
|
1737
|
+
);
|
|
1738
|
+
if (facts.keys.length === 0) {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const params: unknown[] = [kindName, id, facts.createdAt];
|
|
1742
|
+
const tuples = facts.keys.map((entry) => {
|
|
1743
|
+
params.push(entry.key, entry.value);
|
|
1744
|
+
return `($1, $2, $${params.length - 1}, $${params.length}, $3)`;
|
|
1745
|
+
});
|
|
1746
|
+
await client.query(
|
|
1747
|
+
`INSERT INTO resource_list_keys (kind, id, key, value, created_at) VALUES ${tuples.join(", ")}`,
|
|
1748
|
+
params,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* One proven read of `queryResources`: through one key's index range when
|
|
1754
|
+
* `key` is given (the key table drives, its copied creation instant is the
|
|
1755
|
+
* order), else through the organization index. Only rows whose facts are
|
|
1756
|
+
* proven current are returned; the unproven arm answers for the rest.
|
|
1757
|
+
*/
|
|
1758
|
+
function provenReadSql(
|
|
1759
|
+
kindName: string,
|
|
1760
|
+
revision: number,
|
|
1761
|
+
query: ListIndexQuery,
|
|
1762
|
+
key: { readonly name: string; readonly value: string } | undefined,
|
|
1763
|
+
): { sql: string; params: unknown[] } {
|
|
1764
|
+
const params: unknown[] = [kindName, revision];
|
|
1765
|
+
const param = (value: unknown): string => {
|
|
1766
|
+
params.push(value);
|
|
1767
|
+
return `$${params.length}`;
|
|
1768
|
+
};
|
|
1769
|
+
const created = key === undefined ? "r.list_created_at" : "k.created_at";
|
|
1770
|
+
const id = key === undefined ? `r.id COLLATE "C"` : `k.id COLLATE "C"`;
|
|
1771
|
+
const where = [
|
|
1772
|
+
`r.list_indexed_at = r.updated_at`,
|
|
1773
|
+
`r.list_index_revision = $2`,
|
|
1774
|
+
];
|
|
1775
|
+
let from = `resources r`;
|
|
1776
|
+
if (key === undefined) {
|
|
1777
|
+
where.unshift(`r.kind = $1`);
|
|
1778
|
+
} else {
|
|
1779
|
+
// The row behind each key is fetched by primary key and nothing else:
|
|
1780
|
+
// `OFFSET 0` keeps the subquery from being flattened, so a planner
|
|
1781
|
+
// without statistics cannot reach it through the revision index
|
|
1782
|
+
// instead (measured: a scan of the kind per key row).
|
|
1783
|
+
from = `resource_list_keys k CROSS JOIN LATERAL (
|
|
1784
|
+
SELECT id, data, updated_at, list_indexed_at, list_index_revision, list_org
|
|
1785
|
+
FROM resources WHERE kind = k.kind AND id = k.id OFFSET 0
|
|
1786
|
+
) r`;
|
|
1787
|
+
where.unshift(
|
|
1788
|
+
`k.kind = $1`,
|
|
1789
|
+
`k.key = ${param(key.name)}`,
|
|
1790
|
+
`k.value = ${param(key.value)}`,
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
if (query.org !== undefined && query.org !== "") {
|
|
1794
|
+
where.push(`r.list_org = ${param(query.org)}`);
|
|
1795
|
+
}
|
|
1796
|
+
if (query.createdAtOrAfter !== undefined) {
|
|
1797
|
+
where.push(
|
|
1798
|
+
`(${created} >= ${param(query.createdAtOrAfter)} OR ${created} = '')`,
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
if (query.after !== undefined) {
|
|
1802
|
+
const afterCreated = param(query.after.createdAt);
|
|
1803
|
+
const afterId = param(query.after.id);
|
|
1804
|
+
where.push(
|
|
1805
|
+
`(${created} < ${afterCreated} OR (${created} = ${afterCreated} AND ${id} < ${afterId}))`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
const limit = query.limit === undefined ? "" : ` LIMIT ${param(query.limit)}`;
|
|
1809
|
+
return {
|
|
1810
|
+
sql: `SELECT r.id, r.data, ${created} AS created_at FROM ${from}
|
|
1811
|
+
WHERE ${where.join(" AND ")}
|
|
1812
|
+
ORDER BY ${created} DESC, ${id} DESC${limit}`,
|
|
1813
|
+
params,
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/** Keeps the first row per id; the unproven arms overlap. */
|
|
1818
|
+
function oneRowPerId<T extends { readonly id: string }>(
|
|
1819
|
+
rows: ReadonlyArray<T>,
|
|
1820
|
+
): T[] {
|
|
1821
|
+
const seen = new Set<string>();
|
|
1822
|
+
return rows.filter((row) =>
|
|
1823
|
+
seen.has(row.id) ? false : (seen.add(row.id), true),
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/** A declared kind's facts from stored bytes; undefined when they do not decode. */
|
|
1828
|
+
function factsOfBytes(
|
|
1829
|
+
declaration: ListIndexDeclaration,
|
|
1830
|
+
data: Uint8Array,
|
|
1831
|
+
): ListIndexFacts | undefined {
|
|
1832
|
+
try {
|
|
1833
|
+
return listIndexFactsOf(declaration, fromBinary(declaration.schema, data));
|
|
1834
|
+
} catch {
|
|
1835
|
+
return undefined;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1383
1839
|
/** Escapes LIKE metacharacters so a prefix matches literally. */
|
|
1384
1840
|
function escapeLikePattern(value: string): string {
|
|
1385
1841
|
return value
|