@sixb/pg 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/rows.d.ts +0 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7596 -6416
- package/dist/index.js.map +25 -19
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +2 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
- package/dist/objects/query-compiler.d.ts.map +1 -0
- package/dist/objects/read-scope.d.ts +17 -0
- package/dist/objects/read-scope.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +50 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +47 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +8 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +3 -2
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
- package/dist/pg-sync-run-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +11 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +11 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/transactions.d.ts +9 -2
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agents/rows.ts +0 -5
- package/src/agents/runs.ts +1 -6
- package/src/ai-cost-groups.ts +3 -0
- package/src/index.ts +20 -5
- package/src/migrations/035-share-grants.sql +57 -0
- package/src/migrations/036-share-sessions.sql +173 -0
- package/src/migrations/037-execution-requester-groups.sql +21 -0
- package/src/migrations.ts +9 -0
- package/src/objects/index.ts +1 -0
- package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
- package/src/objects/read-scope.ts +324 -0
- package/src/objects/reader.ts +604 -0
- package/src/objects/rows.ts +146 -0
- package/src/objects/selected-reader.ts +118 -0
- package/src/objects/storage.ts +305 -0
- package/src/pg-action-run-storage.ts +6 -7
- package/src/pg-ai-cost-storage.ts +11 -2
- package/src/pg-ai-usage-storage.ts +1 -2
- package/src/pg-execution-storage.ts +27 -6
- package/src/pg-pipeline-run-storage.ts +2 -5
- package/src/pg-sync-run-storage.ts +2 -5
- package/src/pg-workflow-run-storage.ts +0 -5
- package/src/share-grant-storage.ts +236 -0
- package/src/share-session-storage.ts +127 -0
- package/src/storage-errors.ts +6 -0
- package/src/transactions.ts +46 -5
- package/dist/pg-object-query-compiler.d.ts.map +0 -1
- package/dist/pg-object-storage.d.ts.map +0 -1
- package/src/pg-object-storage.ts +0 -734
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import type { ObjectQuery } from "@sixb/core"
|
|
2
|
+
import type {
|
|
3
|
+
CountObjectsInput,
|
|
4
|
+
CountObjectsResult,
|
|
5
|
+
ExistsObjectsInput,
|
|
6
|
+
ExistsObjectsResult,
|
|
7
|
+
FacetObjectsInput,
|
|
8
|
+
FacetObjectsResult,
|
|
9
|
+
LinkBatchKey,
|
|
10
|
+
LinkDirection,
|
|
11
|
+
ObjectBatchKey,
|
|
12
|
+
ObjectFacetResult,
|
|
13
|
+
ObjectLinkRow,
|
|
14
|
+
ObjectReadStorage,
|
|
15
|
+
ObjectRow,
|
|
16
|
+
QueryObjectLinksInput,
|
|
17
|
+
QueryObjectLinksResult,
|
|
18
|
+
QueryObjectsInput,
|
|
19
|
+
QueryObjectsResult,
|
|
20
|
+
} from "@sixb/core/storage"
|
|
21
|
+
import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
|
|
22
|
+
import type { SQLClient, SqlParameter } from "../pg-client"
|
|
23
|
+
import {
|
|
24
|
+
type CompiledPgObjectQuery,
|
|
25
|
+
compilePgObjectCountQuery,
|
|
26
|
+
compilePgObjectExistsQuery,
|
|
27
|
+
compilePgObjectFacetQuery,
|
|
28
|
+
compilePgObjectQuery,
|
|
29
|
+
compilePgObjectStatement,
|
|
30
|
+
type PgObjectQuerySource,
|
|
31
|
+
} from "./query-compiler"
|
|
32
|
+
import type { PgSelectedObjectReadSource } from "./read-scope"
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
type FacetDatabaseRow,
|
|
36
|
+
type LinkBatchDatabaseRow,
|
|
37
|
+
type LinkDatabaseRow,
|
|
38
|
+
linkIdentity,
|
|
39
|
+
type ObjectBatchDatabaseRow,
|
|
40
|
+
type ObjectDatabaseRow,
|
|
41
|
+
type ObjectQueryDatabaseRow,
|
|
42
|
+
type PropertyPermissionBatchDatabaseRow,
|
|
43
|
+
queryRowToObject,
|
|
44
|
+
rowToLink,
|
|
45
|
+
rowToObject,
|
|
46
|
+
} from "./rows"
|
|
47
|
+
|
|
48
|
+
export class PgObjectReader {
|
|
49
|
+
constructor(
|
|
50
|
+
private readonly sql: SQLClient,
|
|
51
|
+
private readonly source: PgObjectQuerySource | PgSelectedObjectReadSource
|
|
52
|
+
) {}
|
|
53
|
+
|
|
54
|
+
async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
|
|
55
|
+
const sql = this.sql
|
|
56
|
+
const source = this.source
|
|
57
|
+
const compiled = compilePgObjectQuery(params.projectId, params.query, {
|
|
58
|
+
includeTotal: params.includeTotal,
|
|
59
|
+
source,
|
|
60
|
+
})
|
|
61
|
+
const total = params.includeTotal === false ? undefined : await readTotal(sql, compiled)
|
|
62
|
+
const rawRows = await sql.unsafe<ObjectQueryDatabaseRow[]>(
|
|
63
|
+
compiled.sql,
|
|
64
|
+
compiled.args as SqlParameter[]
|
|
65
|
+
)
|
|
66
|
+
const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
|
|
67
|
+
const hasMore =
|
|
68
|
+
total === undefined && compiled.hasMoreProbe
|
|
69
|
+
? compiled.hasMoreProbe.hasMore(
|
|
70
|
+
(
|
|
71
|
+
await sql.unsafe(
|
|
72
|
+
compiled.hasMoreProbe.sql,
|
|
73
|
+
compiled.hasMoreProbe.args as SqlParameter[]
|
|
74
|
+
)
|
|
75
|
+
).length
|
|
76
|
+
)
|
|
77
|
+
: compiled.hasMore(rawRows.length, total)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
objects: rows.map((row) => queryRowToObject(row)),
|
|
81
|
+
hasMore,
|
|
82
|
+
nextPageToken: compiled.nextPageToken(rows, rawRows.length),
|
|
83
|
+
...(total === undefined ? {} : { total }),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
|
|
88
|
+
const sql = this.sql
|
|
89
|
+
const source = this.source
|
|
90
|
+
const compiled = compilePgObjectCountQuery(params.projectId, stripOuterRowShape(params.query), {
|
|
91
|
+
source,
|
|
92
|
+
})
|
|
93
|
+
const [row] = await sql.unsafe<{ count: string | number | bigint }[]>(
|
|
94
|
+
compiled.sql,
|
|
95
|
+
compiled.args as SqlParameter[]
|
|
96
|
+
)
|
|
97
|
+
return { count: Number(row?.count ?? 0) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
|
|
101
|
+
const sql = this.sql
|
|
102
|
+
const source = this.source
|
|
103
|
+
const compiled = compilePgObjectExistsQuery(
|
|
104
|
+
params.projectId,
|
|
105
|
+
stripOuterRowShape(params.query),
|
|
106
|
+
{ source }
|
|
107
|
+
)
|
|
108
|
+
const [row] = await sql.unsafe<unknown[]>(compiled.sql, compiled.args as SqlParameter[])
|
|
109
|
+
return { exists: row !== undefined }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
|
|
113
|
+
const sql = this.sql
|
|
114
|
+
const source = this.source
|
|
115
|
+
const facets: ObjectFacetResult[] = []
|
|
116
|
+
for (const facet of params.facets) {
|
|
117
|
+
facets.push({
|
|
118
|
+
propertyId: facet.propertyId,
|
|
119
|
+
buckets: await readFacetBuckets(
|
|
120
|
+
sql,
|
|
121
|
+
compilePgObjectFacetQuery(
|
|
122
|
+
params.projectId,
|
|
123
|
+
stripOuterRowShape(params.query),
|
|
124
|
+
facet.propertyId,
|
|
125
|
+
facet.limit,
|
|
126
|
+
{ source }
|
|
127
|
+
)
|
|
128
|
+
),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
return { facets }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getByPrimaryId(params: {
|
|
135
|
+
projectId: string
|
|
136
|
+
objectTypeId: string
|
|
137
|
+
primaryId: string
|
|
138
|
+
}): Promise<ObjectRow | null> {
|
|
139
|
+
const sql = this.sql
|
|
140
|
+
const source = this.source
|
|
141
|
+
const statement = compilePgObjectStatement(
|
|
142
|
+
`
|
|
143
|
+
SELECT *
|
|
144
|
+
FROM ${source.objectsTable}
|
|
145
|
+
WHERE project_id = ? AND object_type_id = ? AND primary_id = ?
|
|
146
|
+
`,
|
|
147
|
+
[params.projectId, params.objectTypeId, params.primaryId],
|
|
148
|
+
source
|
|
149
|
+
)
|
|
150
|
+
const [row] = await sql.unsafe<ObjectDatabaseRow[]>(
|
|
151
|
+
statement.sql,
|
|
152
|
+
statement.args as SqlParameter[]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return row ? rowToObject(row) : null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async selectsObjectProperties(
|
|
159
|
+
params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
|
|
160
|
+
): Promise<readonly boolean[]> {
|
|
161
|
+
const sql = this.sql
|
|
162
|
+
const source = this.source
|
|
163
|
+
const result = params.items.map(() => false)
|
|
164
|
+
if (params.items.length === 0) return result
|
|
165
|
+
|
|
166
|
+
const items = params.items.map((item, batchIndex) => ({ ...item, batchIndex }))
|
|
167
|
+
const selected = "objectPropertyPermissionsTable" in source
|
|
168
|
+
const storedTable = selected ? source.objectPropertyPermissionsTable : source.objectsTable
|
|
169
|
+
const propertyJoin = selected ? 'AND stored.property_id = requested."propertyId"' : ""
|
|
170
|
+
const statement = compilePgObjectStatement(
|
|
171
|
+
`
|
|
172
|
+
SELECT DISTINCT requested."batchIndex" AS _batch_index
|
|
173
|
+
FROM ${storedTable} AS stored
|
|
174
|
+
JOIN jsonb_to_recordset(?::text::jsonb)
|
|
175
|
+
AS requested(
|
|
176
|
+
"objectTypeId" text,
|
|
177
|
+
"primaryId" text,
|
|
178
|
+
"propertyId" text,
|
|
179
|
+
"batchIndex" integer
|
|
180
|
+
)
|
|
181
|
+
ON stored.object_type_id = requested."objectTypeId"
|
|
182
|
+
AND stored.primary_id = requested."primaryId"
|
|
183
|
+
${propertyJoin}
|
|
184
|
+
WHERE stored.project_id = ?
|
|
185
|
+
`,
|
|
186
|
+
[JSON.stringify(items), params.projectId],
|
|
187
|
+
source
|
|
188
|
+
)
|
|
189
|
+
const rows = await sql.unsafe<PropertyPermissionBatchDatabaseRow[]>(
|
|
190
|
+
statement.sql,
|
|
191
|
+
statement.args as SqlParameter[]
|
|
192
|
+
)
|
|
193
|
+
for (const row of rows) result[row._batch_index] = true
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async listLinks(params: {
|
|
198
|
+
projectId: string
|
|
199
|
+
objectTypeId: string
|
|
200
|
+
objectId: string
|
|
201
|
+
linkId?: string
|
|
202
|
+
direction?: LinkDirection
|
|
203
|
+
}): Promise<readonly ObjectLinkRow[]> {
|
|
204
|
+
const sql = this.sql
|
|
205
|
+
const source = this.source
|
|
206
|
+
const direction = params.direction ?? "outgoing"
|
|
207
|
+
const directionWhere =
|
|
208
|
+
direction === "incoming"
|
|
209
|
+
? "target_type_id = ? AND target_id = ?"
|
|
210
|
+
: direction === "both"
|
|
211
|
+
? "((source_type_id = ? AND source_id = ?) OR (target_type_id = ? AND target_id = ?))"
|
|
212
|
+
: "source_type_id = ? AND source_id = ?"
|
|
213
|
+
const args: unknown[] =
|
|
214
|
+
direction === "both"
|
|
215
|
+
? [
|
|
216
|
+
params.projectId,
|
|
217
|
+
params.objectTypeId,
|
|
218
|
+
params.objectId,
|
|
219
|
+
params.objectTypeId,
|
|
220
|
+
params.objectId,
|
|
221
|
+
]
|
|
222
|
+
: [params.projectId, params.objectTypeId, params.objectId]
|
|
223
|
+
const query = `SELECT * FROM ${source.linksTable} WHERE project_id = ? AND ${directionWhere}${
|
|
224
|
+
params.linkId ? " AND link_id = ?" : ""
|
|
225
|
+
}`
|
|
226
|
+
if (params.linkId) args.push(params.linkId)
|
|
227
|
+
const statement = compilePgObjectStatement(query, args, source)
|
|
228
|
+
const rows = await sql.unsafe<LinkDatabaseRow[]>(
|
|
229
|
+
statement.sql,
|
|
230
|
+
statement.args as SqlParameter[]
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return rows.map((row) => rowToLink(row))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async getByPrimaryIdBatch(params: {
|
|
237
|
+
projectId: string
|
|
238
|
+
items: readonly { objectTypeId: string; primaryId: string }[]
|
|
239
|
+
}): Promise<Map<ObjectBatchKey, ObjectRow>> {
|
|
240
|
+
const sql = this.sql
|
|
241
|
+
const source = this.source
|
|
242
|
+
const result = new Map<ObjectBatchKey, ObjectRow>()
|
|
243
|
+
if (params.items.length === 0) return result
|
|
244
|
+
const items = params.items.map((item, batchIndex) => ({ ...item, batchIndex }))
|
|
245
|
+
const statement = compilePgObjectStatement(
|
|
246
|
+
`
|
|
247
|
+
SELECT object.*, requested."batchIndex" AS _batch_index
|
|
248
|
+
FROM jsonb_to_recordset(?::text::jsonb)
|
|
249
|
+
AS requested("objectTypeId" text, "primaryId" text, "batchIndex" integer)
|
|
250
|
+
JOIN ${source.objectsTable} AS object
|
|
251
|
+
ON object.project_id = ?
|
|
252
|
+
AND object.object_type_id = requested."objectTypeId"
|
|
253
|
+
AND object.primary_id = requested."primaryId"
|
|
254
|
+
`,
|
|
255
|
+
[JSON.stringify(items), params.projectId],
|
|
256
|
+
source
|
|
257
|
+
)
|
|
258
|
+
const rows = await sql.unsafe<ObjectBatchDatabaseRow[]>(
|
|
259
|
+
statement.sql,
|
|
260
|
+
statement.args as SqlParameter[]
|
|
261
|
+
)
|
|
262
|
+
const rowsByIndex = new Map(rows.map((row) => [row._batch_index, row]))
|
|
263
|
+
for (const [index, item] of params.items.entries()) {
|
|
264
|
+
const row = rowsByIndex.get(index)
|
|
265
|
+
if (row) result.set(objectBatchKey(item.objectTypeId, item.primaryId), rowToObject(row))
|
|
266
|
+
}
|
|
267
|
+
return result
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async listLinksBatch(params: {
|
|
271
|
+
projectId: string
|
|
272
|
+
direction?: LinkDirection
|
|
273
|
+
items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
|
|
274
|
+
}): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
|
|
275
|
+
const sql = this.sql
|
|
276
|
+
const source = this.source
|
|
277
|
+
const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
|
|
278
|
+
if (params.items.length === 0) return result
|
|
279
|
+
|
|
280
|
+
const direction = params.direction ?? "outgoing"
|
|
281
|
+
const outgoing =
|
|
282
|
+
'stored.source_type_id = requested."objectTypeId" AND stored.source_id = requested."objectId"'
|
|
283
|
+
const incoming =
|
|
284
|
+
'stored.target_type_id = requested."objectTypeId" AND stored.target_id = requested."objectId"'
|
|
285
|
+
const items = params.items.map((item, batchIndex) => ({ ...item, batchIndex }))
|
|
286
|
+
const requestedSql = `
|
|
287
|
+
jsonb_to_recordset(?::text::jsonb)
|
|
288
|
+
AS requested(
|
|
289
|
+
"objectTypeId" text,
|
|
290
|
+
"objectId" text,
|
|
291
|
+
"linkId" text,
|
|
292
|
+
"batchIndex" integer
|
|
293
|
+
)
|
|
294
|
+
`
|
|
295
|
+
const table = source.linksTable
|
|
296
|
+
const selectedSql =
|
|
297
|
+
direction === "both"
|
|
298
|
+
? `
|
|
299
|
+
SELECT stored.*, requested."batchIndex" AS _batch_index
|
|
300
|
+
FROM ${requestedSql}
|
|
301
|
+
CROSS JOIN LATERAL (
|
|
302
|
+
SELECT outgoing_link.*
|
|
303
|
+
FROM ${table} AS outgoing_link
|
|
304
|
+
WHERE outgoing_link.project_id = ?
|
|
305
|
+
AND outgoing_link.source_type_id = requested."objectTypeId"
|
|
306
|
+
AND outgoing_link.source_id = requested."objectId"
|
|
307
|
+
AND outgoing_link.link_id = requested."linkId"
|
|
308
|
+
|
|
309
|
+
UNION
|
|
310
|
+
|
|
311
|
+
SELECT incoming_link.*
|
|
312
|
+
FROM ${table} AS incoming_link
|
|
313
|
+
WHERE incoming_link.project_id = ?
|
|
314
|
+
AND incoming_link.target_type_id = requested."objectTypeId"
|
|
315
|
+
AND incoming_link.target_id = requested."objectId"
|
|
316
|
+
AND incoming_link.link_id = requested."linkId"
|
|
317
|
+
) AS stored
|
|
318
|
+
`
|
|
319
|
+
: `
|
|
320
|
+
SELECT stored.*, requested."batchIndex" AS _batch_index
|
|
321
|
+
FROM ${requestedSql}
|
|
322
|
+
JOIN ${table} AS stored
|
|
323
|
+
ON ${direction === "incoming" ? incoming : outgoing}
|
|
324
|
+
AND stored.link_id = requested."linkId"
|
|
325
|
+
WHERE stored.project_id = ?
|
|
326
|
+
`
|
|
327
|
+
const statement = compilePgObjectStatement(
|
|
328
|
+
`
|
|
329
|
+
${selectedSql}
|
|
330
|
+
ORDER BY
|
|
331
|
+
_batch_index,
|
|
332
|
+
source_type_id COLLATE "C",
|
|
333
|
+
source_id COLLATE "C",
|
|
334
|
+
link_id COLLATE "C",
|
|
335
|
+
target_type_id COLLATE "C",
|
|
336
|
+
target_id COLLATE "C"
|
|
337
|
+
`,
|
|
338
|
+
direction === "both"
|
|
339
|
+
? [JSON.stringify(items), params.projectId, params.projectId]
|
|
340
|
+
: [JSON.stringify(items), params.projectId],
|
|
341
|
+
source
|
|
342
|
+
)
|
|
343
|
+
const rows = await sql.unsafe<LinkBatchDatabaseRow[]>(
|
|
344
|
+
statement.sql,
|
|
345
|
+
statement.args as SqlParameter[]
|
|
346
|
+
)
|
|
347
|
+
const rowsByIndex = params.items.map(() => new Map<string, ObjectLinkRow>())
|
|
348
|
+
for (const row of rows) {
|
|
349
|
+
const link = rowToLink(row)
|
|
350
|
+
rowsByIndex[row._batch_index]?.set(linkIdentity(link), link)
|
|
351
|
+
}
|
|
352
|
+
for (const [index, item] of params.items.entries()) {
|
|
353
|
+
const links = [...(rowsByIndex[index]?.values() ?? [])]
|
|
354
|
+
if (links.length > 0) {
|
|
355
|
+
result.set(linkBatchKey(item.objectTypeId, item.objectId, item.linkId), links)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return result
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
|
|
362
|
+
const sql = this.sql
|
|
363
|
+
const source = this.source
|
|
364
|
+
if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
|
|
365
|
+
return { links: [], hasMore: false }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const requestedJson = JSON.stringify(params.objectRefs)
|
|
369
|
+
const args: unknown[] =
|
|
370
|
+
params.direction === "both"
|
|
371
|
+
? [requestedJson, params.projectId, params.projectId]
|
|
372
|
+
: [requestedJson, params.projectId]
|
|
373
|
+
const addArg = (value: unknown): string => {
|
|
374
|
+
args.push(value)
|
|
375
|
+
return "?"
|
|
376
|
+
}
|
|
377
|
+
const requestedSql = `
|
|
378
|
+
jsonb_to_recordset(?::text::jsonb)
|
|
379
|
+
AS requested("objectTypeId" text, "primaryId" text)
|
|
380
|
+
`
|
|
381
|
+
const table = source.linksTable
|
|
382
|
+
const incidentSql =
|
|
383
|
+
params.direction === "both"
|
|
384
|
+
? `
|
|
385
|
+
SELECT DISTINCT link.*
|
|
386
|
+
FROM ${requestedSql}
|
|
387
|
+
CROSS JOIN LATERAL (
|
|
388
|
+
SELECT outgoing_link.*
|
|
389
|
+
FROM ${table} AS outgoing_link
|
|
390
|
+
WHERE outgoing_link.project_id = ?
|
|
391
|
+
AND outgoing_link.source_type_id = requested."objectTypeId"
|
|
392
|
+
AND outgoing_link.source_id = requested."primaryId"
|
|
393
|
+
|
|
394
|
+
UNION
|
|
395
|
+
|
|
396
|
+
SELECT incoming_link.*
|
|
397
|
+
FROM ${table} AS incoming_link
|
|
398
|
+
WHERE incoming_link.project_id = ?
|
|
399
|
+
AND incoming_link.target_type_id = requested."objectTypeId"
|
|
400
|
+
AND incoming_link.target_id = requested."primaryId"
|
|
401
|
+
) AS link
|
|
402
|
+
`
|
|
403
|
+
: `
|
|
404
|
+
SELECT DISTINCT link.*
|
|
405
|
+
FROM ${requestedSql}
|
|
406
|
+
JOIN ${table} AS link
|
|
407
|
+
ON requested."objectTypeId" = link.${params.direction === "incoming" ? "target_type_id" : "source_type_id"}
|
|
408
|
+
AND requested."primaryId" = link.${params.direction === "incoming" ? "target_id" : "source_id"}
|
|
409
|
+
WHERE link.project_id = ?
|
|
410
|
+
`
|
|
411
|
+
|
|
412
|
+
const predicates: string[] = []
|
|
413
|
+
if (params.linkId !== undefined) {
|
|
414
|
+
predicates.push(`link_id = ${addArg(params.linkId)}::text`)
|
|
415
|
+
}
|
|
416
|
+
if (params.endpointObjectTypeIds !== undefined) {
|
|
417
|
+
const allowedTypes = JSON.stringify([...new Set(params.endpointObjectTypeIds)])
|
|
418
|
+
predicates.push(
|
|
419
|
+
`source_type_id IN (SELECT jsonb_array_elements_text(${addArg(allowedTypes)}::text::jsonb))`,
|
|
420
|
+
`target_type_id IN (SELECT jsonb_array_elements_text(${addArg(allowedTypes)}::text::jsonb))`
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
if (params.after) {
|
|
424
|
+
const cursor = params.after.map((value) => `${addArg(value)}::text COLLATE "C"`)
|
|
425
|
+
predicates.push(
|
|
426
|
+
`(
|
|
427
|
+
source_type_id COLLATE "C",
|
|
428
|
+
source_id COLLATE "C",
|
|
429
|
+
link_id COLLATE "C",
|
|
430
|
+
target_type_id COLLATE "C",
|
|
431
|
+
target_id COLLATE "C"
|
|
432
|
+
) > (${cursor.join(", ")})`
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
const limit = addArg(params.limit + 1)
|
|
436
|
+
const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : ""
|
|
437
|
+
const statement = compilePgObjectStatement(
|
|
438
|
+
`
|
|
439
|
+
SELECT *
|
|
440
|
+
FROM (${incidentSql}) AS incident
|
|
441
|
+
${whereSql}
|
|
442
|
+
ORDER BY
|
|
443
|
+
source_type_id COLLATE "C",
|
|
444
|
+
source_id COLLATE "C",
|
|
445
|
+
link_id COLLATE "C",
|
|
446
|
+
target_type_id COLLATE "C",
|
|
447
|
+
target_id COLLATE "C"
|
|
448
|
+
LIMIT ${limit}
|
|
449
|
+
`,
|
|
450
|
+
args,
|
|
451
|
+
source
|
|
452
|
+
)
|
|
453
|
+
const rows = await sql.unsafe<LinkDatabaseRow[]>(
|
|
454
|
+
statement.sql,
|
|
455
|
+
statement.args as SqlParameter[]
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
links: rows.slice(0, params.limit).map((row) => rowToLink(row)),
|
|
460
|
+
hasMore: rows.length > params.limit,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async list(
|
|
465
|
+
params: Parameters<ObjectReadStorage["list"]>[0]
|
|
466
|
+
): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
|
|
467
|
+
const sql = this.sql
|
|
468
|
+
const source = this.source
|
|
469
|
+
const queryOffset = params.offset ?? 0
|
|
470
|
+
const limit = params.limit ?? 50
|
|
471
|
+
const orderBy = params.orderBy ?? "updatedAt"
|
|
472
|
+
const order = params.order ?? "desc"
|
|
473
|
+
const orderColumn =
|
|
474
|
+
orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
|
|
475
|
+
const filters = ["project_id = ?"]
|
|
476
|
+
const args: unknown[] = [params.projectId]
|
|
477
|
+
if (typeof params.objectTypeId === "string") {
|
|
478
|
+
filters.push("object_type_id = ?")
|
|
479
|
+
args.push(params.objectTypeId)
|
|
480
|
+
} else if (params.objectTypeId !== undefined) {
|
|
481
|
+
filters.push("object_type_id IN (SELECT jsonb_array_elements_text(?::text::jsonb))")
|
|
482
|
+
args.push(JSON.stringify(params.objectTypeId))
|
|
483
|
+
}
|
|
484
|
+
if (params.primaryIdPrefix) {
|
|
485
|
+
filters.push("primary_id LIKE ?")
|
|
486
|
+
args.push(`${params.primaryIdPrefix}%`)
|
|
487
|
+
}
|
|
488
|
+
if (params.primaryIdSuffix) {
|
|
489
|
+
filters.push("primary_id LIKE ?")
|
|
490
|
+
args.push(`%${params.primaryIdSuffix}`)
|
|
491
|
+
}
|
|
492
|
+
if (params.updatedAfter) {
|
|
493
|
+
filters.push("updated_at >= ?")
|
|
494
|
+
args.push(params.updatedAfter)
|
|
495
|
+
}
|
|
496
|
+
if (params.updatedBefore) {
|
|
497
|
+
filters.push("updated_at <= ?")
|
|
498
|
+
args.push(params.updatedBefore)
|
|
499
|
+
}
|
|
500
|
+
if (params.createdAfter) {
|
|
501
|
+
filters.push("created_at >= ?")
|
|
502
|
+
args.push(params.createdAfter)
|
|
503
|
+
}
|
|
504
|
+
if (params.createdBefore) {
|
|
505
|
+
filters.push("created_at <= ?")
|
|
506
|
+
args.push(params.createdBefore)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const objectsTable = source.objectsTable
|
|
510
|
+
const whereSql = filters.join(" AND ")
|
|
511
|
+
const countStatement = compilePgObjectStatement(
|
|
512
|
+
`SELECT COUNT(*)::int AS total FROM ${objectsTable} WHERE ${whereSql}`,
|
|
513
|
+
args,
|
|
514
|
+
source
|
|
515
|
+
)
|
|
516
|
+
const [countResult] = await sql.unsafe<{ total: number }[]>(
|
|
517
|
+
countStatement.sql,
|
|
518
|
+
countStatement.args as SqlParameter[]
|
|
519
|
+
)
|
|
520
|
+
const total = countResult?.total ?? 0
|
|
521
|
+
|
|
522
|
+
if (limit === 0) return { objects: [], hasMore: queryOffset < total, total }
|
|
523
|
+
|
|
524
|
+
const fetchLimit = limit + 1
|
|
525
|
+
const rowsStatement = compilePgObjectStatement(
|
|
526
|
+
`
|
|
527
|
+
SELECT * FROM ${objectsTable}
|
|
528
|
+
WHERE ${whereSql}
|
|
529
|
+
ORDER BY ${orderColumn} ${order === "asc" ? "ASC" : "DESC"}
|
|
530
|
+
LIMIT ? OFFSET ?
|
|
531
|
+
`,
|
|
532
|
+
[...args, fetchLimit, queryOffset],
|
|
533
|
+
source
|
|
534
|
+
)
|
|
535
|
+
const rows = await sql.unsafe<ObjectDatabaseRow[]>(
|
|
536
|
+
rowsStatement.sql,
|
|
537
|
+
rowsStatement.args as SqlParameter[]
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
const hasMore = rows.length > limit
|
|
541
|
+
const objects = rows.slice(0, limit).map((row) => rowToObject(row))
|
|
542
|
+
|
|
543
|
+
return { objects, hasMore, total }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function readTotal(sql: SQLClient, compiled: CompiledPgObjectQuery): Promise<number> {
|
|
548
|
+
const [row] = await sql.unsafe<
|
|
549
|
+
{
|
|
550
|
+
total: string | number | bigint
|
|
551
|
+
}[]
|
|
552
|
+
>(compiled.totalSql, compiled.totalArgs as SqlParameter[])
|
|
553
|
+
return Number(row?.total ?? 0)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function readFacetBuckets(
|
|
557
|
+
sql: SQLClient,
|
|
558
|
+
compiled: { sql: string; args: readonly unknown[] }
|
|
559
|
+
): Promise<{ value: unknown; count: number }[]> {
|
|
560
|
+
const rows = await sql.unsafe<FacetDatabaseRow[]>(compiled.sql, [
|
|
561
|
+
...compiled.args,
|
|
562
|
+
] as SqlParameter[])
|
|
563
|
+
|
|
564
|
+
return rows.map((row) => ({
|
|
565
|
+
value: pgFacetValue(row.value_type, row.value_text),
|
|
566
|
+
count: Number(row.count),
|
|
567
|
+
}))
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function pgFacetValue(valueType: string | null, valueText: string | null): unknown {
|
|
571
|
+
switch (valueType) {
|
|
572
|
+
case "string":
|
|
573
|
+
return valueText ?? ""
|
|
574
|
+
case "number":
|
|
575
|
+
return valueText === null ? null : Number(valueText)
|
|
576
|
+
case "boolean":
|
|
577
|
+
return valueText === "true"
|
|
578
|
+
case "null":
|
|
579
|
+
return null
|
|
580
|
+
case "array":
|
|
581
|
+
case "object":
|
|
582
|
+
return valueText === null ? null : JSON.parse(valueText)
|
|
583
|
+
default:
|
|
584
|
+
return valueText
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function stripOuterRowShape(query: ObjectQuery): ObjectQuery {
|
|
589
|
+
switch (query.kind) {
|
|
590
|
+
case "limit":
|
|
591
|
+
case "page":
|
|
592
|
+
case "project":
|
|
593
|
+
case "sort":
|
|
594
|
+
return stripOuterRowShape(query.input)
|
|
595
|
+
default:
|
|
596
|
+
return query
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export function assertLinkQueryLimit(limit: number): void {
|
|
601
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
602
|
+
throw new Error("Object link query limit must be a positive safe integer.")
|
|
603
|
+
}
|
|
604
|
+
}
|