@sixb/pg 0.1.12 → 0.1.13

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 (38) hide show
  1. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  2. package/dist/auth-storage/device-authorizations.d.ts.map +1 -1
  3. package/dist/auth-storage/rows.d.ts +6 -3
  4. package/dist/auth-storage/rows.d.ts.map +1 -1
  5. package/dist/auth-storage/sessions.d.ts +8 -1
  6. package/dist/auth-storage/sessions.d.ts.map +1 -1
  7. package/dist/auth-storage/shared.d.ts.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +366 -70
  11. package/dist/index.js.map +16 -15
  12. package/dist/migrations.d.ts.map +1 -1
  13. package/dist/objects/storage.d.ts.map +1 -1
  14. package/dist/ontology-storage/materializations.d.ts +0 -2
  15. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  16. package/dist/ontology-storage/shared.d.ts +2 -1
  17. package/dist/ontology-storage/shared.d.ts.map +1 -1
  18. package/dist/pg-file-upload-session-storage.d.ts +6 -0
  19. package/dist/pg-file-upload-session-storage.d.ts.map +1 -0
  20. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth-storage/access-tokens.ts +10 -14
  23. package/src/auth-storage/auth-storage.ts +4 -6
  24. package/src/auth-storage/device-authorizations.ts +3 -4
  25. package/src/auth-storage/rows.ts +17 -7
  26. package/src/auth-storage/sessions.ts +82 -4
  27. package/src/auth-storage/shared.ts +8 -2
  28. package/src/index.ts +5 -0
  29. package/src/migrations/046-workflow-intervention-principals.sql +15 -0
  30. package/src/migrations/047-ontology-commit-attribution.sql +50 -0
  31. package/src/migrations/048-file-upload-sessions.sql +34 -0
  32. package/src/migrations/049-native-sessions.sql +19 -0
  33. package/src/migrations.ts +12 -0
  34. package/src/objects/storage.ts +2 -0
  35. package/src/ontology-storage/materializations.ts +15 -58
  36. package/src/ontology-storage/shared.ts +7 -3
  37. package/src/pg-file-upload-session-storage.ts +143 -0
  38. package/src/pg-workflow-intervention-storage.ts +11 -15
@@ -1,8 +1,4 @@
1
- import type {
2
- EffectiveChangeCounts,
3
- ExpectedLinkRevision,
4
- ExpectedObjectRevision,
5
- } from "@sixb/core/internal/materialization"
1
+ import type { EffectiveChangeCounts } from "@sixb/core/internal/materialization"
6
2
  import {
7
3
  assertPinnedDatasetWatermark,
8
4
  linkRefKey,
@@ -16,6 +12,9 @@ import {
16
12
  telemetryPointSortKey,
17
13
  } from "@sixb/core/internal/materialization"
18
14
  import {
15
+ assertExpectedLinkRevision,
16
+ assertExpectedLinkScopeRevision,
17
+ assertExpectedObjectRevision,
19
18
  assertMaterializationFinalizationCorrelation,
20
19
  assertMaterializationHeader,
21
20
  assertMaterializationLaneCompletion,
@@ -126,23 +125,21 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
126
125
  true
127
126
  )
128
127
  for (const expected of input.expected.objects) {
129
- this.assertObject(objectRevisions.get(objectRefKey(expected.ref)) ?? null, expected)
128
+ assertExpectedObjectRevision(
129
+ objectRevisions.get(objectRefKey(expected.ref)) ?? null,
130
+ expected
131
+ )
130
132
  }
131
133
  const linkRevisions = await reader.effectiveLinkLastCommits(
132
134
  input.expected.links.map((expected) => expected.ref),
133
135
  true
134
136
  )
135
137
  for (const expected of input.expected.links) {
136
- this.assertLink(linkRevisions.get(linkRefKey(expected.ref)), expected)
138
+ assertExpectedLinkRevision(linkRevisions.get(linkRefKey(expected.ref)), expected)
137
139
  }
138
140
  const linkScopeRevisions = await reader.linkScopeRevisions(input.expected.linkScopes)
139
141
  for (const [index, expected] of input.expected.linkScopes.entries()) {
140
- if (linkScopeRevisions[index]?.fingerprint !== expected.fingerprint) {
141
- throw new MaterializationConflictError(
142
- "effective-state",
143
- `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`
144
- )
145
- }
142
+ assertExpectedLinkScopeRevision(linkScopeRevisions[index]?.fingerprint, expected)
146
143
  }
147
144
  const points = await reader.exactPoints(input.expected.points, true)
148
145
  const pointRevisions = new Map(
@@ -514,48 +511,6 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
514
511
  }
515
512
  }
