@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.
- package/dist/auth-storage/access-tokens.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +6 -3
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +8 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +366 -70
- package/dist/index.js.map +16 -15
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +0 -2
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +2 -1
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/pg-file-upload-session-storage.d.ts +6 -0
- package/dist/pg-file-upload-session-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/auth-storage/access-tokens.ts +10 -14
- package/src/auth-storage/auth-storage.ts +4 -6
- package/src/auth-storage/device-authorizations.ts +3 -4
- package/src/auth-storage/rows.ts +17 -7
- package/src/auth-storage/sessions.ts +82 -4
- package/src/auth-storage/shared.ts +8 -2
- package/src/index.ts +5 -0
- package/src/migrations/046-workflow-intervention-principals.sql +15 -0
- package/src/migrations/047-ontology-commit-attribution.sql +50 -0
- package/src/migrations/048-file-upload-sessions.sql +34 -0
- package/src/migrations/049-native-sessions.sql +19 -0
- package/src/migrations.ts +12 -0
- package/src/objects/storage.ts +2 -0
- package/src/ontology-storage/materializations.ts +15 -58
- package/src/ontology-storage/shared.ts +7 -3
- package/src/pg-file-upload-session-storage.ts +143 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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)}, ${
|
|
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
|
|
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.
|
|
191
|
+
...(row.requested_by === null
|
|
191
192
|
? {}
|
|
192
193
|
: {
|
|
193
|
-
|
|
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 = ${
|
|
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 = ${
|
|
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
|
|
272
|
-
return
|
|
271
|
+
function serializePrincipal(principal: Principal | undefined): string | null {
|
|
272
|
+
return principal ? JSON.stringify(principal) : null
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
function
|
|
276
|
-
value
|
|
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:
|
|
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:
|
|
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:
|
|
330
|
+
submitted_by: Principal | string | null
|
|
335
331
|
response: WorkflowIOSnapshot | string | null
|
|
336
332
|
cancelled_at: Date | string | null
|
|
337
|
-
cancelled_by:
|
|
333
|
+
cancelled_by: Principal | string | null
|
|
338
334
|
expired_at: Date | string | null
|
|
339
335
|
}
|