@sixb/pg 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- package/src/transactions.ts +58 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MaterializationConflictError,
|
|
3
|
+
MaterializationValidationError,
|
|
4
|
+
} from "@sixb/core/internal/materialization"
|
|
5
|
+
import type {
|
|
6
|
+
ClaimedOntologyOutboxRow,
|
|
7
|
+
ClaimOntologyOutboxInput,
|
|
8
|
+
CompleteOntologyOutboxLeaseInput,
|
|
9
|
+
OntologyOutboxStorage,
|
|
10
|
+
OntologyOutboxSummary,
|
|
11
|
+
PurgePublishedOntologyOutboxInput,
|
|
12
|
+
RescheduleOntologyOutboxLeaseInput,
|
|
13
|
+
SummarizeOntologyOutboxInput,
|
|
14
|
+
} from "@sixb/core/storage"
|
|
15
|
+
import {
|
|
16
|
+
assertNonblank,
|
|
17
|
+
assertPositiveInteger,
|
|
18
|
+
assertTimestamp,
|
|
19
|
+
outboxRecord,
|
|
20
|
+
type PgOntologyOutboxRow,
|
|
21
|
+
type PgRootOperation,
|
|
22
|
+
} from "./shared"
|
|
23
|
+
|
|
24
|
+
export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
25
|
+
constructor(private readonly runRootOperation: PgRootOperation) {}
|
|
26
|
+
|
|
27
|
+
async claim(input: ClaimOntologyOutboxInput): Promise<readonly ClaimedOntologyOutboxRow[]> {
|
|
28
|
+
return this.runRootOperation(async (sql) => {
|
|
29
|
+
assertNonblank(input.projectId, "Ontology outbox project id")
|
|
30
|
+
assertPositiveInteger(input.limit, "Ontology outbox limit")
|
|
31
|
+
const now = assertTimestamp(input.now, "Ontology outbox claim time")
|
|
32
|
+
const leaseExpiry = assertTimestamp(input.leaseExpiresAt, "Ontology outbox lease expiry")
|
|
33
|
+
assertNonblank(input.leaseId, "Ontology outbox lease id")
|
|
34
|
+
if (leaseExpiry <= now) {
|
|
35
|
+
throw new MaterializationValidationError(
|
|
36
|
+
"Ontology outbox lease expiry must be later than the claim time."
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rows = await sql<PgOntologyOutboxRow[]>`
|
|
41
|
+
WITH candidates AS (
|
|
42
|
+
SELECT project_id, id
|
|
43
|
+
FROM ontology_outbox
|
|
44
|
+
WHERE project_id = ${input.projectId}
|
|
45
|
+
AND published_at IS NULL
|
|
46
|
+
AND available_at <= ${input.now}
|
|
47
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= ${input.now})
|
|
48
|
+
ORDER BY created_at, commit_id, commit_ordinal
|
|
49
|
+
LIMIT ${input.limit}
|
|
50
|
+
FOR UPDATE SKIP LOCKED
|
|
51
|
+
), claimed AS (
|
|
52
|
+
UPDATE ontology_outbox AS outbox
|
|
53
|
+
SET attempts = outbox.attempts + 1,
|
|
54
|
+
lease_id = ${input.leaseId},
|
|
55
|
+
lease_expires_at = ${input.leaseExpiresAt}
|
|
56
|
+
FROM candidates
|
|
57
|
+
WHERE outbox.project_id = candidates.project_id AND outbox.id = candidates.id
|
|
58
|
+
RETURNING outbox.id AS row_id, outbox.envelope, outbox.available_at,
|
|
59
|
+
outbox.attempts, outbox.lease_id, outbox.lease_expires_at,
|
|
60
|
+
outbox.published_at, outbox.last_error, outbox.created_at,
|
|
61
|
+
outbox.commit_id, outbox.commit_ordinal
|
|
62
|
+
)
|
|
63
|
+
SELECT * FROM claimed ORDER BY created_at, commit_id, commit_ordinal
|
|
64
|
+
`
|
|
65
|
+
return rows.map((row) => {
|
|
66
|
+
const record = outboxRecord(row)
|
|
67
|
+
if (record.leaseId === null || record.leaseExpiresAt === null) {
|
|
68
|
+
throw new MaterializationConflictError(
|
|
69
|
+
"outbox-lease",
|
|
70
|
+
"Ontology outbox claim returned an unpaired lease."
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt }
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async markPublished(input: CompleteOntologyOutboxLeaseInput): Promise<void> {
|
|
79
|
+
await this.runRootOperation(async (sql) => {
|
|
80
|
+
assertLeaseInput(input)
|
|
81
|
+
assertTimestamp(input.publishedAt, "Ontology outbox publishedAt")
|
|
82
|
+
const ids = validateLeaseIds(input.ids)
|
|
83
|
+
if (ids.length === 0) return
|
|
84
|
+
const rows = await sql<{ readonly id: string }[]>`
|
|
85
|
+
WITH leased AS MATERIALIZED (
|
|
86
|
+
SELECT id FROM ontology_outbox
|
|
87
|
+
WHERE project_id = ${input.projectId}
|
|
88
|
+
AND id = ANY(${sql.array(ids)}::text[])
|
|
89
|
+
AND lease_id = ${input.leaseId}
|
|
90
|
+
AND lease_expires_at IS NOT NULL
|
|
91
|
+
FOR UPDATE
|
|
92
|
+
), eligible AS (
|
|
93
|
+
SELECT id FROM leased
|
|
94
|
+
WHERE (SELECT COUNT(*) FROM leased) = ${ids.length}
|
|
95
|
+
)
|
|
96
|
+
UPDATE ontology_outbox AS outbox
|
|
97
|
+
SET published_at = ${input.publishedAt}, lease_id = NULL, lease_expires_at = NULL
|
|
98
|
+
FROM eligible
|
|
99
|
+
WHERE outbox.project_id = ${input.projectId} AND outbox.id = eligible.id
|
|
100
|
+
RETURNING outbox.id
|
|
101
|
+
`
|
|
102
|
+
if (rows.length !== ids.length) throw leaseConflict()
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async reschedule(input: RescheduleOntologyOutboxLeaseInput): Promise<void> {
|
|
107
|
+
await this.runRootOperation(async (sql) => {
|
|
108
|
+
assertLeaseInput(input)
|
|
109
|
+
assertTimestamp(input.availableAt, "Ontology outbox availableAt")
|
|
110
|
+
const ids = validateLeaseIds(input.ids)
|
|
111
|
+
if (ids.length === 0) return
|
|
112
|
+
const rows = await sql<{ readonly id: string }[]>`
|
|
113
|
+
WITH leased AS MATERIALIZED (
|
|
114
|
+
SELECT id FROM ontology_outbox
|
|
115
|
+
WHERE project_id = ${input.projectId}
|
|
116
|
+
AND id = ANY(${sql.array(ids)}::text[])
|
|
117
|
+
AND lease_id = ${input.leaseId}
|
|
118
|
+
AND lease_expires_at IS NOT NULL
|
|
119
|
+
FOR UPDATE
|
|
120
|
+
), eligible AS (
|
|
121
|
+
SELECT id FROM leased
|
|
122
|
+
WHERE (SELECT COUNT(*) FROM leased) = ${ids.length}
|
|
123
|
+
)
|
|
124
|
+
UPDATE ontology_outbox AS outbox
|
|
125
|
+
SET available_at = ${input.availableAt}, last_error = ${input.error},
|
|
126
|
+
lease_id = NULL, lease_expires_at = NULL
|
|
127
|
+
FROM eligible
|
|
128
|
+
WHERE outbox.project_id = ${input.projectId} AND outbox.id = eligible.id
|
|
129
|
+
RETURNING outbox.id
|
|
130
|
+
`
|
|
131
|
+
if (rows.length !== ids.length) throw leaseConflict()
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async purgePublished(input: PurgePublishedOntologyOutboxInput): Promise<number> {
|
|
136
|
+
return this.runRootOperation(async (sql) => {
|
|
137
|
+
assertNonblank(input.projectId, "Ontology outbox project id")
|
|
138
|
+
assertPositiveInteger(input.limit, "Ontology outbox limit")
|
|
139
|
+
assertTimestamp(input.publishedBefore, "Ontology outbox purge cutoff")
|
|
140
|
+
const rows = await sql<{ readonly id: string }[]>`
|
|
141
|
+
WITH selected AS (
|
|
142
|
+
SELECT project_id, id
|
|
143
|
+
FROM ontology_outbox
|
|
144
|
+
WHERE project_id = ${input.projectId}
|
|
145
|
+
AND published_at IS NOT NULL
|
|
146
|
+
AND published_at < ${input.publishedBefore}
|
|
147
|
+
ORDER BY published_at, id
|
|
148
|
+
LIMIT ${input.limit}
|
|
149
|
+
FOR UPDATE SKIP LOCKED
|
|
150
|
+
)
|
|
151
|
+
DELETE FROM ontology_outbox AS outbox
|
|
152
|
+
USING selected
|
|
153
|
+
WHERE outbox.project_id = selected.project_id AND outbox.id = selected.id
|
|
154
|
+
RETURNING outbox.id
|
|
155
|
+
`
|
|
156
|
+
return rows.length
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async summarize(input: SummarizeOntologyOutboxInput): Promise<OntologyOutboxSummary> {
|
|
161
|
+
return this.runRootOperation(async (sql) => {
|
|
162
|
+
assertNonblank(input.projectId, "Ontology outbox project id")
|
|
163
|
+
const [summary] = await sql<
|
|
164
|
+
{
|
|
165
|
+
readonly pending_count: number
|
|
166
|
+
readonly oldest_pending_at: Date | string | null
|
|
167
|
+
readonly retrying_count: number
|
|
168
|
+
readonly max_attempts: number
|
|
169
|
+
}[]
|
|
170
|
+
>`
|
|
171
|
+
SELECT COUNT(*)::int AS pending_count,
|
|
172
|
+
MIN(created_at) AS oldest_pending_at,
|
|
173
|
+
COUNT(*) FILTER (WHERE attempts > 0)::int AS retrying_count,
|
|
174
|
+
COALESCE(MAX(attempts), 0)::int AS max_attempts
|
|
175
|
+
FROM ontology_outbox
|
|
176
|
+
WHERE project_id = ${input.projectId} AND published_at IS NULL
|
|
177
|
+
`
|
|
178
|
+
return {
|
|
179
|
+
pendingCount: summary?.pending_count ?? 0,
|
|
180
|
+
oldestPendingAt: toOptionalIsoString(summary?.oldest_pending_at),
|
|
181
|
+
retryingCount: summary?.retrying_count ?? 0,
|
|
182
|
+
maxAttempts: summary?.max_attempts ?? 0,
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toOptionalIsoString(value: Date | string | null | undefined): string | null {
|
|
189
|
+
if (value === null || value === undefined) return null
|
|
190
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function validateLeaseIds(ids: readonly string[]): string[] {
|
|
194
|
+
const seen = new Set<string>()
|
|
195
|
+
for (const id of ids) {
|
|
196
|
+
assertNonblank(id, "Ontology outbox event id")
|
|
197
|
+
if (seen.has(id)) {
|
|
198
|
+
throw new MaterializationValidationError(`Ontology outbox lease batch repeats event '${id}'.`)
|
|
199
|
+
}
|
|
200
|
+
seen.add(id)
|
|
201
|
+
}
|
|
202
|
+
return [...seen]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function assertLeaseInput(input: {
|
|
206
|
+
readonly projectId: string
|
|
207
|
+
readonly ids: readonly string[]
|
|
208
|
+
readonly leaseId: string
|
|
209
|
+
}): void {
|
|
210
|
+
assertNonblank(input.projectId, "Ontology outbox project id")
|
|
211
|
+
assertNonblank(input.leaseId, "Ontology outbox lease id")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function leaseConflict(): MaterializationConflictError {
|
|
215
|
+
return new MaterializationConflictError("outbox-lease", "Ontology outbox lease does not match.")
|
|
216
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
|
|
2
|
+
import { MaterializationConflictError } from "@sixb/core/internal/materialization"
|
|
3
|
+
import type {
|
|
4
|
+
AssertSourceMaterializationExecutionInput,
|
|
5
|
+
OntologyCommitRecord,
|
|
6
|
+
OntologyCommitWrite,
|
|
7
|
+
OntologyOutboxRecord,
|
|
8
|
+
OntologySourceRecord,
|
|
9
|
+
StageSourceAssertion,
|
|
10
|
+
} from "@sixb/core/storage"
|
|
11
|
+
import type { SQLClient } from "../pg-client"
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
assertNonblank,
|
|
15
|
+
assertNonnegativeInteger,
|
|
16
|
+
assertPositiveInteger,
|
|
17
|
+
assertTimestamp,
|
|
18
|
+
canonicalJson,
|
|
19
|
+
linkRefFromColumns,
|
|
20
|
+
objectRefFromColumns,
|
|
21
|
+
originColumns,
|
|
22
|
+
originWhere,
|
|
23
|
+
sourceEntityKey,
|
|
24
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
25
|
+
|
|
26
|
+
export type PgRootOperation = <T>(run: (sql: SQLClient) => Promise<T>) => Promise<T>
|
|
27
|
+
|
|
28
|
+
type PgJsonValue = Parameters<SQLClient["json"]>[0]
|
|
29
|
+
|
|
30
|
+
export interface PgOntologyCommitRow {
|
|
31
|
+
readonly project_id: string
|
|
32
|
+
readonly id: string
|
|
33
|
+
readonly idempotency_key: string
|
|
34
|
+
readonly request_hash: string
|
|
35
|
+
readonly origin_kind: string
|
|
36
|
+
readonly origin_run_id: string | null
|
|
37
|
+
readonly origin_batch_ordinal: number | string | null
|
|
38
|
+
readonly origin: unknown
|
|
39
|
+
readonly actor: unknown | null
|
|
40
|
+
readonly ontology_revision: string
|
|
41
|
+
readonly projection_revision: string | null
|
|
42
|
+
readonly ownership_hash: string | null
|
|
43
|
+
readonly intent: unknown
|
|
44
|
+
readonly result: unknown
|
|
45
|
+
readonly committed_at: Date | string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PgOntologySourceRow {
|
|
49
|
+
readonly project_id: string
|
|
50
|
+
readonly source_id: string
|
|
51
|
+
readonly materialization_id: string
|
|
52
|
+
readonly projection_run_id: string
|
|
53
|
+
readonly projection_kind: "object" | "link"
|
|
54
|
+
readonly protocol: "replacement"
|
|
55
|
+
readonly status: OntologySourceRecord["status"]
|
|
56
|
+
readonly execution_token: string | null
|
|
57
|
+
readonly dataset_id: string
|
|
58
|
+
readonly dataset_version_id: string
|
|
59
|
+
readonly dataset_version_created_at: Date | string
|
|
60
|
+
readonly projection_revision: string
|
|
61
|
+
readonly ownership_hash: string
|
|
62
|
+
readonly ontology_revision: string
|
|
63
|
+
readonly root_count: number | string | null
|
|
64
|
+
readonly assertion_count: number | string | null
|
|
65
|
+
readonly created_at: Date | string
|
|
66
|
+
readonly ready_at: Date | string | null
|
|
67
|
+
readonly activated_at: Date | string | null
|
|
68
|
+
readonly terminal_at: Date | string | null
|
|
69
|
+
readonly last_commit_id: string | null
|
|
70
|
+
readonly updated_at: Date | string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PgOntologySourceAssertionRow {
|
|
74
|
+
readonly project_id: string
|
|
75
|
+
readonly source_id: string
|
|
76
|
+
readonly materialization_id: string
|
|
77
|
+
readonly entity_kind: "object" | "link"
|
|
78
|
+
readonly entity_key: unknown
|
|
79
|
+
readonly entity_sort_key: string
|
|
80
|
+
readonly root_kind: "object" | "link"
|
|
81
|
+
readonly root_key: unknown
|
|
82
|
+
readonly root_sort_key: string
|
|
83
|
+
readonly staging_ordinal: number | string
|
|
84
|
+
readonly root: unknown
|
|
85
|
+
readonly assertion: unknown
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PgOntologyOverrideRow {
|
|
89
|
+
readonly entity_kind: "object" | "link"
|
|
90
|
+
readonly value: unknown
|
|
91
|
+
readonly last_commit_id: string
|
|
92
|
+
readonly updated_at: Date | string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PgOntologyOutboxRow {
|
|
96
|
+
readonly envelope: unknown
|
|
97
|
+
readonly available_at: Date | string
|
|
98
|
+
readonly attempts: number | string
|
|
99
|
+
readonly lease_id: string | null
|
|
100
|
+
readonly lease_expires_at: Date | string | null
|
|
101
|
+
readonly published_at: Date | string | null
|
|
102
|
+
readonly last_error: string | null
|
|
103
|
+
readonly created_at: Date | string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function jsonParameter(sql: SQLClient, value: unknown): ReturnType<SQLClient["json"]> {
|
|
107
|
+
return sql.json(value as PgJsonValue)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function jsonKeyParameter(
|
|
111
|
+
sql: SQLClient,
|
|
112
|
+
canonicalKey: string
|
|
113
|
+
): ReturnType<SQLClient["json"]> {
|
|
114
|
+
return jsonParameter(sql, JSON.parse(canonicalKey))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function toIsoString(value: Date | string): string {
|
|
118
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function optionalIsoString(value: Date | string | null): string | null {
|
|
122
|
+
return value === null ? null : toIsoString(value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function databaseSafeInteger(value: number | string, label: string): number {
|
|
126
|
+
const result = Number(value)
|
|
127
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
128
|
+
throw new MaterializationConflictError(
|
|
129
|
+
"effective-state",
|
|
130
|
+
`${label} is outside the nonnegative safe-integer range.`
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sourceRecord(row: PgOntologySourceRow): OntologySourceRecord {
|
|
137
|
+
return {
|
|
138
|
+
projectId: row.project_id,
|
|
139
|
+
source: { projectionId: row.source_id },
|
|
140
|
+
materializationId: row.materialization_id,
|
|
141
|
+
projectionRunId: row.projection_run_id,
|
|
142
|
+
projectionKind: row.projection_kind,
|
|
143
|
+
protocol: row.protocol,
|
|
144
|
+
status: row.status,
|
|
145
|
+
executionToken: row.execution_token,
|
|
146
|
+
datasetVersion: {
|
|
147
|
+
datasetId: row.dataset_id,
|
|
148
|
+
versionId: row.dataset_version_id,
|
|
149
|
+
createdAt: toIsoString(row.dataset_version_created_at),
|
|
150
|
+
},
|
|
151
|
+
projectionRevision: row.projection_revision,
|
|
152
|
+
ownershipHash: row.ownership_hash,
|
|
153
|
+
ontologyRevision: row.ontology_revision,
|
|
154
|
+
rootCount:
|
|
155
|
+
row.root_count === null ? null : databaseSafeInteger(row.root_count, "Source root count"),
|
|
156
|
+
assertionCount:
|
|
157
|
+
row.assertion_count === null
|
|
158
|
+
? null
|
|
159
|
+
: databaseSafeInteger(row.assertion_count, "Source assertion count"),
|
|
160
|
+
createdAt: toIsoString(row.created_at),
|
|
161
|
+
readyAt: optionalIsoString(row.ready_at),
|
|
162
|
+
activatedAt: optionalIsoString(row.activated_at),
|
|
163
|
+
terminalAt: optionalIsoString(row.terminal_at),
|
|
164
|
+
lastCommitId: row.last_commit_id,
|
|
165
|
+
updatedAt: toIsoString(row.updated_at),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function sourceAssertion(row: PgOntologySourceAssertionRow): StageSourceAssertion {
|
|
170
|
+
return {
|
|
171
|
+
root: structuredClone(row.root) as ProjectionEntityRef,
|
|
172
|
+
assertion: structuredClone(row.assertion) as StageSourceAssertion["assertion"],
|
|
173
|
+
stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal"),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function commitRecord(row: PgOntologyCommitRow): OntologyCommitRecord {
|
|
178
|
+
const base = {
|
|
179
|
+
projectId: row.project_id,
|
|
180
|
+
id: row.id,
|
|
181
|
+
idempotencyKey: row.idempotency_key,
|
|
182
|
+
requestHash: row.request_hash,
|
|
183
|
+
origin: structuredClone(row.origin) as OntologyCommitWrite["origin"],
|
|
184
|
+
...(row.actor === null
|
|
185
|
+
? {}
|
|
186
|
+
: {
|
|
187
|
+
actor: structuredClone(row.actor) as NonNullable<OntologyCommitWrite["actor"]>,
|
|
188
|
+
}),
|
|
189
|
+
ontologyRevision: row.ontology_revision,
|
|
190
|
+
...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
|
|
191
|
+
...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
|
|
192
|
+
committedAt: toIsoString(row.committed_at),
|
|
193
|
+
}
|
|
194
|
+
const intent = structuredClone(row.intent) as OntologyCommitWrite["intent"]
|
|
195
|
+
const result = structuredClone(row.result) as OntologyCommitRecord["result"]
|
|
196
|
+
return { ...base, intent, result } as OntologyCommitRecord
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function outboxRecord(row: PgOntologyOutboxRow): OntologyOutboxRecord {
|
|
200
|
+
return {
|
|
201
|
+
envelope: structuredClone(row.envelope) as OntologyOutboxRecord["envelope"],
|
|
202
|
+
availableAt: toIsoString(row.available_at),
|
|
203
|
+
attempts: databaseSafeInteger(row.attempts, "Ontology outbox attempts"),
|
|
204
|
+
leaseId: row.lease_id,
|
|
205
|
+
leaseExpiresAt: optionalIsoString(row.lease_expires_at),
|
|
206
|
+
publishedAt: optionalIsoString(row.published_at),
|
|
207
|
+
lastError: row.last_error,
|
|
208
|
+
createdAt: toIsoString(row.created_at),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function assertProjectionExecution(
|
|
213
|
+
sql: SQLClient,
|
|
214
|
+
input: {
|
|
215
|
+
readonly projectId: string
|
|
216
|
+
readonly sourceId: string
|
|
217
|
+
readonly projectionRunId: string
|
|
218
|
+
readonly executionToken: string
|
|
219
|
+
readonly identity?: AssertSourceMaterializationExecutionInput["identity"]
|
|
220
|
+
}
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const [run] = await sql<
|
|
223
|
+
{
|
|
224
|
+
readonly projection_id: string
|
|
225
|
+
readonly projection_kind: string
|
|
226
|
+
readonly status: string
|
|
227
|
+
readonly materialization_protocol: string | null
|
|
228
|
+
readonly execution_token: string | null
|
|
229
|
+
readonly dataset_id: string
|
|
230
|
+
readonly dataset_version_id: string
|
|
231
|
+
readonly dataset_version_created_at: Date | string | null
|
|
232
|
+
readonly ontology_revision: string | null
|
|
233
|
+
readonly projection_revision: string | null
|
|
234
|
+
readonly ownership_hash: string | null
|
|
235
|
+
}[]
|
|
236
|
+
>`
|
|
237
|
+
SELECT projection_id, projection_kind, status, materialization_protocol, execution_token,
|
|
238
|
+
dataset_id, dataset_version_id, dataset_version_created_at,
|
|
239
|
+
ontology_revision, projection_revision, ownership_hash
|
|
240
|
+
FROM projection_runs
|
|
241
|
+
WHERE project_id = ${input.projectId} AND id = ${input.projectionRunId}
|
|
242
|
+
FOR UPDATE
|
|
243
|
+
`
|
|
244
|
+
if (!run || run.status !== "running") {
|
|
245
|
+
throw new MaterializationConflictError(
|
|
246
|
+
"run-correlation",
|
|
247
|
+
`Projection run '${input.projectionRunId}' is missing or is not running.`
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
if (run.projection_id !== input.sourceId || run.materialization_protocol !== "replacement") {
|
|
251
|
+
throw new MaterializationConflictError(
|
|
252
|
+
"run-correlation",
|
|
253
|
+
`Projection run '${input.projectionRunId}' does not own replacement source '${input.sourceId}'.`
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
if (run.execution_token !== input.executionToken) {
|
|
257
|
+
throw new MaterializationConflictError(
|
|
258
|
+
"execution-lost",
|
|
259
|
+
`Projection run '${input.projectionRunId}' execution token is stale.`
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
const identity = input.identity
|
|
263
|
+
if (
|
|
264
|
+
identity &&
|
|
265
|
+
(run.projection_kind !== identity.projectionKind ||
|
|
266
|
+
run.materialization_protocol !== identity.protocol ||
|
|
267
|
+
run.dataset_id !== identity.datasetVersion.datasetId ||
|
|
268
|
+
run.dataset_version_id !== identity.datasetVersion.versionId ||
|
|
269
|
+
run.dataset_version_created_at === null ||
|
|
270
|
+
toIsoString(run.dataset_version_created_at) !== identity.datasetVersion.createdAt ||
|
|
271
|
+
run.ontology_revision !== identity.ontologyRevision ||
|
|
272
|
+
run.projection_revision !== identity.projectionRevision ||
|
|
273
|
+
run.ownership_hash !== identity.ownershipHash)
|
|
274
|
+
) {
|
|
275
|
+
throw new MaterializationConflictError(
|
|
276
|
+
"run-correlation",
|
|
277
|
+
`Projection run '${input.projectionRunId}' immutable source identity does not match.`
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function requireRow<T>(
|
|
283
|
+
row: T | undefined,
|
|
284
|
+
kind: ConstructorParameters<typeof MaterializationConflictError>[0],
|
|
285
|
+
message: string
|
|
286
|
+
): T {
|
|
287
|
+
if (!row) throw new MaterializationConflictError(kind, message)
|
|
288
|
+
return row
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function ontologyLockKey(kind: string, ...parts: readonly string[]): string {
|
|
292
|
+
return `ontology:${kind}:${JSON.stringify(parts)}`
|
|
293
|
+
}
|