516
513
 
517
- private assertObject(
518
- row: { readonly version: number; readonly lastCommitId: string | null } | null,
519
- expected: ExpectedObjectRevision
520
- ): void {
521
- if (!expected.exists) {
522
- if (row) {
523
- throw new MaterializationConflictError(
524
- "effective-state",
525
- `Expected object ${objectRefKey(expected.ref)} to be absent.`
526
- )
527
- }
528
- return
529
- }
530
- if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
531
- throw new MaterializationConflictError(
532
- "effective-state",
533
- `Expected object ${objectRefKey(expected.ref)} changed.`
534
- )
535
- }
536
- }
537
-
538
- private assertLink(
539
- lastCommitId: string | null | undefined,
540
- expected: ExpectedLinkRevision
541
- ): void {
542
- if (!expected.exists) {
543
- if (lastCommitId !== undefined) {
544
- throw new MaterializationConflictError(
545
- "effective-state",
546
- `Expected link ${linkRefKey(expected.ref)} to be absent.`
547
- )
548
- }
549
- return
550
- }
551
- if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
552
- throw new MaterializationConflictError(
553
- "effective-state",
554
- `Expected link ${linkRefKey(expected.ref)} changed.`
555
- )
556
- }
557
- }
558
-
559
514
  private async assertFinalization(
560
515
  session: PgMaterializationSessionState,
561
516
  input: FinalizeMaterializationInput
@@ -873,18 +828,20 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
873
828
  ): Promise<OntologyCommitRecord> {
874
829
  const { commit } = header
875
830
  const origin = originColumns(commit.origin)
876
- const actor = commit.actor === undefined ? null : jsonParameter(this.sql, commit.actor)
831
+ const requestedBy =
832
+ commit.requestedBy === undefined ? null : jsonParameter(this.sql, commit.requestedBy)
877
833
  const rows = await this.sql<PgOntologyCommitRow[]>`
878
834
  INSERT INTO ontology_commits (
879
835
  project_id, id, idempotency_key, request_hash, execution_id,
880
- origin_kind, origin_run_id, origin_batch_ordinal, origin, actor,
836
+ origin_kind, origin_run_id, origin_batch_ordinal, origin, requested_by, executor,
881
837
  ontology_revision, projection_revision, ownership_hash,
882
838
  intent, result, committed_at
883
839
  ) VALUES (
884
840
  ${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
885
841
  ${commit.executionId},
886
842
  ${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
887
- ${jsonParameter(this.sql, commit.origin)}, ${actor}, ${commit.ontologyRevision},
843
+ ${jsonParameter(this.sql, commit.origin)}, ${requestedBy},
844
+ ${jsonParameter(this.sql, commit.executor)}, ${commit.ontologyRevision},
888
845
  ${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
889
846
  ${jsonParameter(this.sql, commit.intent)},
890
847
  ${jsonParameter(this.sql, input.finalization.result)}, ${commit.committedAt}
@@ -39,7 +39,8 @@ export interface PgOntologyCommitRow {
39
39
  readonly origin_run_id: string | null
40
40
  readonly origin_batch_ordinal: number | string | null
41
41
  readonly origin: unknown
42
- readonly actor: unknown | null
42
+ readonly requested_by: unknown | null
43
+ readonly executor: unknown
43
44
  readonly ontology_revision: string
44
45
  readonly projection_revision: string | null
45
46
  readonly ownership_hash: string | null
@@ -187,11 +188,14 @@ export function commitRecord(row: PgOntologyCommitRow): OntologyCommitRecord {
187
188
  requestHash: row.request_hash,
188
189
  executionId: row.execution_id,
189
190
  origin: structuredClone(row.origin) as OntologyCommitWrite["origin"],
190
- ...(row.actor === null
191
+ ...(row.requested_by === null
191
192
  ? {}
192
193
  : {
193
- actor: structuredClone(row.actor) as NonNullable<OntologyCommitWrite["actor"]>,
194
+ requestedBy: structuredClone(row.requested_by) as NonNullable<
195
+ OntologyCommitWrite["requestedBy"]
196
+ >,
194
197
  }),
198
+ executor: structuredClone(row.executor) as OntologyCommitWrite["executor"],
195
199
  ontologyRevision: row.ontology_revision,
196
200
  ...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
197
201
  ...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
@@ -0,0 +1,143 @@
1
+ import {
2
+ DurableFileUploadSessions,
3
+ type FileUploadSessionPersistence,
4
+ type FileUploadSessionPersistenceBackend,
5
+ fileUploadSessionReapAt,
6
+ parseFileUploadSessionRow,
7
+ } from "@sixb/core/internal/file-upload-session-storage-provider"
8
+ import type { FileUploadSession, ListAbandonedFileUploadSessionsInput } from "@sixb/core/storage"
9
+ import { type PgStoreClient, runPgTransaction } from "./transactions"
10
+
11
+ export class PgFileUploadSessionStorage extends DurableFileUploadSessions {
12
+ constructor(sql: PgStoreClient) {
13
+ super(new PgFileUploadSessionBackend(sql))
14
+ }
15
+ }
16
+
17
+ class PgFileUploadSessionBackend implements FileUploadSessionPersistenceBackend {
18
+ constructor(private readonly sql: PgStoreClient) {}
19
+
20
+ read<T>(run: (persistence: FileUploadSessionPersistence) => Promise<T>): Promise<T> {
21
+ return run(new PgFileUploadSessionPersistence(this.sql, false))
22
+ }
23
+
24
+ transaction<T>(run: (persistence: FileUploadSessionPersistence) => Promise<T>): Promise<T> {
25
+ return runPgTransaction(this.sql, (tx) => run(new PgFileUploadSessionPersistence(tx, true)))
26
+ }
27
+ }
28
+
29
+ class PgFileUploadSessionPersistence implements FileUploadSessionPersistence {
30
+ constructor(
31
+ private readonly sql: PgStoreClient,
32
+ private readonly locking: boolean
33
+ ) {}
34
+
35
+ async now(): Promise<Date> {
36
+ const [row] = await this.sql<{ readonly now: Date }[]>`SELECT clock_timestamp() AS now`
37
+ if (!row) throw new Error("[SixbPg] Database clock returned no row.")
38
+ return new Date(row.now)
39
+ }
40
+
41
+ async insert(session: FileUploadSession): Promise<boolean> {
42
+ // A unique violation would abort the surrounding transaction; report the conflict instead.
43
+ const rows = await this.sql`
44
+ INSERT INTO file_upload_sessions (
45
+ id, project_id, owner_principal_type, owner_principal_id, strategy, status, file_name,
46
+ media_type, logical_path, expected_size_bytes, expected_digest, provider_upload,
47
+ signed_parts, file_ref, created_at, expires_at, completed_at, aborted_at, reap_at
48
+ ) VALUES (
49
+ ${session.id}, ${session.projectId}, ${session.principal.type}, ${session.principal.id},
50
+ ${session.strategy}, ${session.status}, ${session.fileName ?? null}, ${session.mediaType ?? null},
51
+ ${session.logicalPath ?? null}, ${session.expectedSizeBytes ?? null},
52
+ ${session.expectedDigest ?? null}, ${jsonText(session.providerUpload)}::text::jsonb,
53
+ ${JSON.stringify(session.signedParts)}::text::jsonb, ${jsonText(session.fileRef)}::text::jsonb,
54
+ ${session.createdAt}, ${session.expiresAt}, ${session.completedAt ?? null},
55
+ ${session.abortedAt ?? null}, ${fileUploadSessionReapAt(session)}
56
+ )
57
+ ON CONFLICT (id) DO NOTHING
58
+ RETURNING id
59
+ `
60
+ return rows.length === 1
61
+ }
62
+
63
+ async get(uploadId: string): Promise<FileUploadSession | null> {
64
+ const [row] = this.locking
65
+ ? await this.sql<PgFileUploadSessionRow[]>`
66
+ SELECT * FROM file_upload_sessions WHERE id = ${uploadId} FOR UPDATE
67
+ `
68
+ : await this.sql<PgFileUploadSessionRow[]>`
69
+ SELECT * FROM file_upload_sessions WHERE id = ${uploadId}
70
+ `
71
+ return row ? sessionFromRow(row) : null
72
+ }
73
+
74
+ async update(session: FileUploadSession): Promise<void> {
75
+ await this.sql`
76
+ UPDATE file_upload_sessions
77
+ SET status = ${session.status},
78
+ provider_upload = ${jsonText(session.providerUpload)}::text::jsonb,
79
+ signed_parts = ${JSON.stringify(session.signedParts)}::text::jsonb,
80
+ file_ref = ${jsonText(session.fileRef)}::text::jsonb,
81
+ created_at = ${session.createdAt},
82
+ expires_at = ${session.expiresAt},
83
+ completed_at = ${session.completedAt ?? null},
84
+ aborted_at = ${session.abortedAt ?? null},
85
+ reap_at = ${fileUploadSessionReapAt(session)}
86
+ WHERE id = ${session.id}
87
+ `
88
+ }
89
+
90
+ async listAbandoned(
91
+ input: ListAbandonedFileUploadSessionsInput
92
+ ): Promise<readonly FileUploadSession[]> {
93
+ const rows = await this.sql<PgFileUploadSessionRow[]>`
94
+ SELECT * FROM file_upload_sessions
95
+ WHERE project_id = ${input.projectId} AND status = 'pending'
96
+ AND provider_upload IS NOT NULL AND expires_at <= ${input.now}
97
+ ORDER BY expires_at, id COLLATE "C"
98
+ LIMIT ${input.limit}
99
+ `
100
+ return rows.map(sessionFromRow)
101
+ }
102
+
103
+ async deleteReapable(now: Date): Promise<number> {
104
+ const rows = await this.sql`
105
+ DELETE FROM file_upload_sessions
106
+ WHERE reap_at IS NOT NULL AND reap_at <= ${now}
107
+ RETURNING id
108
+ `
109
+ return rows.length
110
+ }
111
+ }
112
+
113
+ interface PgFileUploadSessionRow {
114
+ readonly [column: string]: unknown
115
+ }
116
+
117
+ function sessionFromRow(row: PgFileUploadSessionRow): FileUploadSession {
118
+ return parseFileUploadSessionRow({
119
+ id: row.id,
120
+ projectId: row.project_id,
121
+ ownerPrincipalType: row.owner_principal_type,
122
+ ownerPrincipalId: row.owner_principal_id,
123
+ strategy: row.strategy,
124
+ status: row.status,
125
+ fileName: row.file_name,
126
+ mediaType: row.media_type,
127
+ logicalPath: row.logical_path,
128
+ expectedSizeBytes: row.expected_size_bytes,
129
+ expectedDigest: row.expected_digest,
130
+ providerUpload: row.provider_upload,
131
+ signedParts: row.signed_parts,
132
+ fileRef: row.file_ref,
133
+ createdAt: row.created_at,
134
+ expiresAt: row.expires_at,
135
+ completedAt: row.completed_at,
136
+ abortedAt: row.aborted_at,
137
+ })
138
+ }
139
+
140
+ /** JSON text for a nullable jsonb column; always bind with the `::text::jsonb` double cast. */
141
+ function jsonText(value: object | undefined): string | null {
142
+ return value === undefined ? null : JSON.stringify(value)
143
+ }
@@ -5,8 +5,8 @@ import type {
5
5
  ExpireWorkflowInterventionInput,
6
6
  ListWorkflowInterventionsInput,
7
7
  ListWorkflowInterventionsResult,
8
+ Principal,
8
9
  SubmitWorkflowInterventionInput,
9
- WorkflowInterventionActor,
10
10
  WorkflowInterventionRecord,
11
11
  WorkflowInterventionStorage,
12
12
  } from "@sixb/core/storage"
@@ -78,7 +78,7 @@ export class PgWorkflowInterventionStorage implements WorkflowInterventionStorag
78
78
  SET
79
79
  status = ${"submitted"},
80
80
  submitted_at = ${input.submittedAt ?? new Date()},
81
- submitted_by = ${serializeActor(input.submittedBy)}::text::jsonb,
81
+ submitted_by = ${serializePrincipal(input.submittedBy)}::text::jsonb,
82
82
  response = ${serializeRecord(input.response)}::text::jsonb
83
83
  WHERE project_id = ${input.projectId} AND id = ${input.id}
84
84
  RETURNING *
@@ -97,7 +97,7 @@ export class PgWorkflowInterventionStorage implements WorkflowInterventionStorag
97
97
  SET
98
98
  status = ${"cancelled"},
99
99
  cancelled_at = ${input.cancelledAt ?? new Date()},
100
- cancelled_by = ${serializeActor(input.cancelledBy)}::text::jsonb
100
+ cancelled_by = ${serializePrincipal(input.cancelledBy)}::text::jsonb
101
101
  WHERE project_id = ${input.projectId} AND id = ${input.id}
102
102
  RETURNING *
103
103
  `
@@ -268,16 +268,12 @@ function parseRecord(value: WorkflowIOSnapshot | string): WorkflowIOSnapshot {
268
268
  return typeof value === "string" ? (JSON.parse(value) as WorkflowIOSnapshot) : value
269
269
  }
270
270
 
271
- function serializeActor(actor: WorkflowInterventionActor | undefined): string | null {
272
- return actor ? JSON.stringify(actor) : null
271
+ function serializePrincipal(principal: Principal | undefined): string | null {
272
+ return principal ? JSON.stringify(principal) : null
273
273
  }
274
274
 
275
- function parseActor(
276
- value: WorkflowInterventionActor | string | null
277
- ): WorkflowInterventionActor | undefined {
278
- return typeof value === "string"
279
- ? (JSON.parse(value) as WorkflowInterventionActor)
280
- : (value ?? undefined)
275
+ function parsePrincipal(value: Principal | string | null): Principal | undefined {
276
+ return typeof value === "string" ? (JSON.parse(value) as Principal) : (value ?? undefined)
281
277
  }
282
278
 
283
279
  function rowToWorkflowInterventionRecord(
@@ -299,10 +295,10 @@ function rowToWorkflowInterventionRecord(
299
295
  requestedAt: new Date(row.requested_at),
300
296
  expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
301
297
  submittedAt: row.submitted_at ? new Date(row.submitted_at) : undefined,
302
- submittedBy: parseActor(row.submitted_by),
298
+ submittedBy: parsePrincipal(row.submitted_by),
303
299
  response: row.response ? parseRecord(row.response) : undefined,
304
300
  cancelledAt: row.cancelled_at ? new Date(row.cancelled_at) : undefined,
305
- cancelledBy: parseActor(row.cancelled_by),
301
+ cancelledBy: parsePrincipal(row.cancelled_by),
306
302
  expiredAt: row.expired_at ? new Date(row.expired_at) : undefined,
307
303
  }
308
304
  }
@@ -331,9 +327,9 @@ interface WorkflowInterventionDatabaseRow {
331
327
  requested_at: Date | string
332
328
  expires_at: Date | string | null
333
329
  submitted_at: Date | string | null
334
- submitted_by: WorkflowInterventionActor | string | null
330
+ submitted_by: Principal | string | null
335
331
  response: WorkflowIOSnapshot | string | null
336
332
  cancelled_at: Date | string | null
337
- cancelled_by: WorkflowInterventionActor | string | null
333
+ cancelled_by: Principal | string | null
338
334
  expired_at: Date | string | null
339
335
  }