@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
@@ -1,3 +1,4 @@
1
+ import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
1
2
  import type { ReadonlyJsonObject } from "@sixb/core/storage"
2
3
  import {
3
4
  type AiModelCallUsageInput,
@@ -36,7 +37,9 @@ export class PgAiUsageStorage implements AiUsageStorage {
36
37
  attempt,
37
38
  call_id,
38
39
  provider_id,
40
+ provider_ids,
39
41
  requested_model_id,
42
+ requested_reasoning,
40
43
  response_model_id,
41
44
  response_id,
42
45
  input_tokens,
@@ -58,7 +61,13 @@ export class PgAiUsageStorage implements AiUsageStorage {
58
61
  ${record.attempt},
59
62
  ${record.callId},
60
63
  ${record.providerId},
64
+ ${record.providerIds === undefined ? null : JSON.stringify(record.providerIds)}::jsonb,
61
65
  ${record.requestedModelId},
66
+ ${
67
+ record.requestedReasoning === undefined
68
+ ? null
69
+ : JSON.stringify(record.requestedReasoning)
70
+ }::text::jsonb,
62
71
  ${record.responseModelId ?? null},
63
72
  ${record.responseId},
64
73
  ${record.usage.inputTokens ?? null},
@@ -181,7 +190,17 @@ export class PgAiUsageStorage implements AiUsageStorage {
181
190
  callId: row.call_id,
182
191
  requesterGroupIds: groupRows.map((group) => group.group_id),
183
192
  providerId: row.provider_id,
193
+ ...(row.provider_ids === null
194
+ ? {}
195
+ : {
196
+ providerIds: normalizeModelProviderIds(
197
+ typeof row.provider_ids === "string" ? JSON.parse(row.provider_ids) : row.provider_ids
198
+ ),
199
+ }),
184
200
  requestedModelId: row.requested_model_id,
201
+ ...(row.requested_reasoning === null
202
+ ? {}
203
+ : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
185
204
  ...(row.response_model_id === null ? {} : { responseModelId: row.response_model_id }),
186
205
  responseId: row.response_id,
187
206
  usage: {
@@ -237,7 +256,9 @@ interface AiUsageRow {
237
256
  readonly attempt: number | string
238
257
  readonly call_id: string
239
258
  readonly provider_id: string
259
+ readonly provider_ids: Record<string, unknown> | string | null
240
260
  readonly requested_model_id: string
261
+ readonly requested_reasoning: string | Record<string, unknown> | null
241
262
  readonly response_model_id: string | null
242
263
  readonly response_id: string
243
264
  readonly input_tokens: number | string | null
@@ -298,6 +319,19 @@ function rawUsageFromRow(value: Exclude<AiUsageRow["raw_usage"], null>): Readonl
298
319
  return typeof value === "string" ? (JSON.parse(value) as ReadonlyJsonObject) : value
299
320
  }
300
321
 
322
+ export function requestedReasoningFromRow(
323
+ value: Exclude<AiUsageRow["requested_reasoning"], null>
324
+ ): AiModelCallUsageRecord["requestedReasoning"] {
325
+ const parsed: unknown =
326
+ typeof value === "string" && (value.startsWith('"') || value.startsWith("{"))
327
+ ? JSON.parse(value)
328
+ : value
329
+ if (!isModelReasoning(parsed)) {
330
+ throw new Error("[SixbPg] Stored AI usage requested reasoning is invalid.")
331
+ }
332
+ return parsed
333
+ }
334
+
301
335
  function assertNonBlankProjectId(projectId: string): void {
302
336
  if (typeof projectId !== "string" || projectId.trim().length === 0) {
303
337
  throw new TypeError("[Sixb] AI usage projectId must be nonblank.")
@@ -5,7 +5,12 @@ import {
5
5
  normalizeExecutionRecord,
6
6
  validateExecutionRecordReferences,
7
7
  } from "@sixb/core/internal/execution-storage"
8
- import type { CreateExecutionInput, ExecutionRecord, ExecutionStorage } from "@sixb/core/storage"
8
+ import type {
9
+ CreateExecutionInput,
10
+ ExecutionRecord,
11
+ ExecutionStorage,
12
+ ShareSessionStorage,
13
+ } from "@sixb/core/storage"
9
14
  import { ExecutionStorageError } from "@sixb/core/storage"
10
15
  import type { PgAuthStorage } from "./auth-storage"
11
16
  import { isUniqueViolation } from "./storage-errors"
@@ -14,7 +19,8 @@ import type { PgStoreClient } from "./transactions"
14
19
  export class PgExecutionStorage implements ExecutionStorage {
15
20
  constructor(
16
21
  private readonly sql: PgStoreClient,
17
- private readonly auth: PgAuthStorage
22
+ private readonly auth: PgAuthStorage,
23
+ private readonly shareSessions: Pick<ShareSessionStorage, "getById">
18
24
  ) {}
19
25
 
20
26
  async create(input: CreateExecutionInput): Promise<ExecutionRecord> {
@@ -22,6 +28,7 @@ export class PgExecutionStorage implements ExecutionStorage {
22
28
  await validateExecutionRecordReferences(record, {
23
29
  auth: this.auth,
24
30
  getExecution: (params) => this.getById(params),
31
+ getShareSession: (params) => this.shareSessions.getById(params),
25
32
  })
26
33
  const row = executionRecordToStorageRow(record)
27
34
 
@@ -46,6 +53,9 @@ export class PgExecutionStorage implements ExecutionStorage {
46
53
  authority_primitive_kind,
47
54
  authority_primitive_id,
48
55
  authority_kernel_operation,
56
+ authority_delegation_kind,
57
+ authority_delegation_id,
58
+ authority_delegation_session_id,
49
59
  created_at
50
60
  ) VALUES (
51
61
  ${row.projectId},
@@ -66,6 +76,9 @@ export class PgExecutionStorage implements ExecutionStorage {
66
76
  ${row.authorityPrimitiveKind},
67
77
  ${row.authorityPrimitiveId},
68
78
  ${row.authorityKernelOperation},
79
+ ${row.authorityDelegationKind},
80
+ ${row.authorityDelegationId},
81
+ ${row.authorityDelegationSessionId},
69
82
  ${row.createdAt}
70
83
  )
71
84
  RETURNING *
@@ -120,6 +133,9 @@ function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
120
133
  authorityPrimitiveKind: row.authority_primitive_kind,
121
134
  authorityPrimitiveId: row.authority_primitive_id,
122
135
  authorityKernelOperation: row.authority_kernel_operation,
136
+ authorityDelegationKind: row.authority_delegation_kind,
137
+ authorityDelegationId: row.authority_delegation_id,
138
+ authorityDelegationSessionId: row.authority_delegation_session_id,
123
139
  createdAt: new Date(row.created_at),
124
140
  }
125
141
  }
@@ -143,5 +159,8 @@ interface PgExecutionRow {
143
159
  readonly authority_primitive_kind: ExecutionStorageRow["authorityPrimitiveKind"]
144
160
  readonly authority_primitive_id: string | null
145
161
  readonly authority_kernel_operation: ExecutionStorageRow["authorityKernelOperation"]
162
+ readonly authority_delegation_kind: ExecutionStorageRow["authorityDelegationKind"]
163
+ readonly authority_delegation_id: string | null
164
+ readonly authority_delegation_session_id: string | null
146
165
  readonly created_at: Date | string
147
166
  }
@@ -407,16 +407,16 @@ export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorag
407
407
  projectId: input.projectId,
408
408
  executionId: input.executionId,
409
409
  nodeRunId: input.nodeRunId,
410
- agentId: input.agentId,
410
+ actorId: input.actorId,
411
411
  workflowExecutionId: parent.execution_id,
412
412
  })
413
413
 
414
414
  try {
415
415
  const [row] = await this.sql<WorkflowAgentNodeRunDatabaseRow[]>`
416
416
  INSERT INTO workflow_agent_node_runs (
417
- project_id, node_run_id, execution_id, agent_id, status, prompt, attempt, created_at
417
+ project_id, node_run_id, execution_id, actor_id, status, prompt, attempt, created_at
418
418
  ) VALUES (
419
- ${input.projectId}, ${input.nodeRunId}, ${input.executionId}, ${input.agentId},
419
+ ${input.projectId}, ${input.nodeRunId}, ${input.executionId}, ${input.actorId},
420
420
  ${"queued"}, ${input.prompt}, ${0}, ${input.createdAt ?? new Date()}
421
421
  ) RETURNING *
422
422
  `
@@ -554,9 +554,9 @@ export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorag
554
554
  const whereClauses = ["project_id = $1"]
555
555
  const params: SqlParameter[] = [input.projectId]
556
556
  let index = 2
557
- if (input.agentId) {
558
- whereClauses.push(`agent_id = $${index++}`)
559
- params.push(input.agentId)
557
+ if (input.actorId) {
558
+ whereClauses.push(`actor_id = $${index++}`)
559
+ params.push(input.actorId)
560
560
  }
561
561
  index = appendRunListFilters(whereClauses, params, index, input)
562
562
  const result = await queryRunList<WorkflowAgentNodeRunDatabaseRow>({
@@ -910,7 +910,7 @@ interface WorkflowAgentNodeRunDatabaseRow {
910
910
  project_id: string
911
911
  node_run_id: string
912
912
  execution_id: string
913
- agent_id: string
913
+ actor_id: string
914
914
  status: WorkflowAgentNodeRunRecord["status"]
915
915
  prompt: string
916
916
  model_id: string | null
@@ -968,7 +968,7 @@ function rowToWorkflowAgentNodeRunRecord(
968
968
  projectId: row.project_id,
969
969
  nodeRunId: row.node_run_id,
970
970
  executionId: row.execution_id,
971
- agentId: row.agent_id,
971
+ actorId: row.actor_id,
972
972
  status: row.status,
973
973
  prompt: row.prompt,
974
974
  ...(row.model_id ? { modelId: row.model_id } : {}),
@@ -0,0 +1,236 @@
1
+ import {
2
+ normalizeGetShareGrantByIdInput,
3
+ normalizeListShareGrantsInput,
4
+ normalizeRevokeShareGrantInput,
5
+ normalizeShareGrantCreate,
6
+ parseShareGrantRecord,
7
+ } from "@sixb/core/internal/share-grant-storage-provider"
8
+ import type {
9
+ CreateShareGrantInput,
10
+ GetShareGrantByIdInput,
11
+ ListShareGrantsInput,
12
+ ListShareGrantsResult,
13
+ RevokeShareGrantInput,
14
+ ShareGrantRecord,
15
+ ShareGrantStorage,
16
+ } from "@sixb/core/storage"
17
+ import { ShareGrantStorageError } from "@sixb/core/storage"
18
+ import type { SqlParameter } from "./pg-client"
19
+ import { isUniqueViolation } from "./storage-errors"
20
+ import type { PgStoreClient } from "./transactions"
21
+
22
+ export class PgShareGrantStorage implements ShareGrantStorage {
23
+ constructor(private readonly sql: PgStoreClient) {}
24
+
25
+ async create(input: CreateShareGrantInput): Promise<ShareGrantRecord> {
26
+ const record = normalizeShareGrantCreate(input)
27
+ try {
28
+ const [row] = await this.sql<PgShareGrantRow[]>`
29
+ INSERT INTO share_grants (
30
+ project_id,
31
+ id,
32
+ definition_id,
33
+ target_object_type_id,
34
+ target_primary_id,
35
+ issued_by_type,
36
+ issued_by_id,
37
+ authority_version,
38
+ authority_snapshot,
39
+ authority_digest,
40
+ token_hash,
41
+ destination_path,
42
+ created_at,
43
+ expires_at
44
+ ) VALUES (
45
+ ${record.projectId},
46
+ ${record.id},
47
+ ${record.definitionId},
48
+ ${record.target.objectTypeId},
49
+ ${record.target.primaryId},
50
+ ${record.issuedBy.type},
51
+ ${record.issuedBy.id},
52
+ ${record.authoritySnapshot.version},
53
+ ${JSON.stringify(record.authoritySnapshot)}::text::jsonb,
54
+ ${record.authorityDigest},
55
+ ${record.tokenHash},
56
+ ${record.destinationPath},
57
+ ${record.createdAt},
58
+ ${record.expiresAt}
59
+ )
60
+ RETURNING *
61
+ `
62
+ if (!row) {
63
+ throw invalidRecord(`Share grant '${record.id}' disappeared after create.`)
64
+ }
65
+ return recordFromRow(row)
66
+ } catch (error) {
67
+ if (isUniqueViolation(error)) {
68
+ throw new ShareGrantStorageError(
69
+ "duplicate",
70
+ `[SixbPg] Share grant '${record.id}' conflicts with an existing record.`,
71
+ { cause: error }
72
+ )
73
+ }
74
+ throw error
75
+ }
76
+ }
77
+
78
+ async getById(input: GetShareGrantByIdInput): Promise<ShareGrantRecord | null> {
79
+ const normalized = normalizeGetShareGrantByIdInput(input)
80
+ const [row] = await this.sql<PgShareGrantRow[]>`
81
+ SELECT *
82
+ FROM share_grants
83
+ WHERE project_id = ${normalized.projectId} AND id = ${normalized.id}
84
+ `
85
+ return row ? recordFromRow(row) : null
86
+ }
87
+
88
+ async list(input: ListShareGrantsInput): Promise<ListShareGrantsResult> {
89
+ const normalized = normalizeListShareGrantsInput(input)
90
+ const where = ["project_id = $1"]
91
+ const params: SqlParameter[] = [normalized.projectId]
92
+ let parameter = 2
93
+
94
+ if (normalized.definitionId !== undefined) {
95
+ where.push(`definition_id = $${parameter++}`)
96
+ params.push(normalized.definitionId)
97
+ }
98
+ if (normalized.target !== undefined) {
99
+ where.push(`target_object_type_id = $${parameter++}`, `target_primary_id = $${parameter++}`)
100
+ params.push(normalized.target.objectTypeId, normalized.target.primaryId)
101
+ }
102
+ if (!normalized.includeRevoked) where.push("revoked_at IS NULL")
103
+ if (!normalized.includeExpired) {
104
+ where.push(`expires_at > $${parameter++}`)
105
+ params.push(normalized.now)
106
+ }
107
+
108
+ const predicate = where.join(" AND ")
109
+ const [totalRow] = await this.sql.unsafe<PgShareGrantCountRow[]>(
110
+ `SELECT COUNT(*)::bigint AS total FROM share_grants WHERE ${predicate}`,
111
+ params
112
+ )
113
+ const total = parseCount(totalRow?.total)
114
+
115
+ const listParams = [...params, normalized.limit, normalized.offset]
116
+ const rows = await this.sql.unsafe<PgShareGrantRow[]>(
117
+ `
118
+ SELECT *
119
+ FROM share_grants
120
+ WHERE ${predicate}
121
+ ORDER BY created_at DESC, id DESC
122
+ LIMIT $${parameter++} OFFSET $${parameter}
123
+ `,
124
+ listParams
125
+ )
126
+ const grants = rows.map(recordFromRow)
127
+ return {
128
+ grants,
129
+ total,
130
+ hasMore: normalized.offset + grants.length < total,
131
+ }
132
+ }
133
+
134
+ async revoke(input: RevokeShareGrantInput): Promise<ShareGrantRecord | null> {
135
+ const normalized = normalizeRevokeShareGrantInput(input)
136
+ const current = await this.getById({ projectId: normalized.projectId, id: normalized.id })
137
+ if (!current) return null
138
+ if (current.revokedAt) return current
139
+
140
+ const revocation = normalizeRevokeShareGrantInput(normalized, current.createdAt)
141
+ const [updated] = await this.sql<PgShareGrantRow[]>`
142
+ UPDATE share_grants
143
+ SET revoked_at = ${revocation.revokedAt},
144
+ revoked_by_type = ${revocation.revokedBy.type},
145
+ revoked_by_id = ${revocation.revokedBy.id}
146
+ WHERE project_id = ${revocation.projectId}
147
+ AND id = ${revocation.id}
148
+ AND revoked_at IS NULL
149
+ RETURNING *
150
+ `
151
+ if (updated) return recordFromRow(updated)
152
+
153
+ return this.getById({ projectId: revocation.projectId, id: revocation.id })
154
+ }
155
+ }
156
+
157
+ interface PgShareGrantRow {
158
+ readonly project_id: unknown
159
+ readonly id: unknown
160
+ readonly definition_id: unknown
161
+ readonly target_object_type_id: unknown
162
+ readonly target_primary_id: unknown
163
+ readonly issued_by_type: unknown
164
+ readonly issued_by_id: unknown
165
+ readonly authority_version: unknown
166
+ readonly authority_snapshot: unknown
167
+ readonly authority_digest: unknown
168
+ readonly token_hash: unknown
169
+ readonly destination_path: unknown
170
+ readonly created_at: unknown
171
+ readonly expires_at: unknown
172
+ readonly revoked_at: unknown
173
+ readonly revoked_by_type: unknown
174
+ readonly revoked_by_id: unknown
175
+ }
176
+
177
+ interface PgShareGrantCountRow {
178
+ readonly total: unknown
179
+ }
180
+
181
+ function recordFromRow(row: PgShareGrantRow): ShareGrantRecord {
182
+ const authoritySnapshot = parseJsonColumn(row.authority_snapshot)
183
+ if (
184
+ !isRecord(authoritySnapshot) ||
185
+ !Number.isSafeInteger(row.authority_version) ||
186
+ authoritySnapshot.version !== row.authority_version
187
+ ) {
188
+ throw invalidRecord("Share authority version does not match its snapshot.")
189
+ }
190
+
191
+ return parseShareGrantRecord({
192
+ id: row.id,
193
+ projectId: row.project_id,
194
+ definitionId: row.definition_id,
195
+ target: {
196
+ objectTypeId: row.target_object_type_id,
197
+ primaryId: row.target_primary_id,
198
+ },
199
+ issuedBy: { type: row.issued_by_type, id: row.issued_by_id },
200
+ authoritySnapshot,
201
+ authorityDigest: row.authority_digest,
202
+ tokenHash: row.token_hash,
203
+ destinationPath: row.destination_path,
204
+ createdAt: row.created_at,
205
+ expiresAt: row.expires_at,
206
+ ...(row.revoked_at === null ? {} : { revokedAt: row.revoked_at }),
207
+ ...(row.revoked_by_type === null && row.revoked_by_id === null
208
+ ? {}
209
+ : { revokedBy: { type: row.revoked_by_type, id: row.revoked_by_id } }),
210
+ })
211
+ }
212
+
213
+ function parseJsonColumn(value: unknown): unknown {
214
+ if (typeof value !== "string") return value
215
+ try {
216
+ return JSON.parse(value)
217
+ } catch (error) {
218
+ throw invalidRecord("Share authority snapshot is not valid JSON.", error)
219
+ }
220
+ }
221
+
222
+ function parseCount(value: unknown): number {
223
+ const count = Number(value)
224
+ if (!Number.isSafeInteger(count) || count < 0) {
225
+ throw invalidRecord("Share grant count is not a non-negative safe integer.")
226
+ }
227
+ return count
228
+ }
229
+
230
+ function isRecord(value: unknown): value is Record<string, unknown> {
231
+ return typeof value === "object" && value !== null && !Array.isArray(value)
232
+ }
233
+
234
+ function invalidRecord(message: string, cause?: unknown): ShareGrantStorageError {
235
+ return new ShareGrantStorageError("invalid_record", `[SixbPg] ${message}`, { cause })
236
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ normalizeGetShareSessionByIdInput,
3
+ normalizeRenewShareSessionIfValidInput,
4
+ normalizeRevokeShareSessionInput,
5
+ normalizeShareSessionCreate,
6
+ parseShareSessionRecord,
7
+ } from "@sixb/core/internal/share-session-storage-provider"
8
+ import type {
9
+ CreateShareSessionInput,
10
+ GetShareSessionByIdInput,
11
+ RenewShareSessionIfValidInput,
12
+ RevokeShareSessionInput,
13
+ ShareSessionRecord,
14
+ ShareSessionStorage,
15
+ } from "@sixb/core/storage"
16
+ import { ShareSessionStorageError } from "@sixb/core/storage"
17
+ import { isForeignKeyViolation, isUniqueViolation } from "./storage-errors"
18
+ import type { PgStoreClient } from "./transactions"
19
+
20
+ export class PgShareSessionStorage implements ShareSessionStorage {
21
+ constructor(private readonly sql: PgStoreClient) {}
22
+
23
+ async create(input: CreateShareSessionInput): Promise<ShareSessionRecord> {
24
+ const record = normalizeShareSessionCreate(input)
25
+ try {
26
+ const [row] = await this.sql<PgShareSessionRow[]>`
27
+ INSERT INTO share_sessions (
28
+ project_id, id, grant_id, token_hash, created_at, expires_at, absolute_expires_at
29
+ ) VALUES (
30
+ ${record.projectId}, ${record.id}, ${record.grantId}, ${record.tokenHash},
31
+ ${record.createdAt}, ${record.expiresAt}, ${record.absoluteExpiresAt}
32
+ )
33
+ RETURNING *
34
+ `
35
+ if (!row) throw invalidRecord(`Share session '${record.id}' disappeared after create.`)
36
+ return recordFromRow(row)
37
+ } catch (error) {
38
+ if (isUniqueViolation(error)) {
39
+ throw new ShareSessionStorageError(
40
+ "duplicate",
41
+ `[SixbPg] Share session '${record.id}' conflicts with an existing record.`,
42
+ { cause: error }
43
+ )
44
+ }
45
+ if (isForeignKeyViolation(error)) {
46
+ throw new ShareSessionStorageError(
47
+ "invalid_input",
48
+ `[SixbPg] Share grant '${record.grantId}' does not exist in project '${record.projectId}'.`,
49
+ { cause: error }
50
+ )
51
+ }
52
+ throw error
53
+ }
54
+ }
55
+
56
+ async getById(input: GetShareSessionByIdInput): Promise<ShareSessionRecord | null> {
57
+ const normalized = normalizeGetShareSessionByIdInput(input)
58
+ const [row] = await this.sql<PgShareSessionRow[]>`
59
+ SELECT * FROM share_sessions
60
+ WHERE project_id = ${normalized.projectId} AND id = ${normalized.id}
61
+ `
62
+ return row ? recordFromRow(row) : null
63
+ }
64
+
65
+ async renewIfValid(input: RenewShareSessionIfValidInput): Promise<ShareSessionRecord | null> {
66
+ const normalized = normalizeRenewShareSessionIfValidInput(input)
67
+ const [row] = await this.sql<PgShareSessionRow[]>`
68
+ UPDATE share_sessions
69
+ SET expires_at = LEAST(absolute_expires_at, GREATEST(expires_at, ${normalized.expiresAt}))
70
+ WHERE project_id = ${normalized.projectId}
71
+ AND id = ${normalized.id}
72
+ AND grant_id = ${normalized.grantId}
73
+ AND token_hash = ${normalized.tokenHash}
74
+ AND revoked_at IS NULL
75
+ AND created_at <= ${normalized.now}
76
+ AND expires_at > ${normalized.now}
77
+ AND absolute_expires_at > ${normalized.now}
78
+ RETURNING *
79
+ `
80
+ return row ? recordFromRow(row) : null
81
+ }
82
+
83
+ async revoke(input: RevokeShareSessionInput): Promise<ShareSessionRecord | null> {
84
+ const normalized = normalizeRevokeShareSessionInput(input)
85
+ const current = await this.getById(normalized)
86
+ if (!current || current.revokedAt) return current
87
+ const revocation = normalizeRevokeShareSessionInput(normalized, current.createdAt)
88
+ const [updated] = await this.sql<PgShareSessionRow[]>`
89
+ UPDATE share_sessions
90
+ SET revoked_at = ${revocation.revokedAt}
91
+ WHERE project_id = ${revocation.projectId}
92
+ AND id = ${revocation.id}
93
+ AND revoked_at IS NULL
94
+ RETURNING *
95
+ `
96
+ if (updated) return recordFromRow(updated)
97
+ return this.getById({ projectId: revocation.projectId, id: revocation.id })
98
+ }
99
+ }
100
+
101
+ interface PgShareSessionRow {
102
+ readonly project_id: unknown
103
+ readonly id: unknown
104
+ readonly grant_id: unknown
105
+ readonly token_hash: unknown
106
+ readonly created_at: unknown
107
+ readonly expires_at: unknown
108
+ readonly absolute_expires_at: unknown
109
+ readonly revoked_at: unknown
110
+ }
111
+
112
+ function recordFromRow(row: PgShareSessionRow): ShareSessionRecord {
113
+ return parseShareSessionRecord({
114
+ projectId: row.project_id,
115
+ id: row.id,
116
+ grantId: row.grant_id,
117
+ tokenHash: row.token_hash,
118
+ createdAt: row.created_at,
119
+ expiresAt: row.expires_at,
120
+ absoluteExpiresAt: row.absolute_expires_at,
121
+ ...(row.revoked_at === null ? {} : { revokedAt: row.revoked_at }),
122
+ })
123
+ }
124
+
125
+ function invalidRecord(message: string, cause?: unknown): ShareSessionStorageError {
126
+ return new ShareSessionStorageError("invalid_record", `[SixbPg] ${message}`, { cause })
127
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  /** PostgreSQL SQLSTATE for a unique-constraint violation. */
7
7
  const UNIQUE_VIOLATION = "23505"
8
+ const FOREIGN_KEY_VIOLATION = "23503"
8
9
 
9
10
  /**
10
11
  * PostgreSQL SQLSTATEs in class 40 (transaction rollback) that are transient and safe to retry by
@@ -46,6 +47,11 @@ export function isUniqueViolation(error: unknown): boolean {
46
47
  return pgErrorCode(error) === UNIQUE_VIOLATION
47
48
  }
48
49
 
50
+ /** True when the error is a PostgreSQL foreign-key violation (SQLSTATE 23503). */
51
+ export function isForeignKeyViolation(error: unknown): boolean {
52
+ return pgErrorCode(error) === FOREIGN_KEY_VIOLATION
53
+ }
54
+
49
55
  /**
50
56
  * True when the error is a transient transaction-rollback conflict that is safe to retry by
51
57
  * replaying the transaction — a serialization failure (SQLSTATE 40001) or a deadlock (40P01).
@@ -4,10 +4,14 @@ import type { SQL, SQLClient } from "./pg-client"
4
4
  export type PgStoreClient = SQL | SQLClient
5
5
 
6
6
  export interface RunPgTransactionOptions {
7
- /** Open the transaction at `SERIALIZABLE`; omit for the server default (`READ COMMITTED`). */
8
- readonly isolation?: "serializable"
7
+ /** Open the transaction at an explicit isolation; omit for the server default. */
8
+ readonly isolation?: "repeatableRead" | "serializable"
9
9
  }
10
10
 
11
+ type PgTransactionIsolation = "unverifiedDefault" | "repeatableRead" | "serializable"
12
+
13
+ const activePgTransactions = new WeakMap<PgStoreClient, PgTransactionIsolation>()
14
+
11
15
  export async function runPgTransaction<T>(
12
16
  sql: PgStoreClient,
13
17
  run: (tx: SQLClient) => Promise<T>,
@@ -24,10 +28,47 @@ export async function runPgTransaction<T>(
24
28
  // The isolation level is folded into `BEGIN` (`BEGIN ISOLATION LEVEL SERIALIZABLE`) rather than
25
29
  // a separate `SET TRANSACTION` statement: one round-trip instead of two, and the level is
26
30
  // guaranteed to take effect before any data statement of the transaction runs.
27
- if (options.isolation === "serializable") {
28
- return sql.begin("isolation level serializable", run) as Promise<T>
31
+ const isolation = options.isolation ?? "unverifiedDefault"
32
+ const trackedRun = async (tx: SQLClient): Promise<T> => {
33
+ activePgTransactions.set(tx, isolation)
34
+ try {
35
+ return await run(tx)
36
+ } finally {
37
+ activePgTransactions.delete(tx)
38
+ }
39
+ }
40
+
41
+ if (isolation === "serializable") {
42
+ return sql.begin("isolation level serializable", trackedRun) as Promise<T>
43
+ }
44
+ if (isolation === "repeatableRead") {
45
+ return sql.begin("isolation level repeatable read", trackedRun) as Promise<T>
46
+ }
47
+ return sql.begin(trackedRun) as Promise<T>
48
+ }
49
+
50
+ /**
51
+ * Run a multi-statement read against one proven PostgreSQL snapshot.
52
+ *
53
+ * A provider-owned repeatable-read or serializable transaction may be reused. An external
54
+ * transaction client is rejected because postgres.js does not expose its current isolation.
55
+ */
56
+ export async function runPgRepeatableReadTransaction<T>(
57
+ sql: PgStoreClient,
58
+ run: (tx: SQLClient) => Promise<T>
59
+ ): Promise<T> {
60
+ if (canStartPgTransaction(sql)) {
61
+ return runPgTransaction(sql, run, { isolation: "repeatableRead" })
29
62
  }
30
- return sql.begin(run) as Promise<T>
63
+
64
+ const isolation = activePgTransactions.get(sql)
65
+ if (isolation === "repeatableRead" || isolation === "serializable") {
66
+ return run(sql)
67
+ }
68
+
69
+ throw new Error(
70
+ '[SixbPg] Selected object reads cannot join an unverified PostgreSQL transaction. Use storage.transaction(..., { isolation: "serializable" }) when reading through tx.objects.'
71
+ )
31
72
  }
32
73
 
33
74
  export function authLockKey(kind: string, ...parts: readonly string[]): string {
@@ -1 +0,0 @@
1
- {"version":3,"file":"pg-object-query-compiler.d.ts","sourceRoot":"","sources":["../src/pg-object-query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAKhB,KAAK,eAAe,EACrB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,OAAO,EAAE,CAAA;IACpB,KAAK,EAAE,aAAa,CAAA;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAA;IACnC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAClD,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,CAAC,EAAE,CAAA;IAC7C,aAAa,CAAC,IAAI,EAAE,SAAS,oBAAoB,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAC3F;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,EAAE,CAAA;CAChB;AAED,UAAU,oBAAoB;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;CACnC;AAgBD,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACrC,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,KAAK,kBAAkB,GACnB;IACE,IAAI,EAAE,UAAU,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;IACzB,UAAU,CAAC,EAAE,eAAe,CAAA;CAC7B,GACD;IACE,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,gBAAgB,GAAG,YAAY,CAAA;IACvC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;CAC1B,CAAA;AAmBL,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,EAClB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,qBAAqB,CAevB;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,GACjB,qBAAqB,CASvB;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,GACjB,qBAAqB,CAUvB;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,GACZ,oBAAoB,CAkBtB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"pg-object-storage.d.ts","sourceRoot":"","sources":["../src/pg-object-storage.ts"],"names":[],"mappings":"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"}