@sixb/pg 0.1.6 → 0.1.8

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.
Files changed (88) hide show
  1. package/dist/agents/rows.d.ts +8 -5
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/auth-storage/auth-storage.d.ts +4 -1
  9. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  10. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  11. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  12. package/dist/auth-storage/rows.d.ts +19 -1
  13. package/dist/auth-storage/rows.d.ts.map +1 -1
  14. package/dist/auth-storage/shared.d.ts +1 -1
  15. package/dist/auth-storage/shared.d.ts.map +1 -1
  16. package/dist/index.d.ts +6 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8327 -5838
  19. package/dist/index.js.map +28 -19
  20. package/dist/migrations.d.ts.map +1 -1
  21. package/dist/objects/index.d.ts +2 -0
  22. package/dist/objects/index.d.ts.map +1 -0
  23. package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
  24. package/dist/objects/query-compiler.d.ts.map +1 -0
  25. package/dist/objects/read-scope.d.ts +17 -0
  26. package/dist/objects/read-scope.d.ts.map +1 -0
  27. package/dist/objects/reader.d.ts +50 -0
  28. package/dist/objects/reader.d.ts.map +1 -0
  29. package/dist/objects/rows.d.ts +47 -0
  30. package/dist/objects/rows.d.ts.map +1 -0
  31. package/dist/objects/selected-reader.d.ts +8 -0
  32. package/dist/objects/selected-reader.d.ts.map +1 -0
  33. package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
  34. package/dist/objects/storage.d.ts.map +1 -0
  35. package/dist/pg-ai-cost-storage.d.ts +2 -1
  36. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  37. package/dist/pg-ai-limit-storage.d.ts +34 -0
  38. package/dist/pg-ai-limit-storage.d.ts.map +1 -0
  39. package/dist/pg-ai-usage-storage.d.ts +28 -0
  40. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  41. package/dist/pg-execution-storage.d.ts +3 -2
  42. package/dist/pg-execution-storage.d.ts.map +1 -1
  43. package/dist/share-grant-storage.d.ts +11 -0
  44. package/dist/share-grant-storage.d.ts.map +1 -0
  45. package/dist/share-session-storage.d.ts +11 -0
  46. package/dist/share-session-storage.d.ts.map +1 -0
  47. package/dist/storage-errors.d.ts +2 -0
  48. package/dist/storage-errors.d.ts.map +1 -1
  49. package/dist/transactions.d.ts +9 -2
  50. package/dist/transactions.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/agents/rows.ts +38 -9
  53. package/src/agents/runs.ts +148 -11
  54. package/src/agents/threads.ts +1 -17
  55. package/src/ai-cost-groups.ts +102 -0
  56. package/src/auth-storage/auth-storage.ts +84 -0
  57. package/src/auth-storage/device-authorizations.ts +131 -0
  58. package/src/auth-storage/rows.ts +41 -0
  59. package/src/auth-storage/shared.ts +1 -0
  60. package/src/index.ts +24 -4
  61. package/src/migrations/029-model-accounting.sql +24 -0
  62. package/src/migrations/030-ai-usage-limits.sql +94 -0
  63. package/src/migrations/031-subagent-runs.sql +97 -0
  64. package/src/migrations/032-conversation-run-spec.sql +23 -0
  65. package/src/migrations/033-retire-agent-definitions.sql +28 -0
  66. package/src/migrations/034-device-authorizations.sql +24 -0
  67. package/src/migrations/035-share-grants.sql +57 -0
  68. package/src/migrations/036-share-sessions.sql +173 -0
  69. package/src/migrations.ts +22 -0
  70. package/src/objects/index.ts +1 -0
  71. package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
  72. package/src/objects/read-scope.ts +324 -0
  73. package/src/objects/reader.ts +604 -0
  74. package/src/objects/rows.ts +146 -0
  75. package/src/objects/selected-reader.ts +118 -0
  76. package/src/objects/storage.ts +305 -0
  77. package/src/pg-ai-cost-storage.ts +71 -24
  78. package/src/pg-ai-limit-storage.ts +901 -0
  79. package/src/pg-ai-usage-storage.ts +34 -0
  80. package/src/pg-execution-storage.ts +21 -2
  81. package/src/pg-workflow-run-storage.ts +8 -8
  82. package/src/share-grant-storage.ts +236 -0
  83. package/src/share-session-storage.ts +127 -0
  84. package/src/storage-errors.ts +6 -0
  85. package/src/transactions.ts +46 -5
  86. package/dist/pg-object-query-compiler.d.ts.map +0 -1
  87. package/dist/pg-object-storage.d.ts.map +0 -1
  88. 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
+ }