@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,308 @@
|
|
|
1
|
+
import type { RuleEventSubject } from "@sixb/core"
|
|
2
|
+
import type {
|
|
3
|
+
ListActiveRuleStatesInput,
|
|
4
|
+
ListActiveRuleStatesResult,
|
|
5
|
+
ListRuleStatesReconciliationPageInput,
|
|
6
|
+
ListRuleStatesReconciliationPageResult,
|
|
7
|
+
RuleStateRecord,
|
|
8
|
+
RuleStateTransitionEvent,
|
|
9
|
+
RulesStorage,
|
|
10
|
+
StoredRuleResolvedEvent,
|
|
11
|
+
StoredRuleTriggeredEvent,
|
|
12
|
+
} from "@sixb/core/storage"
|
|
13
|
+
import type { SqlParameter } from "./pg-client"
|
|
14
|
+
import type { PgStoreClient } from "./transactions"
|
|
15
|
+
|
|
16
|
+
export class PgRulesStorage implements RulesStorage {
|
|
17
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
18
|
+
|
|
19
|
+
async getActive(params: {
|
|
20
|
+
projectId: string
|
|
21
|
+
ruleId: string
|
|
22
|
+
subject: RuleEventSubject
|
|
23
|
+
}): Promise<RuleStateRecord | null> {
|
|
24
|
+
const [row] = await this.sql<RuleStateRow[]>`
|
|
25
|
+
SELECT
|
|
26
|
+
project_id,
|
|
27
|
+
rule_id,
|
|
28
|
+
subject_kind,
|
|
29
|
+
object_type_id,
|
|
30
|
+
primary_id,
|
|
31
|
+
triggered_at
|
|
32
|
+
FROM rule_states
|
|
33
|
+
WHERE project_id = ${params.projectId}
|
|
34
|
+
AND rule_id = ${params.ruleId}
|
|
35
|
+
AND subject_kind = ${params.subject.kind}
|
|
36
|
+
AND object_type_id = ${params.subject.objectTypeId}
|
|
37
|
+
AND primary_id = ${params.subject.primaryId}
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
return row ? rowToRuleStateRecord(row) : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getActiveBatch(params: {
|
|
44
|
+
projectId: string
|
|
45
|
+
items: readonly {
|
|
46
|
+
readonly ruleId: string
|
|
47
|
+
readonly subject: RuleEventSubject
|
|
48
|
+
}[]
|
|
49
|
+
}): Promise<readonly RuleStateRecord[]> {
|
|
50
|
+
if (params.items.length === 0) return []
|
|
51
|
+
const requested = JSON.stringify(
|
|
52
|
+
params.items.map((item) => ({
|
|
53
|
+
rule_id: item.ruleId,
|
|
54
|
+
subject_kind: item.subject.kind,
|
|
55
|
+
object_type_id: item.subject.objectTypeId,
|
|
56
|
+
primary_id: item.subject.primaryId,
|
|
57
|
+
}))
|
|
58
|
+
)
|
|
59
|
+
const rows = await this.sql<RuleStateRow[]>`
|
|
60
|
+
WITH requested AS (
|
|
61
|
+
SELECT *
|
|
62
|
+
FROM jsonb_to_recordset(${requested}::text::jsonb) AS identity(
|
|
63
|
+
rule_id text,
|
|
64
|
+
subject_kind text,
|
|
65
|
+
object_type_id text,
|
|
66
|
+
primary_id text
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
SELECT project_id, rule_id, subject_kind, object_type_id, primary_id, triggered_at
|
|
70
|
+
FROM rule_states AS states
|
|
71
|
+
JOIN requested USING (rule_id, subject_kind, object_type_id, primary_id)
|
|
72
|
+
WHERE states.project_id = ${params.projectId}
|
|
73
|
+
`
|
|
74
|
+
return rows.map(rowToRuleStateRecord)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async listActive(input: ListActiveRuleStatesInput): Promise<ListActiveRuleStatesResult> {
|
|
78
|
+
if (input.objectTypeIds?.length === 0) {
|
|
79
|
+
return { states: [], hasMore: false, total: 0 }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const whereClauses = ["project_id = $1"]
|
|
83
|
+
const params: SqlParameter[] = [input.projectId]
|
|
84
|
+
let index = 2
|
|
85
|
+
|
|
86
|
+
if (input.ruleId) {
|
|
87
|
+
whereClauses.push(`rule_id = $${index++}`)
|
|
88
|
+
params.push(input.ruleId)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (input.objectTypeId) {
|
|
92
|
+
whereClauses.push(`object_type_id = $${index++}`)
|
|
93
|
+
params.push(input.objectTypeId)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (input.objectTypeIds) {
|
|
97
|
+
const placeholders = input.objectTypeIds.map(() => `$${index++}`)
|
|
98
|
+
whereClauses.push(`object_type_id IN (${placeholders.join(", ")})`)
|
|
99
|
+
params.push(...input.objectTypeIds)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (input.primaryId) {
|
|
103
|
+
whereClauses.push(`primary_id = $${index++}`)
|
|
104
|
+
params.push(input.primaryId)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const where = `WHERE ${whereClauses.join(" AND ")}`
|
|
108
|
+
const order = input.order === "asc" ? "ASC" : "DESC"
|
|
109
|
+
const offset = input.offset ?? 0
|
|
110
|
+
|
|
111
|
+
const [totalRow] = await this.sql.unsafe<{ count: string | number }[]>(
|
|
112
|
+
`SELECT COUNT(*)::bigint AS count FROM rule_states ${where}`,
|
|
113
|
+
params
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const queryParams = [...params]
|
|
117
|
+
let query = `
|
|
118
|
+
SELECT
|
|
119
|
+
project_id,
|
|
120
|
+
rule_id,
|
|
121
|
+
subject_kind,
|
|
122
|
+
object_type_id,
|
|
123
|
+
primary_id,
|
|
124
|
+
triggered_at
|
|
125
|
+
FROM rule_states
|
|
126
|
+
${where}
|
|
127
|
+
ORDER BY triggered_at ${order}, rule_id ${order}, object_type_id ${order}, primary_id ${order}
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
if (input.limit !== undefined) {
|
|
131
|
+
query += ` LIMIT $${index++} OFFSET $${index++}`
|
|
132
|
+
queryParams.push(input.limit, offset)
|
|
133
|
+
} else if (offset > 0) {
|
|
134
|
+
query += ` OFFSET $${index++}`
|
|
135
|
+
queryParams.push(offset)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rows = await this.sql.unsafe<RuleStateRow[]>(query, queryParams)
|
|
139
|
+
const total = Number(totalRow?.count ?? 0)
|
|
140
|
+
const states = rows.map(rowToRuleStateRecord)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
states,
|
|
144
|
+
hasMore: offset + states.length < total,
|
|
145
|
+
total,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async listReconciliationPage(
|
|
150
|
+
input: ListRuleStatesReconciliationPageInput
|
|
151
|
+
): Promise<ListRuleStatesReconciliationPageResult> {
|
|
152
|
+
assertPositiveLimit(input.limit)
|
|
153
|
+
const params: SqlParameter[] = [input.projectId]
|
|
154
|
+
const cursor = input.after ? "AND (rule_id, object_type_id, primary_id) > ($2, $3, $4)" : ""
|
|
155
|
+
if (input.after) {
|
|
156
|
+
params.push(input.after.ruleId, input.after.objectTypeId, input.after.primaryId)
|
|
157
|
+
}
|
|
158
|
+
params.push(input.limit + 1)
|
|
159
|
+
const limitParameter = `$${params.length}`
|
|
160
|
+
const rows = await this.sql.unsafe<RuleStateRow[]>(
|
|
161
|
+
`
|
|
162
|
+
SELECT project_id, rule_id, subject_kind, object_type_id, primary_id, triggered_at
|
|
163
|
+
FROM rule_states
|
|
164
|
+
WHERE project_id = $1 AND subject_kind = 'object' ${cursor}
|
|
165
|
+
ORDER BY rule_id ASC, object_type_id ASC, primary_id ASC
|
|
166
|
+
LIMIT ${limitParameter}
|
|
167
|
+
`,
|
|
168
|
+
params
|
|
169
|
+
)
|
|
170
|
+
return reconciliationPage(rows, input.limit)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async applyTriggered(event: StoredRuleTriggeredEvent): Promise<void> {
|
|
174
|
+
await this.sql`
|
|
175
|
+
INSERT INTO rule_states (
|
|
176
|
+
project_id,
|
|
177
|
+
rule_id,
|
|
178
|
+
subject_kind,
|
|
179
|
+
object_type_id,
|
|
180
|
+
primary_id,
|
|
181
|
+
triggered_at
|
|
182
|
+
) VALUES (
|
|
183
|
+
${event.projectId},
|
|
184
|
+
${event.payload.ruleId},
|
|
185
|
+
${event.payload.subject.kind},
|
|
186
|
+
${event.payload.subject.objectTypeId},
|
|
187
|
+
${event.payload.subject.primaryId},
|
|
188
|
+
${event.payload.triggeredAt}
|
|
189
|
+
)
|
|
190
|
+
ON CONFLICT (project_id, rule_id, subject_kind, object_type_id, primary_id)
|
|
191
|
+
DO UPDATE SET triggered_at = excluded.triggered_at
|
|
192
|
+
`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async applyResolved(event: StoredRuleResolvedEvent): Promise<void> {
|
|
196
|
+
await this.sql`
|
|
197
|
+
DELETE FROM rule_states
|
|
198
|
+
WHERE project_id = ${event.projectId}
|
|
199
|
+
AND rule_id = ${event.payload.ruleId}
|
|
200
|
+
AND subject_kind = ${event.payload.subject.kind}
|
|
201
|
+
AND object_type_id = ${event.payload.subject.objectTypeId}
|
|
202
|
+
AND primary_id = ${event.payload.subject.primaryId}
|
|
203
|
+
`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async applyTransitions(events: readonly RuleStateTransitionEvent[]): Promise<void> {
|
|
207
|
+
if (events.length === 0) return
|
|
208
|
+
const rows = JSON.stringify(events.map(transitionRow))
|
|
209
|
+
await this.sql`
|
|
210
|
+
WITH transitions AS MATERIALIZED (
|
|
211
|
+
SELECT *
|
|
212
|
+
FROM jsonb_to_recordset(${rows}::text::jsonb) AS transition(
|
|
213
|
+
type text,
|
|
214
|
+
project_id text,
|
|
215
|
+
rule_id text,
|
|
216
|
+
subject_kind text,
|
|
217
|
+
object_type_id text,
|
|
218
|
+
primary_id text,
|
|
219
|
+
transition_at timestamptz
|
|
220
|
+
)
|
|
221
|
+
), upserted AS (
|
|
222
|
+
INSERT INTO rule_states (
|
|
223
|
+
project_id, rule_id, subject_kind, object_type_id, primary_id, triggered_at
|
|
224
|
+
)
|
|
225
|
+
SELECT project_id, rule_id, subject_kind, object_type_id, primary_id, transition_at
|
|
226
|
+
FROM transitions
|
|
227
|
+
WHERE type = 'rule.triggered'
|
|
228
|
+
ON CONFLICT (project_id, rule_id, subject_kind, object_type_id, primary_id)
|
|
229
|
+
DO UPDATE SET triggered_at = excluded.triggered_at
|
|
230
|
+
RETURNING 1
|
|
231
|
+
)
|
|
232
|
+
DELETE FROM rule_states AS states
|
|
233
|
+
USING transitions
|
|
234
|
+
WHERE transitions.type = 'rule.resolved'
|
|
235
|
+
AND states.project_id = transitions.project_id
|
|
236
|
+
AND states.rule_id = transitions.rule_id
|
|
237
|
+
AND states.subject_kind = transitions.subject_kind
|
|
238
|
+
AND states.object_type_id = transitions.object_type_id
|
|
239
|
+
AND states.primary_id = transitions.primary_id
|
|
240
|
+
`
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function transitionRow(event: RuleStateTransitionEvent) {
|
|
245
|
+
return {
|
|
246
|
+
type: event.type,
|
|
247
|
+
project_id: event.projectId,
|
|
248
|
+
rule_id: event.payload.ruleId,
|
|
249
|
+
subject_kind: event.payload.subject.kind,
|
|
250
|
+
object_type_id: event.payload.subject.objectTypeId,
|
|
251
|
+
primary_id: event.payload.subject.primaryId,
|
|
252
|
+
transition_at:
|
|
253
|
+
event.type === "rule.triggered" ? event.payload.triggeredAt : event.payload.resolvedAt,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function reconciliationPage(
|
|
258
|
+
rows: readonly RuleStateRow[],
|
|
259
|
+
limit: number
|
|
260
|
+
): ListRuleStatesReconciliationPageResult {
|
|
261
|
+
const hasMore = rows.length > limit
|
|
262
|
+
const states = rows.slice(0, limit).map(rowToRuleStateRecord)
|
|
263
|
+
const last = states.at(-1)
|
|
264
|
+
return {
|
|
265
|
+
states,
|
|
266
|
+
...(hasMore && last
|
|
267
|
+
? {
|
|
268
|
+
next: {
|
|
269
|
+
ruleId: last.ruleId,
|
|
270
|
+
objectTypeId: last.subject.objectTypeId,
|
|
271
|
+
primaryId: last.subject.primaryId,
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
: {}),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function assertPositiveLimit(limit: number): void {
|
|
279
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
280
|
+
throw new Error("Rules reconciliation page limit must be a positive safe integer.")
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface RuleStateRow {
|
|
285
|
+
readonly project_id: string
|
|
286
|
+
readonly rule_id: string
|
|
287
|
+
readonly subject_kind: RuleEventSubject["kind"]
|
|
288
|
+
readonly object_type_id: string
|
|
289
|
+
readonly primary_id: string
|
|
290
|
+
readonly triggered_at: Date | string
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function rowToRuleStateRecord(row: RuleStateRow): RuleStateRecord {
|
|
294
|
+
return {
|
|
295
|
+
projectId: row.project_id,
|
|
296
|
+
ruleId: row.rule_id,
|
|
297
|
+
subject: {
|
|
298
|
+
kind: row.subject_kind,
|
|
299
|
+
objectTypeId: row.object_type_id,
|
|
300
|
+
primaryId: row.primary_id,
|
|
301
|
+
},
|
|
302
|
+
triggeredAt: toIsoString(row.triggered_at),
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function toIsoString(value: Date | string): string {
|
|
307
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
|
|
308
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { JsonValue } from "@sixb/core"
|
|
2
|
+
import type {
|
|
3
|
+
FinishSyncRunInput,
|
|
4
|
+
ListLatestSyncRunsInput,
|
|
5
|
+
ListLatestSyncRunsResult,
|
|
6
|
+
ListSyncRunsInput,
|
|
7
|
+
ListSyncRunsResult,
|
|
8
|
+
StartSyncRunInput,
|
|
9
|
+
SyncRunFailure,
|
|
10
|
+
SyncRunRecord,
|
|
11
|
+
SyncRunStorage,
|
|
12
|
+
} from "@sixb/core/storage"
|
|
13
|
+
import { SyncRunError } from "@sixb/core/storage"
|
|
14
|
+
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
15
|
+
import type { SqlParameter } from "./pg-client"
|
|
16
|
+
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
|
|
17
|
+
import { isUniqueViolation } from "./storage-errors"
|
|
18
|
+
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
19
|
+
|
|
20
|
+
export class PgSyncRunStorage implements SyncRunStorage {
|
|
21
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
22
|
+
|
|
23
|
+
async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
|
|
24
|
+
try {
|
|
25
|
+
const [row] = await this.sql<DatabaseRow[]>`
|
|
26
|
+
INSERT INTO sync_runs (
|
|
27
|
+
project_id,
|
|
28
|
+
id,
|
|
29
|
+
sync_id,
|
|
30
|
+
dataset_id,
|
|
31
|
+
mode,
|
|
32
|
+
status,
|
|
33
|
+
started_at,
|
|
34
|
+
expected_latest_version_id,
|
|
35
|
+
commit_message
|
|
36
|
+
) VALUES (
|
|
37
|
+
${input.projectId},
|
|
38
|
+
${input.id},
|
|
39
|
+
${input.syncId},
|
|
40
|
+
${input.datasetId},
|
|
41
|
+
${input.mode},
|
|
42
|
+
${"running"},
|
|
43
|
+
${input.startedAt ?? new Date()},
|
|
44
|
+
${input.expectedLatestVersionId ?? null},
|
|
45
|
+
${input.commitMessage ?? null}
|
|
46
|
+
)
|
|
47
|
+
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
return rowToSyncRunRecord(row)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isUniqueViolation(error)) {
|
|
53
|
+
throw new SyncRunError(
|
|
54
|
+
`[SixbPg] Sync run '${input.id}' already exists for project '${input.projectId}'.`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async finish(input: FinishSyncRunInput): Promise<SyncRunRecord> {
|
|
63
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
64
|
+
const [existing] = await tx<DatabaseRow[]>`
|
|
65
|
+
SELECT *, checkpoint IS NOT NULL AS checkpoint_present FROM sync_runs
|
|
66
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
67
|
+
`
|
|
68
|
+
|
|
69
|
+
if (!existing) {
|
|
70
|
+
throw new SyncRunError(
|
|
71
|
+
`[SixbPg] Sync run '${input.id}' not found for project '${input.projectId}'.`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (input.status === "succeeded") {
|
|
76
|
+
if (input.output && input.output.datasetId !== existing.dataset_id) {
|
|
77
|
+
throw new SyncRunError(
|
|
78
|
+
`[SixbPg] Sync run '${input.id}' output dataset '${input.output.datasetId}' does not match '${existing.dataset_id}'.`
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if (!input.output && (existing.mode !== "append" || input.rowsRead !== 0)) {
|
|
82
|
+
throw new SyncRunError(
|
|
83
|
+
`[SixbPg] Sync run '${input.id}' may omit its output only for an empty append.`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [updated] =
|
|
89
|
+
input.status === "succeeded"
|
|
90
|
+
? await tx<DatabaseRow[]>`
|
|
91
|
+
UPDATE sync_runs
|
|
92
|
+
SET
|
|
93
|
+
status = ${input.status},
|
|
94
|
+
finished_at = ${input.finishedAt ?? new Date()},
|
|
95
|
+
rows_read = ${input.rowsRead},
|
|
96
|
+
output_version_id = ${input.output?.versionId ?? null},
|
|
97
|
+
error_name = ${null},
|
|
98
|
+
error_message = ${null},
|
|
99
|
+
checkpoint = ${serializeCheckpoint(input.checkpoint)}::text::jsonb
|
|
100
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
101
|
+
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
102
|
+
`
|
|
103
|
+
: await tx<DatabaseRow[]>`
|
|
104
|
+
UPDATE sync_runs
|
|
105
|
+
SET
|
|
106
|
+
status = ${input.status},
|
|
107
|
+
finished_at = ${input.finishedAt ?? new Date()},
|
|
108
|
+
rows_read = ${input.rowsRead ?? existing.rows_read ?? null},
|
|
109
|
+
output_version_id = ${null},
|
|
110
|
+
error_name = ${input.error?.name ?? null},
|
|
111
|
+
error_message = ${input.error?.message ?? null},
|
|
112
|
+
checkpoint = ${null}
|
|
113
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
114
|
+
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
return rowToSyncRunRecord(updated)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getById(params: { projectId: string; id: string }): Promise<SyncRunRecord | null> {
|
|
122
|
+
const [row] = await this.sql<DatabaseRow[]>`
|
|
123
|
+
SELECT *, checkpoint IS NOT NULL AS checkpoint_present FROM sync_runs
|
|
124
|
+
WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
return row ? rowToSyncRunRecord(row) : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async list(input: ListSyncRunsInput): Promise<ListSyncRunsResult> {
|
|
131
|
+
if (hasEmptyStatuses(input) || input.syncIds?.length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
runs: [],
|
|
134
|
+
hasMore: false,
|
|
135
|
+
total: 0,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const whereClauses = ["project_id = $1"]
|
|
140
|
+
const params: SqlParameter[] = [input.projectId]
|
|
141
|
+
let index = 2
|
|
142
|
+
|
|
143
|
+
if (input.syncId) {
|
|
144
|
+
whereClauses.push(`sync_id = $${index++}`)
|
|
145
|
+
params.push(input.syncId)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (input.syncIds) {
|
|
149
|
+
const placeholders = input.syncIds.map(() => `$${index++}`)
|
|
150
|
+
whereClauses.push(`sync_id IN (${placeholders.join(", ")})`)
|
|
151
|
+
params.push(...input.syncIds)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (input.datasetId) {
|
|
155
|
+
whereClauses.push(`dataset_id = $${index++}`)
|
|
156
|
+
params.push(input.datasetId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
index = appendRunListFilters(whereClauses, params, index, input)
|
|
160
|
+
|
|
161
|
+
const { rows, total, hasMore } = await queryRunList<DatabaseRow>({
|
|
162
|
+
sql: this.sql,
|
|
163
|
+
tableName: "sync_runs",
|
|
164
|
+
selectList: "*, checkpoint IS NOT NULL AS checkpoint_present",
|
|
165
|
+
whereClauses,
|
|
166
|
+
params,
|
|
167
|
+
nextIndex: index,
|
|
168
|
+
order: input.order,
|
|
169
|
+
limit: input.limit,
|
|
170
|
+
offset: input.offset,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
runs: rows.map(rowToSyncRunRecord),
|
|
175
|
+
hasMore,
|
|
176
|
+
total,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async listLatestBySyncIds(input: ListLatestSyncRunsInput): Promise<ListLatestSyncRunsResult> {
|
|
181
|
+
const rows = await queryLatestRunsByOwnerId<DatabaseRow>({
|
|
182
|
+
sql: this.sql,
|
|
183
|
+
tableName: "sync_runs",
|
|
184
|
+
ownerColumn: "sync_id",
|
|
185
|
+
ownerIds: input.syncIds,
|
|
186
|
+
projectId: input.projectId,
|
|
187
|
+
ownerIdFor: (row) => row.sync_id,
|
|
188
|
+
selectList: "*, checkpoint IS NOT NULL AS checkpoint_present",
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
runs: rows.map(rowToSyncRunRecord),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function serializeCheckpoint(checkpoint: JsonValue | undefined): string | null {
|
|
198
|
+
return checkpoint !== undefined ? JSON.stringify(checkpoint) : null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function toSyncRunFailure(row: DatabaseRow): SyncRunFailure | undefined {
|
|
202
|
+
if (!row.error_message) {
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
name: row.error_name ?? undefined,
|
|
208
|
+
message: row.error_message,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
|
|
213
|
+
return {
|
|
214
|
+
id: row.id,
|
|
215
|
+
projectId: row.project_id,
|
|
216
|
+
syncId: row.sync_id,
|
|
217
|
+
datasetId: row.dataset_id,
|
|
218
|
+
mode: row.mode,
|
|
219
|
+
status: row.status,
|
|
220
|
+
startedAt: new Date(row.started_at),
|
|
221
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
222
|
+
rowsRead: row.rows_read != null ? Number(row.rows_read) : undefined,
|
|
223
|
+
output: row.output_version_id
|
|
224
|
+
? {
|
|
225
|
+
datasetId: row.dataset_id,
|
|
226
|
+
versionId: row.output_version_id,
|
|
227
|
+
}
|
|
228
|
+
: undefined,
|
|
229
|
+
expectedLatestVersionId: row.expected_latest_version_id ?? undefined,
|
|
230
|
+
commitMessage: row.commit_message ?? undefined,
|
|
231
|
+
error: toSyncRunFailure(row),
|
|
232
|
+
checkpoint: hasCheckpoint(row) ? row.checkpoint : undefined,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasCheckpoint(row: DatabaseRow): boolean {
|
|
237
|
+
return (
|
|
238
|
+
row.checkpoint_present === true ||
|
|
239
|
+
row.checkpoint_present === 1 ||
|
|
240
|
+
row.checkpoint_present === "1" ||
|
|
241
|
+
row.checkpoint_present === "t" ||
|
|
242
|
+
row.checkpoint_present === "true"
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface DatabaseRow {
|
|
247
|
+
project_id: string
|
|
248
|
+
id: string
|
|
249
|
+
sync_id: string
|
|
250
|
+
dataset_id: string
|
|
251
|
+
mode: SyncRunRecord["mode"]
|
|
252
|
+
status: SyncRunRecord["status"]
|
|
253
|
+
started_at: Date | string
|
|
254
|
+
finished_at: Date | string | null
|
|
255
|
+
rows_read: number | string | null
|
|
256
|
+
output_version_id: string | null
|
|
257
|
+
expected_latest_version_id: string | null
|
|
258
|
+
commit_message: string | null
|
|
259
|
+
error_name: string | null
|
|
260
|
+
error_message: string | null
|
|
261
|
+
checkpoint: JsonValue | null
|
|
262
|
+
checkpoint_present: boolean | number | string
|
|
263
|
+
}
|