@sixb/pg 0.1.12 → 0.1.14
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/auth-storage/access-tokens.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +2 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +7 -4
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +8 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +403 -115
- package/dist/index.js.map +16 -15
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +0 -2
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +2 -1
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/pg-file-upload-session-storage.d.ts +6 -0
- package/dist/pg-file-upload-session-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/auth-storage/access-tokens.ts +10 -14
- package/src/auth-storage/auth-storage.ts +47 -81
- package/src/auth-storage/device-authorizations.ts +3 -4
- package/src/auth-storage/rows.ts +18 -8
- package/src/auth-storage/sessions.ts +82 -4
- package/src/auth-storage/shared.ts +8 -2
- package/src/index.ts +5 -0
- package/src/migrations/046-workflow-intervention-principals.sql +15 -0
- package/src/migrations/047-ontology-commit-attribution.sql +50 -0
- package/src/migrations/048-file-upload-sessions.sql +34 -0
- package/src/migrations/049-native-sessions.sql +19 -0
- package/src/migrations/050-directory-group-memberships.sql +5 -0
- package/src/migrations.ts +16 -0
- package/src/objects/storage.ts +2 -0
- package/src/ontology-storage/materializations.ts +15 -58
- package/src/ontology-storage/shared.ts +7 -3
- package/src/pg-file-upload-session-storage.ts +143 -0
- package/src/pg-workflow-intervention-storage.ts +11 -15
package/src/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { PgAiLimitStorage } from "./pg-ai-limit-storage"
|
|
|
42
42
|
import { PgAiUsageStorage } from "./pg-ai-usage-storage"
|
|
43
43
|
import { createPgClient, type SQL, type SQLClient } from "./pg-client"
|
|
44
44
|
import { PgExecutionStorage } from "./pg-execution-storage"
|
|
45
|
+
import { PgFileUploadSessionStorage } from "./pg-file-upload-session-storage"
|
|
45
46
|
import { PgPipelineRunStorage } from "./pg-pipeline-run-storage"
|
|
46
47
|
import { PgProjectionRunStorage } from "./pg-projection-run-storage"
|
|
47
48
|
import { PgRulesStorage } from "./pg-rules-storage"
|
|
@@ -158,6 +159,7 @@ export class PostgresStorage implements MigrationCapableStorage {
|
|
|
158
159
|
readonly connectorConnections: PgConnectorConnectionStorage
|
|
159
160
|
readonly shareGrants: PgShareGrantStorage
|
|
160
161
|
readonly shareSessions: PgShareSessionStorage
|
|
162
|
+
readonly fileUploadSessions: PgFileUploadSessionStorage
|
|
161
163
|
readonly migrators: readonly StorageMigrator[]
|
|
162
164
|
|
|
163
165
|
private readonly sql: SQL
|
|
@@ -235,6 +237,7 @@ export class PostgresStorage implements MigrationCapableStorage {
|
|
|
235
237
|
this.connectorConnections = createOperationScopedFacade(stores.connectorConnections, scope)
|
|
236
238
|
this.shareGrants = createOperationScopedFacade(stores.shareGrants, scope)
|
|
237
239
|
this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope)
|
|
240
|
+
this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope)
|
|
238
241
|
registerPostgresStorageTestingAdapter(this, (durationMs) =>
|
|
239
242
|
stores.connectorConnections.advanceTimeForTesting(durationMs)
|
|
240
243
|
)
|
|
@@ -371,6 +374,7 @@ function createPostgresStores(
|
|
|
371
374
|
connectorConnections: new PgConnectorConnectionStorage(sql),
|
|
372
375
|
shareGrants,
|
|
373
376
|
shareSessions,
|
|
377
|
+
fileUploadSessions: new PgFileUploadSessionStorage(sql),
|
|
374
378
|
}
|
|
375
379
|
}
|
|
376
380
|
|
|
@@ -395,6 +399,7 @@ interface PostgresStoreSet {
|
|
|
395
399
|
readonly connectorConnections: PgConnectorConnectionStorage
|
|
396
400
|
readonly shareGrants: PgShareGrantStorage
|
|
397
401
|
readonly shareSessions: PgShareSessionStorage
|
|
402
|
+
readonly fileUploadSessions: PgFileUploadSessionStorage
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
function resolveTimeoutMillis(value: number | undefined, label: string): number | undefined {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Intervention actors were stored as { principalType, principalId }. Rewrite them to the
|
|
2
|
+
-- canonical Principal shape { type, id } used by every other stored principal.
|
|
3
|
+
UPDATE workflow_interventions
|
|
4
|
+
SET submitted_by = jsonb_build_object(
|
|
5
|
+
'type', submitted_by->>'principalType',
|
|
6
|
+
'id', submitted_by->>'principalId'
|
|
7
|
+
)
|
|
8
|
+
WHERE submitted_by->>'principalType' IS NOT NULL;
|
|
9
|
+
|
|
10
|
+
UPDATE workflow_interventions
|
|
11
|
+
SET cancelled_by = jsonb_build_object(
|
|
12
|
+
'type', cancelled_by->>'principalType',
|
|
13
|
+
'id', cancelled_by->>'principalId'
|
|
14
|
+
)
|
|
15
|
+
WHERE cancelled_by->>'principalType' IS NOT NULL;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Commits and their events name who requested the write and what ran it, replacing the
|
|
2
|
+
-- authority-only `actor`. Both are copied from the commit's execution, which stays canonical and
|
|
3
|
+
-- is immutable, so the backfill is exact.
|
|
4
|
+
ALTER TABLE ontology_commits
|
|
5
|
+
ADD COLUMN requested_by JSONB,
|
|
6
|
+
ADD COLUMN executor JSONB;
|
|
7
|
+
|
|
8
|
+
UPDATE ontology_commits c SET
|
|
9
|
+
requested_by = CASE
|
|
10
|
+
WHEN e.requested_by_user_id IS NOT NULL
|
|
11
|
+
THEN jsonb_build_object('type', 'user', 'id', e.requested_by_user_id)
|
|
12
|
+
WHEN e.requested_by_service_account_id IS NOT NULL
|
|
13
|
+
THEN jsonb_build_object('type', 'serviceAccount', 'id', e.requested_by_service_account_id)
|
|
14
|
+
END,
|
|
15
|
+
executor = CASE e.executor_kind
|
|
16
|
+
WHEN 'request' THEN jsonb_build_object('type', 'request', 'requestId', e.executor_id)
|
|
17
|
+
WHEN 'agent' THEN jsonb_build_object('type', 'agent', 'runId', e.executor_id)
|
|
18
|
+
WHEN 'kernel' THEN jsonb_build_object(
|
|
19
|
+
'type', 'kernel',
|
|
20
|
+
'operation',
|
|
21
|
+
CASE e.authority_kernel_operation
|
|
22
|
+
WHEN 'ontology.recover'
|
|
23
|
+
THEN jsonb_build_object('type', 'ontology.recover', 'recoveryId', e.executor_id)
|
|
24
|
+
ELSE jsonb_build_object('type', 'ontology.indexVectors', 'indexingId', e.executor_id)
|
|
25
|
+
END
|
|
26
|
+
)
|
|
27
|
+
ELSE jsonb_build_object(
|
|
28
|
+
'type', 'primitive',
|
|
29
|
+
'kind', e.executor_kind,
|
|
30
|
+
'id', e.authority_primitive_id,
|
|
31
|
+
'runId', e.executor_id
|
|
32
|
+
)
|
|
33
|
+
END
|
|
34
|
+
FROM executions e
|
|
35
|
+
WHERE e.project_id = c.project_id AND e.id = c.execution_id;
|
|
36
|
+
|
|
37
|
+
ALTER TABLE ontology_commits ALTER COLUMN executor SET NOT NULL;
|
|
38
|
+
|
|
39
|
+
-- Events not yet published would otherwise reach consumers in the retired shape.
|
|
40
|
+
UPDATE ontology_outbox o SET envelope =
|
|
41
|
+
(o.envelope - 'actor')
|
|
42
|
+
|| jsonb_build_object('executor', c.executor)
|
|
43
|
+
|| CASE
|
|
44
|
+
WHEN c.requested_by IS NULL THEN '{}'::jsonb
|
|
45
|
+
ELSE jsonb_build_object('requestedBy', c.requested_by)
|
|
46
|
+
END
|
|
47
|
+
FROM ontology_commits c
|
|
48
|
+
WHERE c.project_id = o.project_id AND c.id = o.commit_id;
|
|
49
|
+
|
|
50
|
+
ALTER TABLE ontology_commits DROP COLUMN actor;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Durable staged-upload sessions, so an upload survives restarts and spans API replicas.
|
|
2
|
+
CREATE TABLE file_upload_sessions (
|
|
3
|
+
id TEXT PRIMARY KEY CHECK (length(trim(id)) > 0),
|
|
4
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
5
|
+
owner_principal_type TEXT NOT NULL
|
|
6
|
+
CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
7
|
+
owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
|
|
8
|
+
strategy TEXT NOT NULL CHECK (strategy IN ('server', 'direct-put', 'multipart')),
|
|
9
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'aborted')),
|
|
10
|
+
file_name TEXT,
|
|
11
|
+
media_type TEXT,
|
|
12
|
+
logical_path TEXT,
|
|
13
|
+
expected_size_bytes BIGINT CHECK (expected_size_bytes IS NULL OR expected_size_bytes >= 0),
|
|
14
|
+
expected_digest TEXT,
|
|
15
|
+
provider_upload JSONB CHECK (provider_upload IS NULL OR jsonb_typeof(provider_upload) = 'object'),
|
|
16
|
+
signed_parts JSONB NOT NULL CHECK (jsonb_typeof(signed_parts) = 'array'),
|
|
17
|
+
file_ref JSONB CHECK (file_ref IS NULL OR jsonb_typeof(file_ref) = 'object'),
|
|
18
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
19
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
20
|
+
completed_at TIMESTAMPTZ,
|
|
21
|
+
aborted_at TIMESTAMPTZ,
|
|
22
|
+
-- Derived by core (fileUploadSessionReapAt); NULL while an abandoned upload awaits its abort.
|
|
23
|
+
reap_at TIMESTAMPTZ,
|
|
24
|
+
CHECK ((status = 'completed') = (completed_at IS NOT NULL)),
|
|
25
|
+
CHECK ((status = 'aborted') = (aborted_at IS NOT NULL))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX idx_file_upload_sessions_reap
|
|
29
|
+
ON file_upload_sessions (reap_at)
|
|
30
|
+
WHERE reap_at IS NOT NULL;
|
|
31
|
+
|
|
32
|
+
CREATE INDEX idx_file_upload_sessions_abandoned
|
|
33
|
+
ON file_upload_sessions (project_id, expires_at)
|
|
34
|
+
WHERE status = 'pending' AND provider_upload IS NOT NULL;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Native clients, such as the CLI, hold their session as bearer tokens: a
|
|
2
|
+
-- short-lived access token, whose hash is `token_hash`, and a rotating refresh token. Browser
|
|
3
|
+
-- sessions leave these columns NULL; `refresh_token_hash IS NULL` is what marks a cookie session.
|
|
4
|
+
ALTER TABLE auth_sessions
|
|
5
|
+
ADD COLUMN client_name TEXT,
|
|
6
|
+
ADD COLUMN access_expires_at TIMESTAMPTZ,
|
|
7
|
+
ADD COLUMN refresh_token_hash TEXT,
|
|
8
|
+
ADD COLUMN previous_refresh_token_hash TEXT,
|
|
9
|
+
ADD COLUMN refreshed_at TIMESTAMPTZ,
|
|
10
|
+
ADD CONSTRAINT auth_sessions_bearer_state CHECK (
|
|
11
|
+
(refresh_token_hash IS NULL) = (client_name IS NULL)
|
|
12
|
+
AND (refresh_token_hash IS NULL) = (access_expires_at IS NULL)
|
|
13
|
+
AND (refresh_token_hash IS NOT NULL OR previous_refresh_token_hash IS NULL)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- The device flow now starts a session instead of issuing a personal access token.
|
|
17
|
+
ALTER TABLE auth_device_authorizations
|
|
18
|
+
DROP COLUMN token_name,
|
|
19
|
+
DROP COLUMN token_expires_at;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- A membership the identity provider owns is recorded with source 'directory' and synced to the
|
|
2
|
+
-- provider's groups on every sign-in. Service accounts never sign in, so their table is unchanged.
|
|
3
|
+
ALTER TABLE auth_group_memberships DROP CONSTRAINT auth_group_memberships_source_check;
|
|
4
|
+
ALTER TABLE auth_group_memberships ADD CONSTRAINT auth_group_memberships_source_check
|
|
5
|
+
CHECK (source IN ('invitation', 'manual', 'agent', 'directory'));
|
package/src/migrations.ts
CHANGED
|
@@ -99,6 +99,16 @@ import vectorBatchingSql from "./migrations/044-vector-batching.sql" with { type
|
|
|
99
99
|
import agentThreadSandboxStateSql from "./migrations/045-agent-thread-sandbox-state.sql" with {
|
|
100
100
|
type: "text",
|
|
101
101
|
}
|
|
102
|
+
import workflowInterventionPrincipalsSql from "./migrations/046-workflow-intervention-principals.sql" with {
|
|
103
|
+
type: "text",
|
|
104
|
+
}
|
|
105
|
+
import ontologyCommitAttributionSql from "./migrations/047-ontology-commit-attribution.sql" with {
|
|
106
|
+
type: "text",
|
|
107
|
+
}
|
|
108
|
+
import nativeSessionsSql from "./migrations/049-native-sessions.sql" with { type: "text" }
|
|
109
|
+
import directoryGroupMembershipsSql from "./migrations/050-directory-group-memberships.sql" with {
|
|
110
|
+
type: "text",
|
|
111
|
+
}
|
|
102
112
|
import type { SQL, SQLClient } from "./pg-client"
|
|
103
113
|
|
|
104
114
|
export interface PostgresMigrationContext {
|
|
@@ -397,6 +407,11 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
|
|
|
397
407
|
pgSql("043-vector-indexing", vectorIndexingSql),
|
|
398
408
|
pgSql("044-vector-batching", vectorBatchingSql),
|
|
399
409
|
pgSql("045-agent-thread-sandbox-state", agentThreadSandboxStateSql),
|
|
410
|
+
pgSql("046-workflow-intervention-principals", workflowInterventionPrincipalsSql),
|
|
411
|
+
pgSql("047-ontology-commit-attribution", ontologyCommitAttributionSql),
|
|
412
|
+
pgSql("048-file-upload-sessions", fileUploadSessionsSql),
|
|
413
|
+
pgSql("049-native-sessions", nativeSessionsSql),
|
|
414
|
+
pgSql("050-directory-group-memberships", directoryGroupMembershipsSql),
|
|
400
415
|
],
|
|
401
416
|
})
|
|
402
417
|
|
|
@@ -420,3 +435,4 @@ import outboxPublicationOrderSql from "./migrations/038-outbox-publication-order
|
|
|
420
435
|
import connectorOptionalPkceSql from "./migrations/040-connector-optional-pkce.sql" with {
|
|
421
436
|
type: "text",
|
|
422
437
|
}
|
|
438
|
+
import fileUploadSessionsSql from "./migrations/048-file-upload-sessions.sql" with { type: "text" }
|
package/src/objects/storage.ts
CHANGED
|
@@ -88,6 +88,8 @@ const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
|
88
88
|
decimal: { equality: true, ordering: true },
|
|
89
89
|
date: { equality: true, ordering: true },
|
|
90
90
|
timestamp: { equality: true, ordering: true },
|
|
91
|
+
// jsonb equality and containment compare both fields of `{ type, id }`, whatever key order.
|
|
92
|
+
userRef: { equality: true },
|
|
91
93
|
},
|
|
92
94
|
limits: {
|
|
93
95
|
totalCount: true,
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EffectiveChangeCounts,
|
|
3
|
-
ExpectedLinkRevision,
|
|
4
|
-
ExpectedObjectRevision,
|
|
5
|
-
} from "@sixb/core/internal/materialization"
|
|
1
|
+
import type { EffectiveChangeCounts } from "@sixb/core/internal/materialization"
|
|
6
2
|
import {
|
|
7
3
|
assertPinnedDatasetWatermark,
|
|
8
4
|
linkRefKey,
|
|
@@ -16,6 +12,9 @@ import {
|
|
|
16
12
|
telemetryPointSortKey,
|
|
17
13
|
} from "@sixb/core/internal/materialization"
|
|
18
14
|
import {
|
|
15
|
+
assertExpectedLinkRevision,
|
|
16
|
+
assertExpectedLinkScopeRevision,
|
|
17
|
+
assertExpectedObjectRevision,
|
|
19
18
|
assertMaterializationFinalizationCorrelation,
|
|
20
19
|
assertMaterializationHeader,
|
|
21
20
|
assertMaterializationLaneCompletion,
|
|
@@ -126,23 +125,21 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
126
125
|
true
|
|
127
126
|
)
|
|
128
127
|
for (const expected of input.expected.objects) {
|
|
129
|
-
|
|
128
|
+
assertExpectedObjectRevision(
|
|
129
|
+
objectRevisions.get(objectRefKey(expected.ref)) ?? null,
|
|
130
|
+
expected
|
|
131
|
+
)
|
|
130
132
|
}
|
|
131
133
|
const linkRevisions = await reader.effectiveLinkLastCommits(
|
|
132
134
|
input.expected.links.map((expected) => expected.ref),
|
|
133
135
|
true
|
|
134
136
|
)
|
|
135
137
|
for (const expected of input.expected.links) {
|
|
136
|
-
|
|
138
|
+
assertExpectedLinkRevision(linkRevisions.get(linkRefKey(expected.ref)), expected)
|
|
137
139
|
}
|
|
138
140
|
const linkScopeRevisions = await reader.linkScopeRevisions(input.expected.linkScopes)
|
|
139
141
|
for (const [index, expected] of input.expected.linkScopes.entries()) {
|
|
140
|
-
|
|
141
|
-
throw new MaterializationConflictError(
|
|
142
|
-
"effective-state",
|
|
143
|
-
`Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`
|
|
144
|
-
)
|
|
145
|
-
}
|
|
142
|
+
assertExpectedLinkScopeRevision(linkScopeRevisions[index]?.fingerprint, expected)
|
|
146
143
|
}
|
|
147
144
|
const points = await reader.exactPoints(input.expected.points, true)
|
|
148
145
|
const pointRevisions = new Map(
|
|
@@ -514,48 +511,6 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
514
511
|
}
|
|
515
512
|
}
|
|
516
513
|
|
|
517
|
-
private assertObject(
|
|
518
|
-
row: { readonly version: number; readonly lastCommitId: string | null } | null,
|
|
519
|
-
expected: ExpectedObjectRevision
|
|
520
|
-
): void {
|
|
521
|
-
if (!expected.exists) {
|
|
522
|
-
if (row) {
|
|
523
|
-
throw new MaterializationConflictError(
|
|
524
|
-
"effective-state",
|
|
525
|
-
`Expected object ${objectRefKey(expected.ref)} to be absent.`
|
|
526
|
-
)
|
|
527
|
-
}
|
|
528
|
-
return
|
|
529
|
-
}
|
|
530
|
-
if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
|
|
531
|
-
throw new MaterializationConflictError(
|
|
532
|
-
"effective-state",
|
|
533
|
-
`Expected object ${objectRefKey(expected.ref)} changed.`
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
private assertLink(
|
|
539
|
-
lastCommitId: string | null | undefined,
|
|
540
|
-
expected: ExpectedLinkRevision
|
|
541
|
-
): void {
|
|
542
|
-
if (!expected.exists) {
|
|
543
|
-
if (lastCommitId !== undefined) {
|
|
544
|
-
throw new MaterializationConflictError(
|
|
545
|
-
"effective-state",
|
|
546
|
-
`Expected link ${linkRefKey(expected.ref)} to be absent.`
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
|
|
552
|
-
throw new MaterializationConflictError(
|
|
553
|
-
"effective-state",
|
|
554
|
-
`Expected link ${linkRefKey(expected.ref)} changed.`
|
|
555
|
-
)
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
514
|
private async assertFinalization(
|
|
560
515
|
session: PgMaterializationSessionState,
|
|
561
516
|
input: FinalizeMaterializationInput
|
|
@@ -873,18 +828,20 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
873
828
|
): Promise<OntologyCommitRecord> {
|
|
874
829
|
const { commit } = header
|
|
875
830
|
const origin = originColumns(commit.origin)
|
|
876
|
-
const
|
|
831
|
+
const requestedBy =
|
|
832
|
+
commit.requestedBy === undefined ? null : jsonParameter(this.sql, commit.requestedBy)
|
|
877
833
|
const rows = await this.sql<PgOntologyCommitRow[]>`
|
|
878
834
|
INSERT INTO ontology_commits (
|
|
879
835
|
project_id, id, idempotency_key, request_hash, execution_id,
|
|
880
|
-
origin_kind, origin_run_id, origin_batch_ordinal, origin,
|
|
836
|
+
origin_kind, origin_run_id, origin_batch_ordinal, origin, requested_by, executor,
|
|
881
837
|
ontology_revision, projection_revision, ownership_hash,
|
|
882
838
|
intent, result, committed_at
|
|
883
839
|
) VALUES (
|
|
884
840
|
${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
|
|
885
841
|
${commit.executionId},
|
|
886
842
|
${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
|
|
887
|
-
${jsonParameter(this.sql, commit.origin)}, ${
|
|
843
|
+
${jsonParameter(this.sql, commit.origin)}, ${requestedBy},
|
|
844
|
+
${jsonParameter(this.sql, commit.executor)}, ${commit.ontologyRevision},
|
|
888
845
|
${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
|
|
889
846
|
${jsonParameter(this.sql, commit.intent)},
|
|
890
847
|
${jsonParameter(this.sql, input.finalization.result)}, ${commit.committedAt}
|
|
@@ -39,7 +39,8 @@ export interface PgOntologyCommitRow {
|
|
|
39
39
|
readonly origin_run_id: string | null
|
|
40
40
|
readonly origin_batch_ordinal: number | string | null
|
|
41
41
|
readonly origin: unknown
|
|
42
|
-
readonly
|
|
42
|
+
readonly requested_by: unknown | null
|
|
43
|
+
readonly executor: unknown
|
|
43
44
|
readonly ontology_revision: string
|
|
44
45
|
readonly projection_revision: string | null
|
|
45
46
|
readonly ownership_hash: string | null
|
|
@@ -187,11 +188,14 @@ export function commitRecord(row: PgOntologyCommitRow): OntologyCommitRecord {
|
|
|
187
188
|
requestHash: row.request_hash,
|
|
188
189
|
executionId: row.execution_id,
|
|
189
190
|
origin: structuredClone(row.origin) as OntologyCommitWrite["origin"],
|
|
190
|
-
...(row.
|
|
191
|
+
...(row.requested_by === null
|
|
191
192
|
? {}
|
|
192
193
|
: {
|
|
193
|
-
|
|
194
|
+
requestedBy: structuredClone(row.requested_by) as NonNullable<
|
|
195
|
+
OntologyCommitWrite["requestedBy"]
|
|
196
|
+
>,
|
|
194
197
|
}),
|
|
198
|
+
executor: structuredClone(row.executor) as OntologyCommitWrite["executor"],
|
|
195
199
|
ontologyRevision: row.ontology_revision,
|
|
196
200
|
...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
|
|
197
201
|
...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DurableFileUploadSessions,
|
|
3
|
+
type FileUploadSessionPersistence,
|
|
4
|
+
type FileUploadSessionPersistenceBackend,
|
|
5
|
+
fileUploadSessionReapAt,
|
|
6
|
+
parseFileUploadSessionRow,
|
|
7
|
+
} from "@sixb/core/internal/file-upload-session-storage-provider"
|
|
8
|
+
import type { FileUploadSession, ListAbandonedFileUploadSessionsInput } from "@sixb/core/storage"
|
|
9
|
+
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
10
|
+
|
|
11
|
+
export class PgFileUploadSessionStorage extends DurableFileUploadSessions {
|
|
12
|
+
constructor(sql: PgStoreClient) {
|
|
13
|
+
super(new PgFileUploadSessionBackend(sql))
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class PgFileUploadSessionBackend implements FileUploadSessionPersistenceBackend {
|
|
18
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
19
|
+
|
|
20
|
+
read<T>(run: (persistence: FileUploadSessionPersistence) => Promise<T>): Promise<T> {
|
|
21
|
+
return run(new PgFileUploadSessionPersistence(this.sql, false))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
transaction<T>(run: (persistence: FileUploadSessionPersistence) => Promise<T>): Promise<T> {
|
|
25
|
+
return runPgTransaction(this.sql, (tx) => run(new PgFileUploadSessionPersistence(tx, true)))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class PgFileUploadSessionPersistence implements FileUploadSessionPersistence {
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly sql: PgStoreClient,
|
|
32
|
+
private readonly locking: boolean
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
async now(): Promise<Date> {
|
|
36
|
+
const [row] = await this.sql<{ readonly now: Date }[]>`SELECT clock_timestamp() AS now`
|
|
37
|
+
if (!row) throw new Error("[SixbPg] Database clock returned no row.")
|
|
38
|
+
return new Date(row.now)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async insert(session: FileUploadSession): Promise<boolean> {
|
|
42
|
+
// A unique violation would abort the surrounding transaction; report the conflict instead.
|
|
43
|
+
const rows = await this.sql`
|
|
44
|
+
INSERT INTO file_upload_sessions (
|
|
45
|
+
id, project_id, owner_principal_type, owner_principal_id, strategy, status, file_name,
|
|
46
|
+
media_type, logical_path, expected_size_bytes, expected_digest, provider_upload,
|
|
47
|
+
signed_parts, file_ref, created_at, expires_at, completed_at, aborted_at, reap_at
|
|
48
|
+
) VALUES (
|
|
49
|
+
${session.id}, ${session.projectId}, ${session.principal.type}, ${session.principal.id},
|
|
50
|
+
${session.strategy}, ${session.status}, ${session.fileName ?? null}, ${session.mediaType ?? null},
|
|
51
|
+
${session.logicalPath ?? null}, ${session.expectedSizeBytes ?? null},
|
|
52
|
+
${session.expectedDigest ?? null}, ${jsonText(session.providerUpload)}::text::jsonb,
|
|
53
|
+
${JSON.stringify(session.signedParts)}::text::jsonb, ${jsonText(session.fileRef)}::text::jsonb,
|
|
54
|
+
${session.createdAt}, ${session.expiresAt}, ${session.completedAt ?? null},
|
|
55
|
+
${session.abortedAt ?? null}, ${fileUploadSessionReapAt(session)}
|
|
56
|
+
)
|
|
57
|
+
ON CONFLICT (id) DO NOTHING
|
|
58
|
+
RETURNING id
|
|
59
|
+
`
|
|
60
|
+
return rows.length === 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async get(uploadId: string): Promise<FileUploadSession | null> {
|
|
64
|
+
const [row] = this.locking
|
|
65
|
+
? await this.sql<PgFileUploadSessionRow[]>`
|
|
66
|
+
SELECT * FROM file_upload_sessions WHERE id = ${uploadId} FOR UPDATE
|
|
67
|
+
`
|
|
68
|
+
: await this.sql<PgFileUploadSessionRow[]>`
|
|
69
|
+
SELECT * FROM file_upload_sessions WHERE id = ${uploadId}
|
|
70
|
+
`
|
|
71
|
+
return row ? sessionFromRow(row) : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async update(session: FileUploadSession): Promise<void> {
|
|
75
|
+
await this.sql`
|
|
76
|
+
UPDATE file_upload_sessions
|
|
77
|
+
SET status = ${session.status},
|
|
78
|
+
provider_upload = ${jsonText(session.providerUpload)}::text::jsonb,
|
|
79
|
+
signed_parts = ${JSON.stringify(session.signedParts)}::text::jsonb,
|
|
80
|
+
file_ref = ${jsonText(session.fileRef)}::text::jsonb,
|
|
81
|
+
created_at = ${session.createdAt},
|
|
82
|
+
expires_at = ${session.expiresAt},
|
|
83
|
+
completed_at = ${session.completedAt ?? null},
|
|
84
|
+
aborted_at = ${session.abortedAt ?? null},
|
|
85
|
+
reap_at = ${fileUploadSessionReapAt(session)}
|
|
86
|
+
WHERE id = ${session.id}
|
|
87
|
+
`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async listAbandoned(
|
|
91
|
+
input: ListAbandonedFileUploadSessionsInput
|
|
92
|
+
): Promise<readonly FileUploadSession[]> {
|
|
93
|
+
const rows = await this.sql<PgFileUploadSessionRow[]>`
|
|
94
|
+
SELECT * FROM file_upload_sessions
|
|
95
|
+
WHERE project_id = ${input.projectId} AND status = 'pending'
|
|
96
|
+
AND provider_upload IS NOT NULL AND expires_at <= ${input.now}
|
|
97
|
+
ORDER BY expires_at, id COLLATE "C"
|
|
98
|
+
LIMIT ${input.limit}
|
|
99
|
+
`
|
|
100
|
+
return rows.map(sessionFromRow)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async deleteReapable(now: Date): Promise<number> {
|
|
104
|
+
const rows = await this.sql`
|
|
105
|
+
DELETE FROM file_upload_sessions
|
|
106
|
+
WHERE reap_at IS NOT NULL AND reap_at <= ${now}
|
|
107
|
+
RETURNING id
|
|
108
|
+
`
|
|
109
|
+
return rows.length
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface PgFileUploadSessionRow {
|
|
114
|
+
readonly [column: string]: unknown
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sessionFromRow(row: PgFileUploadSessionRow): FileUploadSession {
|
|
118
|
+
return parseFileUploadSessionRow({
|
|
119
|
+
id: row.id,
|
|
120
|
+
projectId: row.project_id,
|
|
121
|
+
ownerPrincipalType: row.owner_principal_type,
|
|
122
|
+
ownerPrincipalId: row.owner_principal_id,
|
|
123
|
+
strategy: row.strategy,
|
|
124
|
+
status: row.status,
|
|
125
|
+
fileName: row.file_name,
|
|
126
|
+
mediaType: row.media_type,
|
|
127
|
+
logicalPath: row.logical_path,
|
|
128
|
+
expectedSizeBytes: row.expected_size_bytes,
|
|
129
|
+
expectedDigest: row.expected_digest,
|
|
130
|
+
providerUpload: row.provider_upload,
|
|
131
|
+
signedParts: row.signed_parts,
|
|
132
|
+
fileRef: row.file_ref,
|
|
133
|
+
createdAt: row.created_at,
|
|
134
|
+
expiresAt: row.expires_at,
|
|
135
|
+
completedAt: row.completed_at,
|
|
136
|
+
abortedAt: row.aborted_at,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** JSON text for a nullable jsonb column; always bind with the `::text::jsonb` double cast. */
|
|
141
|
+
function jsonText(value: object | undefined): string | null {
|
|
142
|
+
return value === undefined ? null : JSON.stringify(value)
|
|
143
|
+
}
|
|
@@ -5,8 +5,8 @@ import type {
|
|
|
5
5
|
ExpireWorkflowInterventionInput,
|
|
6
6
|
ListWorkflowInterventionsInput,
|
|
7
7
|
ListWorkflowInterventionsResult,
|
|
8
|
+
Principal,
|
|
8
9
|
SubmitWorkflowInterventionInput,
|
|
9
|
-
WorkflowInterventionActor,
|
|
10
10
|
WorkflowInterventionRecord,
|
|
11
11
|
WorkflowInterventionStorage,
|
|
12
12
|
} from "@sixb/core/storage"
|
|
@@ -78,7 +78,7 @@ export class PgWorkflowInterventionStorage implements WorkflowInterventionStorag
|
|
|
78
78
|
SET
|
|
79
79
|
status = ${"submitted"},
|
|
80
80
|
submitted_at = ${input.submittedAt ?? new Date()},
|
|
81
|
-
submitted_by = ${
|
|
81
|
+
submitted_by = ${serializePrincipal(input.submittedBy)}::text::jsonb,
|
|
82
82
|
response = ${serializeRecord(input.response)}::text::jsonb
|
|
83
83
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
84
84
|
RETURNING *
|
|
@@ -97,7 +97,7 @@ export class PgWorkflowInterventionStorage implements WorkflowInterventionStorag
|
|
|
97
97
|
SET
|
|
98
98
|
status = ${"cancelled"},
|
|
99
99
|
cancelled_at = ${input.cancelledAt ?? new Date()},
|
|
100
|
-
cancelled_by = ${
|
|
100
|
+
cancelled_by = ${serializePrincipal(input.cancelledBy)}::text::jsonb
|
|
101
101
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
102
102
|
RETURNING *
|
|
103
103
|
`
|
|
@@ -268,16 +268,12 @@ function parseRecord(value: WorkflowIOSnapshot | string): WorkflowIOSnapshot {
|
|
|
268
268
|
return typeof value === "string" ? (JSON.parse(value) as WorkflowIOSnapshot) : value
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function
|
|
272
|
-
return
|
|
271
|
+
function serializePrincipal(principal: Principal | undefined): string | null {
|
|
272
|
+
return principal ? JSON.stringify(principal) : null
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
function
|
|
276
|
-
value
|
|
277
|
-
): WorkflowInterventionActor | undefined {
|
|
278
|
-
return typeof value === "string"
|
|
279
|
-
? (JSON.parse(value) as WorkflowInterventionActor)
|
|
280
|
-
: (value ?? undefined)
|
|
275
|
+
function parsePrincipal(value: Principal | string | null): Principal | undefined {
|
|
276
|
+
return typeof value === "string" ? (JSON.parse(value) as Principal) : (value ?? undefined)
|
|
281
277
|
}
|
|
282
278
|
|
|
283
279
|
function rowToWorkflowInterventionRecord(
|
|
@@ -299,10 +295,10 @@ function rowToWorkflowInterventionRecord(
|
|
|
299
295
|
requestedAt: new Date(row.requested_at),
|
|
300
296
|
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
301
297
|
submittedAt: row.submitted_at ? new Date(row.submitted_at) : undefined,
|
|
302
|
-
submittedBy:
|
|
298
|
+
submittedBy: parsePrincipal(row.submitted_by),
|
|
303
299
|
response: row.response ? parseRecord(row.response) : undefined,
|
|
304
300
|
cancelledAt: row.cancelled_at ? new Date(row.cancelled_at) : undefined,
|
|
305
|
-
cancelledBy:
|
|
301
|
+
cancelledBy: parsePrincipal(row.cancelled_by),
|
|
306
302
|
expiredAt: row.expired_at ? new Date(row.expired_at) : undefined,
|
|
307
303
|
}
|
|
308
304
|
}
|
|
@@ -331,9 +327,9 @@ interface WorkflowInterventionDatabaseRow {
|
|
|
331
327
|
requested_at: Date | string
|
|
332
328
|
expires_at: Date | string | null
|
|
333
329
|
submitted_at: Date | string | null
|
|
334
|
-
submitted_by:
|
|
330
|
+
submitted_by: Principal | string | null
|
|
335
331
|
response: WorkflowIOSnapshot | string | null
|
|
336
332
|
cancelled_at: Date | string | null
|
|
337
|
-
cancelled_by:
|
|
333
|
+
cancelled_by: Principal | string | null
|
|
338
334
|
expired_at: Date | string | null
|
|
339
335
|
}
|