@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.
Files changed (68) hide show
  1. package/dist/agents/rows.d.ts +0 -1
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts.map +1 -1
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +7596 -6416
  7. package/dist/index.js.map +25 -19
  8. package/dist/migrations.d.ts.map +1 -1
  9. package/dist/objects/index.d.ts +2 -0
  10. package/dist/objects/index.d.ts.map +1 -0
  11. package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
  12. package/dist/objects/query-compiler.d.ts.map +1 -0
  13. package/dist/objects/read-scope.d.ts +17 -0
  14. package/dist/objects/read-scope.d.ts.map +1 -0
  15. package/dist/objects/reader.d.ts +50 -0
  16. package/dist/objects/reader.d.ts.map +1 -0
  17. package/dist/objects/rows.d.ts +47 -0
  18. package/dist/objects/rows.d.ts.map +1 -0
  19. package/dist/objects/selected-reader.d.ts +8 -0
  20. package/dist/objects/selected-reader.d.ts.map +1 -0
  21. package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
  22. package/dist/objects/storage.d.ts.map +1 -0
  23. package/dist/pg-action-run-storage.d.ts.map +1 -1
  24. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  25. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  26. package/dist/pg-execution-storage.d.ts +3 -2
  27. package/dist/pg-execution-storage.d.ts.map +1 -1
  28. package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
  29. package/dist/pg-sync-run-storage.d.ts.map +1 -1
  30. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  31. package/dist/share-grant-storage.d.ts +11 -0
  32. package/dist/share-grant-storage.d.ts.map +1 -0
  33. package/dist/share-session-storage.d.ts +11 -0
  34. package/dist/share-session-storage.d.ts.map +1 -0
  35. package/dist/storage-errors.d.ts +2 -0
  36. package/dist/storage-errors.d.ts.map +1 -1
  37. package/dist/transactions.d.ts +9 -2
  38. package/dist/transactions.d.ts.map +1 -1
  39. package/package.json +4 -4
  40. package/src/agents/rows.ts +0 -5
  41. package/src/agents/runs.ts +1 -6
  42. package/src/ai-cost-groups.ts +3 -0
  43. package/src/index.ts +20 -5
  44. package/src/migrations/035-share-grants.sql +57 -0
  45. package/src/migrations/036-share-sessions.sql +173 -0
  46. package/src/migrations/037-execution-requester-groups.sql +21 -0
  47. package/src/migrations.ts +9 -0
  48. package/src/objects/index.ts +1 -0
  49. package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
  50. package/src/objects/read-scope.ts +324 -0
  51. package/src/objects/reader.ts +604 -0
  52. package/src/objects/rows.ts +146 -0
  53. package/src/objects/selected-reader.ts +118 -0
  54. package/src/objects/storage.ts +305 -0
  55. package/src/pg-action-run-storage.ts +6 -7
  56. package/src/pg-ai-cost-storage.ts +11 -2
  57. package/src/pg-ai-usage-storage.ts +1 -2
  58. package/src/pg-execution-storage.ts +27 -6
  59. package/src/pg-pipeline-run-storage.ts +2 -5
  60. package/src/pg-sync-run-storage.ts +2 -5
  61. package/src/pg-workflow-run-storage.ts +0 -5
  62. package/src/share-grant-storage.ts +236 -0
  63. package/src/share-session-storage.ts +127 -0
  64. package/src/storage-errors.ts +6 -0
  65. package/src/transactions.ts +46 -5
  66. package/dist/pg-object-query-compiler.d.ts.map +0 -1
  67. package/dist/pg-object-storage.d.ts.map +0 -1
  68. package/src/pg-object-storage.ts +0 -734
@@ -2,10 +2,14 @@ import {
2
2
  type ExecutionStorageRow,
3
3
  executionRecordFromStorageRow,
4
4
  executionRecordToStorageRow,
5
- normalizeExecutionRecord,
6
- validateExecutionRecordReferences,
5
+ prepareExecutionRecord,
7
6
  } from "@sixb/core/internal/execution-storage"
