@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,638 @@
|
|
|
1
|
+
import type { ObjectQuery } from "@sixb/core"
|
|
2
|
+
import type {
|
|
3
|
+
CountObjectsInput,
|
|
4
|
+
CountObjectsResult,
|
|
5
|
+
ExistsObjectsInput,
|
|
6
|
+
ExistsObjectsResult,
|
|
7
|
+
ExpandedLinkValue,
|
|
8
|
+
ExpandedObjectRow,
|
|
9
|
+
FacetObjectsInput,
|
|
10
|
+
FacetObjectsResult,
|
|
11
|
+
LinkDirection,
|
|
12
|
+
ObjectLinkRow,
|
|
13
|
+
ObjectQueryCapabilities,
|
|
14
|
+
ObjectRow,
|
|
15
|
+
ObjectRowLinks,
|
|
16
|
+
ObjectStorage,
|
|
17
|
+
QueryObjectsInput,
|
|
18
|
+
QueryObjectsResult,
|
|
19
|
+
} from "@sixb/core/storage"
|
|
20
|
+
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
21
|
+
import {
|
|
22
|
+
type CompiledPgObjectQuery,
|
|
23
|
+
compilePgObjectCountQuery,
|
|
24
|
+
compilePgObjectExistsQuery,
|
|
25
|
+
compilePgObjectFacetQuery,
|
|
26
|
+
compilePgObjectQuery,
|
|
27
|
+
} from "./pg-object-query-compiler"
|
|
28
|
+
import type { PgStoreClient } from "./transactions"
|
|
29
|
+
|
|
30
|
+
const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
31
|
+
queryObjects: true,
|
|
32
|
+
countObjects: true,
|
|
33
|
+
existsObjects: true,
|
|
34
|
+
facetObjects: true,
|
|
35
|
+
nodes: {
|
|
36
|
+
start: true,
|
|
37
|
+
filter: true,
|
|
38
|
+
text: true,
|
|
39
|
+
sort: true,
|
|
40
|
+
limit: true,
|
|
41
|
+
page: true,
|
|
42
|
+
traverse: true,
|
|
43
|
+
set: true,
|
|
44
|
+
project: true,
|
|
45
|
+
expand: true,
|
|
46
|
+
},
|
|
47
|
+
predicateOps: {
|
|
48
|
+
and: true,
|
|
49
|
+
or: true,
|
|
50
|
+
not: true,
|
|
51
|
+
eq: true,
|
|
52
|
+
neq: true,
|
|
53
|
+
lt: true,
|
|
54
|
+
lte: true,
|
|
55
|
+
gt: true,
|
|
56
|
+
gte: true,
|
|
57
|
+
in: true,
|
|
58
|
+
exists: true,
|
|
59
|
+
contains: true,
|
|
60
|
+
},
|
|
61
|
+
sortKinds: {
|
|
62
|
+
property: true,
|
|
63
|
+
},
|
|
64
|
+
traversalDirections: {
|
|
65
|
+
outgoing: true,
|
|
66
|
+
incoming: true,
|
|
67
|
+
},
|
|
68
|
+
setOps: {
|
|
69
|
+
union: true,
|
|
70
|
+
intersect: true,
|
|
71
|
+
subtract: true,
|
|
72
|
+
},
|
|
73
|
+
scalarOperations: {
|
|
74
|
+
string: { equality: true, ordering: true },
|
|
75
|
+
uuid: { equality: true, ordering: true },
|
|
76
|
+
boolean: { equality: true },
|
|
77
|
+
integer: { equality: true, ordering: true },
|
|
78
|
+
double: { equality: true, ordering: true },
|
|
79
|
+
decimal: { equality: true, ordering: true },
|
|
80
|
+
date: { equality: true, ordering: true },
|
|
81
|
+
timestamp: { equality: true, ordering: true },
|
|
82
|
+
},
|
|
83
|
+
limits: {
|
|
84
|
+
totalCount: true,
|
|
85
|
+
stablePageTokens: true,
|
|
86
|
+
},
|
|
87
|
+
notes: [
|
|
88
|
+
"PostgreSQL object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSONB properties and object links.",
|
|
89
|
+
"expand hydrates linked objects in-database (top-N per parent via LATERAL + jsonb_agg); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
|
|
90
|
+
"Exact decimal predicates, ordering, and keyset pagination use PostgreSQL numeric casts.",
|
|
91
|
+
"Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build a `SELECT ... JOIN (VALUES ...) AS t(...) ON ... WHERE ...` query
|
|
97
|
+
* with positional parameters. Bun SQL's tagged-template helpers don't
|
|
98
|
+
* support VALUES inside SELECT/JOIN, so we construct the query string
|
|
99
|
+
* manually while keeping all user data in the parameter array.
|
|
100
|
+
*/
|
|
101
|
+
function valuesJoin<Row = unknown>(
|
|
102
|
+
sql: SQLClient,
|
|
103
|
+
select: string,
|
|
104
|
+
columns: string[],
|
|
105
|
+
tuples: unknown[][],
|
|
106
|
+
where: string,
|
|
107
|
+
whereParams: unknown[]
|
|
108
|
+
): Promise<Row[]> {
|
|
109
|
+
const alias = "t"
|
|
110
|
+
const colWidth = columns.length
|
|
111
|
+
|
|
112
|
+
// $1 … $N are reserved for the WHERE params that come first.
|
|
113
|
+
const base = whereParams.length
|
|
114
|
+
const valuePlaceholders = tuples
|
|
115
|
+
.map((_, i) => {
|
|
116
|
+
const cols = columns.map((_, j) => `$${base + i * colWidth + j + 1}`)
|
|
117
|
+
return `(${cols.join(",")})`
|
|
118
|
+
})
|
|
119
|
+
.join(",")
|
|
120
|
+
|
|
121
|
+
const onClause = columns
|
|
122
|
+
.map((c) => {
|
|
123
|
+
// Infer the table alias from the SELECT clause (first word after SELECT ... FROM)
|
|
124
|
+
const srcAlias = select.match(/FROM\s+\w+\s+(\w+)/i)?.[1] ?? select.split(" ").pop()
|
|
125
|
+
return `${srcAlias}.${c} = ${alias}.${c}`
|
|
126
|
+
})
|
|
127
|
+
.join(" AND ")
|
|
128
|
+
|
|
129
|
+
const query = `${select} JOIN (VALUES ${valuePlaceholders}) AS ${alias}(${columns.join(",")}) ON ${onClause} ${where}`
|
|
130
|
+
const params = [...whereParams, ...tuples.flat()]
|
|
131
|
+
|
|
132
|
+
return sql.unsafe(query, params as SqlParameter[]) as unknown as Promise<Row[]>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* PostgreSQL-based ObjectStorage implementation.
|
|
137
|
+
*
|
|
138
|
+
* Stores object projections and links. V1 object-query IR pushdown covers the
|
|
139
|
+
* scalar JSONB-property and link-traversal subset declared by queryCapabilities().
|
|
140
|
+
*
|
|
141
|
+
* Requires `search_path` to be set to the Sixb schema on the connection.
|
|
142
|
+
*
|
|
143
|
+
* JSONB handling notes (Bun SQL specifics):
|
|
144
|
+
* - Writes use `${JSON.stringify(v)}::text::jsonb`. The `::text` forces the
|
|
145
|
+
* text wire type so that `::jsonb` parses the JSON text into a proper JSONB
|
|
146
|
+
* value (object, number, etc.) instead of a JSONB string literal.
|
|
147
|
+
* - The `@>` containment operator receives a plain JS object which Bun SQL
|
|
148
|
+
* auto-serialises as JSONB.
|
|
149
|
+
* - Reads return JSONB as native JS types (objects, numbers, booleans) when
|
|
150
|
+
* the data was stored correctly.
|
|
151
|
+
*/
|
|
152
|
+
export class PgObjectStorage implements ObjectStorage {
|
|
153
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
154
|
+
|
|
155
|
+
queryCapabilities(): ObjectQueryCapabilities {
|
|
156
|
+
return PG_OBJECT_QUERY_CAPABILITIES
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
|
|
160
|
+
const compiled = compilePgObjectQuery(params.projectId, params.query, {
|
|
161
|
+
includeTotal: params.includeTotal,
|
|
162
|
+
})
|
|
163
|
+
const total = params.includeTotal === false ? undefined : await readTotal(this.sql, compiled)
|
|
164
|
+
const rawRows = await this.sql.unsafe<ObjectQueryDatabaseRow[]>(
|
|
165
|
+
compiled.sql,
|
|
166
|
+
compiled.args as SqlParameter[]
|
|
167
|
+
)
|
|
168
|
+
const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
|
|
169
|
+
const hasMore =
|
|
170
|
+
total === undefined && compiled.hasMoreProbe
|
|
171
|
+
? compiled.hasMoreProbe.hasMore(
|
|
172
|
+
(
|
|
173
|
+
await this.sql.unsafe(
|
|
174
|
+
compiled.hasMoreProbe.sql,
|
|
175
|
+
compiled.hasMoreProbe.args as SqlParameter[]
|
|
176
|
+
)
|
|
177
|
+
).length
|
|
178
|
+
)
|
|
179
|
+
: compiled.hasMore(rawRows.length, total)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
objects: rows.map((row) => queryRowToObject(row)),
|
|
183
|
+
hasMore,
|
|
184
|
+
nextPageToken: compiled.nextPageToken(rows, rawRows.length),
|
|
185
|
+
...(total === undefined ? {} : { total }),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
|
|
190
|
+
const compiled = compilePgObjectCountQuery(params.projectId, stripOuterRowShape(params.query))
|
|
191
|
+
const [row] = await this.sql.unsafe<{ count: string | number | bigint }[]>(
|
|
192
|
+
compiled.sql,
|
|
193
|
+
compiled.args as SqlParameter[]
|
|
194
|
+
)
|
|
195
|
+
return { count: Number(row?.count ?? 0) }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
|
|
199
|
+
const compiled = compilePgObjectExistsQuery(params.projectId, stripOuterRowShape(params.query))
|
|
200
|
+
const [row] = await this.sql.unsafe<unknown[]>(compiled.sql, compiled.args as SqlParameter[])
|
|
201
|
+
return { exists: row !== undefined }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
|
|
205
|
+
return {
|
|
206
|
+
facets: await Promise.all(
|
|
207
|
+
params.facets.map(async (facet) => ({
|
|
208
|
+
propertyId: facet.propertyId,
|
|
209
|
+
buckets: await readFacetBuckets(
|
|
210
|
+
this.sql,
|
|
211
|
+
compilePgObjectFacetQuery(
|
|
212
|
+
params.projectId,
|
|
213
|
+
stripOuterRowShape(params.query),
|
|
214
|
+
facet.propertyId,
|
|
215
|
+
facet.limit
|
|
216
|
+
)
|
|
217
|
+
),
|
|
218
|
+
}))
|
|
219
|
+
),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async getByPrimaryId(params: {
|
|
224
|
+
projectId: string
|
|
225
|
+
objectTypeId: string
|
|
226
|
+
primaryId: string
|
|
227
|
+
}): Promise<ObjectRow | null> {
|
|
228
|
+
const [row] = await this.sql<ObjectDatabaseRow[]>`
|
|
229
|
+
SELECT * FROM objects
|
|
230
|
+
WHERE project_id = ${params.projectId}
|
|
231
|
+
AND object_type_id = ${params.objectTypeId}
|
|
232
|
+
AND primary_id = ${params.primaryId}
|
|
233
|
+
`
|
|
234
|
+
|
|
235
|
+
return row ? rowToObject(row) : null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async listLinks(params: {
|
|
239
|
+
projectId: string
|
|
240
|
+
objectTypeId: string
|
|
241
|
+
objectId: string
|
|
242
|
+
linkId?: string
|
|
243
|
+
direction?: LinkDirection
|
|
244
|
+
}): Promise<readonly ObjectLinkRow[]> {
|
|
245
|
+
const direction = params.direction ?? "outgoing"
|
|
246
|
+
const directionWhere =
|
|
247
|
+
direction === "incoming"
|
|
248
|
+
? "target_type_id = $2 AND target_id = $3"
|
|
249
|
+
: direction === "both"
|
|
250
|
+
? "((source_type_id = $2 AND source_id = $3) OR (target_type_id = $2 AND target_id = $3))"
|
|
251
|
+
: "source_type_id = $2 AND source_id = $3"
|
|
252
|
+
const query = `SELECT * FROM links WHERE project_id = $1 AND ${directionWhere}${
|
|
253
|
+
params.linkId ? " AND link_id = $4" : ""
|
|
254
|
+
}`
|
|
255
|
+
const args = params.linkId
|
|
256
|
+
? [params.projectId, params.objectTypeId, params.objectId, params.linkId]
|
|
257
|
+
: [params.projectId, params.objectTypeId, params.objectId]
|
|
258
|
+
const rows = await this.sql.unsafe<LinkDatabaseRow[]>(query, args)
|
|
259
|
+
|
|
260
|
+
return rows.map((row) => rowToLink(row))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async getByPrimaryIdBatch(params: {
|
|
264
|
+
projectId: string
|
|
265
|
+
items: readonly { objectTypeId: string; primaryId: string }[]
|
|
266
|
+
}): Promise<Map<string, ObjectRow>> {
|
|
267
|
+
const result = new Map<string, ObjectRow>()
|
|
268
|
+
if (params.items.length === 0) return result
|
|
269
|
+
const rows = await valuesJoin<ObjectDatabaseRow>(
|
|
270
|
+
this.sql,
|
|
271
|
+
"SELECT o.* FROM objects o",
|
|
272
|
+
["object_type_id", "primary_id"],
|
|
273
|
+
params.items.map((i) => [i.objectTypeId, i.primaryId]),
|
|
274
|
+
`WHERE o.project_id = $1`,
|
|
275
|
+
[params.projectId]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
for (const row of rows) {
|
|
279
|
+
result.set(`${row.object_type_id}:${row.primary_id}`, rowToObject(row))
|
|
280
|
+
}
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async listLinksBatch(params: {
|
|
285
|
+
projectId: string
|
|
286
|
+
items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
|
|
287
|
+
}): Promise<Map<string, ObjectLinkRow[]>> {
|
|
288
|
+
const result = new Map<string, ObjectLinkRow[]>()
|
|
289
|
+
if (params.items.length === 0) return result
|
|
290
|
+
const rows = await valuesJoin<LinkDatabaseRow>(
|
|
291
|
+
this.sql,
|
|
292
|
+
"SELECT l.* FROM links l",
|
|
293
|
+
["source_type_id", "source_id", "link_id"],
|
|
294
|
+
params.items.map((i) => [i.objectTypeId, i.objectId, i.linkId]),
|
|
295
|
+
`WHERE l.project_id = $1`,
|
|
296
|
+
[params.projectId]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
for (const row of rows) {
|
|
300
|
+
const key = `${row.source_type_id}:${row.source_id}:${row.link_id}`
|
|
301
|
+
const existing = result.get(key) ?? []
|
|
302
|
+
existing.push(rowToLink(row))
|
|
303
|
+
result.set(key, existing)
|
|
304
|
+
}
|
|
305
|
+
return result
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async listIncidentLinksBatch(params: {
|
|
309
|
+
projectId: string
|
|
310
|
+
items: readonly { objectTypeId: string; objectId: string }[]
|
|
311
|
+
}): Promise<readonly ObjectLinkRow[]> {
|
|
312
|
+
if (params.items.length === 0) return []
|
|
313
|
+
const tuples = params.items.map((item) => [item.objectTypeId, item.objectId])
|
|
314
|
+
|
|
315
|
+
// Cover both link directions with two index-friendly equality joins (source-side, then
|
|
316
|
+
// target-side) rather than a single OR-join, which would defeat index usage. This is a constant
|
|
317
|
+
// number of round trips regardless of how many objects are deleted — issued sequentially because
|
|
318
|
+
// these reads run on the serializable commit transaction's single connection. A link incident to
|
|
319
|
+
// two listed objects matches both halves and is de-duplicated below.
|
|
320
|
+
const sourceRows = await valuesJoin<LinkDatabaseRow>(
|
|
321
|
+
this.sql,
|
|
322
|
+
"SELECT l.* FROM links l",
|
|
323
|
+
["source_type_id", "source_id"],
|
|
324
|
+
tuples,
|
|
325
|
+
"WHERE l.project_id = $1",
|
|
326
|
+
[params.projectId]
|
|
327
|
+
)
|
|
328
|
+
const targetRows = await valuesJoin<LinkDatabaseRow>(
|
|
329
|
+
this.sql,
|
|
330
|
+
"SELECT l.* FROM links l",
|
|
331
|
+
["target_type_id", "target_id"],
|
|
332
|
+
tuples,
|
|
333
|
+
"WHERE l.project_id = $1",
|
|
334
|
+
[params.projectId]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const deduped = new Map<string, ObjectLinkRow>()
|
|
338
|
+
for (const row of [...sourceRows, ...targetRows]) {
|
|
339
|
+
const link = rowToLink(row)
|
|
340
|
+
deduped.set(
|
|
341
|
+
`${link.sourceTypeId}:${link.sourceId}:${link.linkId}:${link.targetTypeId}:${link.targetId}`,
|
|
342
|
+
link
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
return [...deduped.values()]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async listByPrimaryIdPage(params: {
|
|
349
|
+
projectId: string
|
|
350
|
+
objectTypeId: string
|
|
351
|
+
afterPrimaryId?: string
|
|
352
|
+
limit: number
|
|
353
|
+
}): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
|
|
354
|
+
assertReconciliationPageLimit(params.limit)
|
|
355
|
+
const rows = await this.sql<ObjectDatabaseRow[]>`
|
|
356
|
+
SELECT * FROM objects
|
|
357
|
+
WHERE project_id = ${params.projectId}
|
|
358
|
+
AND object_type_id = ${params.objectTypeId}
|
|
359
|
+
${params.afterPrimaryId ? this.sql`AND primary_id > ${params.afterPrimaryId}` : this.sql``}
|
|
360
|
+
ORDER BY primary_id ASC
|
|
361
|
+
LIMIT ${params.limit + 1}
|
|
362
|
+
`
|
|
363
|
+
const hasMore = rows.length > params.limit
|
|
364
|
+
const objects = rows.slice(0, params.limit).map((row) => rowToObject(row))
|
|
365
|
+
const last = objects.at(-1)
|
|
366
|
+
return {
|
|
367
|
+
objects,
|
|
368
|
+
...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async list(params: {
|
|
373
|
+
projectId: string
|
|
374
|
+
objectTypeId?: string | readonly string[]
|
|
375
|
+
primaryIdPrefix?: string
|
|
376
|
+
primaryIdSuffix?: string
|
|
377
|
+
updatedAfter?: Date
|
|
378
|
+
updatedBefore?: Date
|
|
379
|
+
createdAfter?: Date
|
|
380
|
+
createdBefore?: Date
|
|
381
|
+
limit?: number
|
|
382
|
+
offset?: number
|
|
383
|
+
orderBy?: "createdAt" | "updatedAt" | "primaryId"
|
|
384
|
+
order?: "asc" | "desc"
|
|
385
|
+
}): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
|
|
386
|
+
const queryOffset = params.offset ?? 0
|
|
387
|
+
const limit = params.limit ?? 50
|
|
388
|
+
const orderBy = params.orderBy ?? "updatedAt"
|
|
389
|
+
const order = params.order ?? "desc"
|
|
390
|
+
const orderColumn =
|
|
391
|
+
orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
|
|
392
|
+
|
|
393
|
+
// Build WHERE conditions using conditional fragments
|
|
394
|
+
const typeFilter =
|
|
395
|
+
params.objectTypeId === undefined
|
|
396
|
+
? this.sql``
|
|
397
|
+
: typeof params.objectTypeId === "string"
|
|
398
|
+
? this.sql`AND object_type_id = ${params.objectTypeId}`
|
|
399
|
+
: this.sql`AND object_type_id IN ${this.sql(params.objectTypeId as string[])}`
|
|
400
|
+
|
|
401
|
+
const primaryIdPrefixFilter = params.primaryIdPrefix
|
|
402
|
+
? this.sql`AND primary_id LIKE ${`${params.primaryIdPrefix}%`}`
|
|
403
|
+
: this.sql``
|
|
404
|
+
|
|
405
|
+
const primaryIdSuffixFilter = params.primaryIdSuffix
|
|
406
|
+
? this.sql`AND primary_id LIKE ${`%${params.primaryIdSuffix}`}`
|
|
407
|
+
: this.sql``
|
|
408
|
+
|
|
409
|
+
const updatedAfterFilter = params.updatedAfter
|
|
410
|
+
? this.sql`AND updated_at >= ${params.updatedAfter}`
|
|
411
|
+
: this.sql``
|
|
412
|
+
|
|
413
|
+
const updatedBeforeFilter = params.updatedBefore
|
|
414
|
+
? this.sql`AND updated_at <= ${params.updatedBefore}`
|
|
415
|
+
: this.sql``
|
|
416
|
+
|
|
417
|
+
const createdAfterFilter = params.createdAfter
|
|
418
|
+
? this.sql`AND created_at >= ${params.createdAfter}`
|
|
419
|
+
: this.sql``
|
|
420
|
+
|
|
421
|
+
const createdBeforeFilter = params.createdBefore
|
|
422
|
+
? this.sql`AND created_at <= ${params.createdBefore}`
|
|
423
|
+
: this.sql``
|
|
424
|
+
|
|
425
|
+
const filters = this.sql`
|
|
426
|
+
${typeFilter}
|
|
427
|
+
${primaryIdPrefixFilter}
|
|
428
|
+
${primaryIdSuffixFilter}
|
|
429
|
+
${updatedAfterFilter}
|
|
430
|
+
${updatedBeforeFilter}
|
|
431
|
+
${createdAfterFilter}
|
|
432
|
+
${createdBeforeFilter}
|
|
433
|
+
`
|
|
434
|
+
|
|
435
|
+
// Get total count
|
|
436
|
+
const [countResult] = await this.sql<{ total: number }[]>`
|
|
437
|
+
SELECT COUNT(*)::int AS total FROM objects
|
|
438
|
+
WHERE project_id = ${params.projectId}
|
|
439
|
+
${filters}
|
|
440
|
+
`
|
|
441
|
+
const total = countResult.total
|
|
442
|
+
|
|
443
|
+
if (limit === 0) return { objects: [], hasMore: queryOffset < total, total }
|
|
444
|
+
|
|
445
|
+
// Get paginated results (+1 for hasMore check)
|
|
446
|
+
// Column and direction derived from a fixed mapping — safe to use as identifiers
|
|
447
|
+
const fetchLimit = limit + 1
|
|
448
|
+
const rows = await this.sql<ObjectDatabaseRow[]>`
|
|
449
|
+
SELECT * FROM objects
|
|
450
|
+
WHERE project_id = ${params.projectId}
|
|
451
|
+
${filters}
|
|
452
|
+
ORDER BY ${this.sql(orderColumn)} ${order === "asc" ? this.sql`ASC` : this.sql`DESC`}
|
|
453
|
+
LIMIT ${fetchLimit}
|
|
454
|
+
OFFSET ${queryOffset}
|
|
455
|
+
`
|
|
456
|
+
|
|
457
|
+
const hasMore = rows.length > limit
|
|
458
|
+
const objects = rows.slice(0, limit).map((row) => rowToObject(row))
|
|
459
|
+
|
|
460
|
+
return { objects, hasMore, total }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function assertReconciliationPageLimit(limit: number): void {
|
|
465
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
466
|
+
throw new Error("Object reconciliation page limit must be a positive safe integer.")
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function readTotal(sql: SQLClient, compiled: CompiledPgObjectQuery): Promise<number> {
|
|
471
|
+
const [row] = await sql.unsafe<
|
|
472
|
+
{
|
|
473
|
+
total: string | number | bigint
|
|
474
|
+
}[]
|
|
475
|
+
>(compiled.totalSql, compiled.totalArgs as SqlParameter[])
|
|
476
|
+
return Number(row?.total ?? 0)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function readFacetBuckets(
|
|
480
|
+
sql: SQLClient,
|
|
481
|
+
compiled: { sql: string; args: readonly unknown[] }
|
|
482
|
+
): Promise<{ value: unknown; count: number }[]> {
|
|
483
|
+
const rows = await sql.unsafe<FacetDatabaseRow[]>(compiled.sql, [
|
|
484
|
+
...compiled.args,
|
|
485
|
+
] as SqlParameter[])
|
|
486
|
+
|
|
487
|
+
return rows.map((row) => ({
|
|
488
|
+
value: pgFacetValue(row.value_type, row.value_text),
|
|
489
|
+
count: Number(row.count),
|
|
490
|
+
}))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function pgFacetValue(valueType: string | null, valueText: string | null): unknown {
|
|
494
|
+
switch (valueType) {
|
|
495
|
+
case "string":
|
|
496
|
+
return valueText ?? ""
|
|
497
|
+
case "number":
|
|
498
|
+
return valueText === null ? null : Number(valueText)
|
|
499
|
+
case "boolean":
|
|
500
|
+
return valueText === "true"
|
|
501
|
+
case "null":
|
|
502
|
+
return null
|
|
503
|
+
case "array":
|
|
504
|
+
case "object":
|
|
505
|
+
return valueText === null ? null : JSON.parse(valueText)
|
|
506
|
+
default:
|
|
507
|
+
return valueText
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function stripOuterRowShape(query: ObjectQuery): ObjectQuery {
|
|
512
|
+
switch (query.kind) {
|
|
513
|
+
case "limit":
|
|
514
|
+
case "page":
|
|
515
|
+
case "project":
|
|
516
|
+
case "sort":
|
|
517
|
+
return stripOuterRowShape(query.input)
|
|
518
|
+
default:
|
|
519
|
+
return query
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function rowToObject(row: ObjectDatabaseRow): ObjectRow {
|
|
524
|
+
return {
|
|
525
|
+
projectId: row.project_id,
|
|
526
|
+
objectTypeId: row.object_type_id,
|
|
527
|
+
primaryId: row.primary_id,
|
|
528
|
+
properties: row.properties as Record<string, unknown>,
|
|
529
|
+
createdAt: new Date(row.created_at),
|
|
530
|
+
updatedAt: new Date(row.updated_at),
|
|
531
|
+
version: row.version,
|
|
532
|
+
lastCommitId: row.last_commit_id,
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Map a query row, attaching `links` when an `expand` pushdown produced them. The
|
|
537
|
+
// base columns map as usual; `_expand` (a `jsonb_build_object`) is revived into
|
|
538
|
+
// the runtime link shape the core executor's fallback also produces.
|
|
539
|
+
function queryRowToObject(row: ObjectQueryDatabaseRow): ObjectRow {
|
|
540
|
+
const base = rowToObject(row)
|
|
541
|
+
const links = reviveExpandedLinks(row._expand)
|
|
542
|
+
return links ? { ...base, links } : base
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function reviveExpandedLinks(value: unknown): ObjectRowLinks | undefined {
|
|
546
|
+
if (!isPlainRecord(value)) return undefined
|
|
547
|
+
const links: ObjectRowLinks = {}
|
|
548
|
+
for (const [linkId, raw] of Object.entries(value)) {
|
|
549
|
+
links[linkId] = reviveExpandedLinkValue(raw)
|
|
550
|
+
}
|
|
551
|
+
return links
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// A "one" expansion arrives as a single object or null; a "many" expansion as an
|
|
555
|
+
// array (already ordered and trimmed in the database).
|
|
556
|
+
function reviveExpandedLinkValue(value: unknown): ExpandedLinkValue {
|
|
557
|
+
if (value === null || value === undefined) return null
|
|
558
|
+
if (Array.isArray(value)) return value.map(reviveExpandedRow)
|
|
559
|
+
return reviveExpandedRow(value)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Revive one hydrated neighbour from `compileExpansionChildJson`: JSONB timestamp
|
|
563
|
+
// strings back to `Date`, empty link properties dropped (matching the fallback),
|
|
564
|
+
// and nested links recursed.
|
|
565
|
+
function reviveExpandedRow(value: unknown): ExpandedObjectRow {
|
|
566
|
+
const row = isPlainRecord(value) ? value : {}
|
|
567
|
+
const expanded: ExpandedObjectRow = {
|
|
568
|
+
projectId: String(row.projectId),
|
|
569
|
+
objectTypeId: String(row.objectTypeId),
|
|
570
|
+
primaryId: String(row.primaryId),
|
|
571
|
+
properties: isPlainRecord(row.properties) ? row.properties : {},
|
|
572
|
+
createdAt: new Date(row.createdAt as string),
|
|
573
|
+
updatedAt: new Date(row.updatedAt as string),
|
|
574
|
+
version: Number(row.version),
|
|
575
|
+
lastCommitId: String(row.lastCommitId),
|
|
576
|
+
}
|
|
577
|
+
if (isPlainRecord(row.linkProperties) && Object.keys(row.linkProperties).length > 0) {
|
|
578
|
+
expanded.linkProperties = row.linkProperties
|
|
579
|
+
}
|
|
580
|
+
const links = reviveExpandedLinks(row.links)
|
|
581
|
+
if (links) expanded.links = links
|
|
582
|
+
return expanded
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
586
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
|
|
590
|
+
return {
|
|
591
|
+
projectId: row.project_id,
|
|
592
|
+
sourceTypeId: row.source_type_id,
|
|
593
|
+
sourceId: row.source_id,
|
|
594
|
+
linkId: row.link_id,
|
|
595
|
+
targetTypeId: row.target_type_id,
|
|
596
|
+
targetId: row.target_id,
|
|
597
|
+
properties: row.properties ? (row.properties as Record<string, unknown>) : undefined,
|
|
598
|
+
createdAt: new Date(row.created_at),
|
|
599
|
+
updatedAt: new Date(row.updated_at),
|
|
600
|
+
lastCommitId: row.last_commit_id,
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
interface ObjectDatabaseRow {
|
|
605
|
+
project_id: string
|
|
606
|
+
object_type_id: string
|
|
607
|
+
primary_id: string
|
|
608
|
+
properties: unknown
|
|
609
|
+
created_at: Date | string
|
|
610
|
+
updated_at: Date | string
|
|
611
|
+
version: number
|
|
612
|
+
last_commit_id: string
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
interface ObjectQueryDatabaseRow extends ObjectDatabaseRow {
|
|
616
|
+
_cursor_properties?: unknown
|
|
617
|
+
/** `jsonb_build_object(linkId, value, ...)` from an `expand` pushdown; absent otherwise. */
|
|
618
|
+
_expand?: unknown
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
interface FacetDatabaseRow {
|
|
622
|
+
value_type: string | null
|
|
623
|
+
value_text: string | null
|
|
624
|
+
count: string | number | bigint
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
interface LinkDatabaseRow {
|
|
628
|
+
project_id: string
|
|
629
|
+
source_type_id: string
|
|
630
|
+
source_id: string
|
|
631
|
+
link_id: string
|
|
632
|
+
target_type_id: string
|
|
633
|
+
target_id: string
|
|
634
|
+
properties: unknown | null
|
|
635
|
+
created_at: Date | string
|
|
636
|
+
updated_at: Date | string
|
|
637
|
+
last_commit_id: string
|
|
638
|
+
}
|