8
- import type { CreateExecutionInput, ExecutionRecord, ExecutionStorage } from "@sixb/core/storage"
7
+ import type {
8
+ CreateExecutionInput,
9
+ ExecutionRecord,
10
+ ExecutionStorage,
11
+ ShareSessionStorage,
12
+ } from "@sixb/core/storage"
9
13
  import { ExecutionStorageError } from "@sixb/core/storage"
10
14
  import type { PgAuthStorage } from "./auth-storage"
11
15
  import { isUniqueViolation } from "./storage-errors"
@@ -14,14 +18,15 @@ import type { PgStoreClient } from "./transactions"
14
18
  export class PgExecutionStorage implements ExecutionStorage {
15
19
  constructor(
16
20
  private readonly sql: PgStoreClient,
17
- private readonly auth: PgAuthStorage
21
+ private readonly auth: PgAuthStorage,
22
+ private readonly shareSessions: Pick<ShareSessionStorage, "getById">
18
23
  ) {}
19
24
 
20
25
  async create(input: CreateExecutionInput): Promise<ExecutionRecord> {
21
- const record = normalizeExecutionRecord(input)
22
- await validateExecutionRecordReferences(record, {
26
+ const record = await prepareExecutionRecord(input, {
23
27
  auth: this.auth,
24
28
  getExecution: (params) => this.getById(params),
29
+ getShareSession: (params) => this.shareSessions.getById(params),
25
30
  })
26
31
  const row = executionRecordToStorageRow(record)
27
32
 
@@ -36,6 +41,7 @@ export class PgExecutionStorage implements ExecutionStorage {
36
41
  source_id,
37
42
  requested_by_user_id,
38
43
  requested_by_service_account_id,
44
+ requester_group_ids,
39
45
  correlation_id,
40
46
  parent_execution_id,
41
47
  authority_kind,
@@ -46,6 +52,9 @@ export class PgExecutionStorage implements ExecutionStorage {
46
52
  authority_primitive_kind,
47
53
  authority_primitive_id,
48
54
  authority_kernel_operation,
55
+ authority_delegation_kind,
56
+ authority_delegation_id,
57
+ authority_delegation_session_id,
49
58
  created_at
50
59
  ) VALUES (
51
60
  ${row.projectId},
@@ -56,6 +65,7 @@ export class PgExecutionStorage implements ExecutionStorage {
56
65
  ${row.sourceId},
57
66
  ${row.requestedByUserId},
58
67
  ${row.requestedByServiceAccountId},
68
+ ${JSON.stringify(row.requesterGroupIds)}::text::jsonb,
59
69
  ${row.correlationId},
60
70
  ${row.parentExecutionId},
61
71
  ${row.authorityKind},
@@ -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 *
@@ -110,6 +123,7 @@ function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
110
123
  sourceId: row.source_id,
111
124
  requestedByUserId: row.requested_by_user_id,
112
125
  requestedByServiceAccountId: row.requested_by_service_account_id,
126
+ requesterGroupIds: row.requester_group_ids,
113
127
  correlationId: row.correlation_id,
114
128
  parentExecutionId: row.parent_execution_id,
115
129
  authorityKind: row.authority_kind,
@@ -120,6 +134,9 @@ function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
120
134
  authorityPrimitiveKind: row.authority_primitive_kind,
121
135
  authorityPrimitiveId: row.authority_primitive_id,
122
136
  authorityKernelOperation: row.authority_kernel_operation,
137
+ authorityDelegationKind: row.authority_delegation_kind,
138
+ authorityDelegationId: row.authority_delegation_id,
139
+ authorityDelegationSessionId: row.authority_delegation_session_id,
123
140
  createdAt: new Date(row.created_at),
124
141
  }
125
142
  }
@@ -133,6 +150,7 @@ interface PgExecutionRow {
133
150
  readonly source_id: string
134
151
  readonly requested_by_user_id: string | null
135
152
  readonly requested_by_service_account_id: string | null
153
+ readonly requester_group_ids: string[]
136
154
  readonly correlation_id: string
137
155
  readonly parent_execution_id: string | null
138
156
  readonly authority_kind: ExecutionStorageRow["authorityKind"]
@@ -143,5 +161,8 @@ interface PgExecutionRow {
143
161
  readonly authority_primitive_kind: ExecutionStorageRow["authorityPrimitiveKind"]
144
162
  readonly authority_primitive_id: string | null
145
163
  readonly authority_kernel_operation: ExecutionStorageRow["authorityKernelOperation"]
164
+ readonly authority_delegation_kind: ExecutionStorageRow["authorityDelegationKind"]
165
+ readonly authority_delegation_id: string | null
166
+ readonly authority_delegation_session_id: string | null
146
167
  readonly created_at: Date | string
147
168
  }
@@ -1,6 +1,7 @@
1
1
  import type { JsonValue } from "@sixb/core"
2
2
  import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
3
3
  import { assertPipelineRunExecution } from "@sixb/core/internal/pipeline-run-storage-provider"
4
+ import { canRequeuePipelineRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
4
5
  import type { DatasetVersionRef } from "@sixb/core/lake-storage"
5
6
  import type {
6
7
  ExecutionStorage,
@@ -19,11 +20,7 @@ import type {
19
20
  StartPipelineRunInput,
20
21
  StartPipelineStepRunInput,
21
22
  } from "@sixb/core/storage"
22
- import {
23
- canRequeuePipelineRunAfterEnqueueFailure,
24
- PIPELINE_RUN_FAILURE_CODES,
25
- PipelineRunError,
26
- } from "@sixb/core/storage"
23
+ import { PIPELINE_RUN_FAILURE_CODES, PipelineRunError } from "@sixb/core/storage"
27
24
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
28
25
  import type { SqlParameter } from "./pg-client"
29
26
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
@@ -1,5 +1,6 @@
1
1
  import type { JsonValue } from "@sixb/core"
2
2
  import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
3
+ import { canRequeueSyncRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
3
4
  import { assertSyncRunExecution } from "@sixb/core/internal/sync-run-storage-provider"
4
5
  import type {
5
6
  ExecutionStorage,
@@ -13,11 +14,7 @@ import type {
13
14
  SyncRunRecord,
14
15
  SyncRunStorage,
15
16
  } from "@sixb/core/storage"
16
- import {
17
- canRequeueSyncRunAfterEnqueueFailure,
18
- SYNC_RUN_FAILURE_CODES,
19
- SyncRunError,
20
- } from "@sixb/core/storage"
17
+ import { SYNC_RUN_FAILURE_CODES, SyncRunError } from "@sixb/core/storage"
21
18
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
22
19
  import type { SqlParameter } from "./pg-client"
23
20
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
@@ -1,5 +1,4 @@
1
1
  import type { JsonValue } from "@sixb/core"
2
- import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
3
2
  import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
4
3
  import {
5
4
  assertWorkflowAgentNodeRunExecution,
@@ -83,7 +82,6 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
83
82
  input,
84
83
  queued_at,
85
84
  started_at,
86
- requester_group_ids,
87
85
  attempt
88
86
  ) VALUES (
89
87
  ${input.projectId},
@@ -94,7 +92,6 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
94
92
  ${serializeRecord(input.input)}::text::jsonb,
95
93
  ${queuedAt},
96
94
  ${queuedAt},
97
- ${JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds))}::text::jsonb,
98
95
  ${0}
99
96
  )
100
97
  RETURNING *
@@ -822,7 +819,6 @@ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord
822
819
  startedAt: new Date(row.started_at),
823
820
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
824
821
  error: row.error === null ? undefined : parseSixbFailure(row.error, WORKFLOW_RUN_FAILURE_CODES),
825
- requesterGroupIds: parseJson(row.requester_group_ids),
826
822
  attempt: Number(row.attempt),
827
823
  ...(row.execution_token && row.execution_queue_lease_expires_at
828
824
  ? {
@@ -883,7 +879,6 @@ interface WorkflowRunDatabaseRow {
883
879
  started_at: Date | string
884
880
  finished_at: Date | string | null
885
881
  error: JsonValue | null
886
- requester_group_ids: string[] | string
887
882
  attempt: number | string
888
883
  execution_token: string | null
889
884
  execution_queue_lease_expires_at: Date | string | null
@@ -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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"}