@agent-native/core 0.173.1 → 0.173.2-nightly-20260825163448
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/corpus/README.md +1 -1
- package/corpus/templates/clips/desktop/src/app.tsx +77 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +91 -25
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +20 -2
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +7 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +372 -70
- package/corpus/templates/clips/server/lib/resumable-session.ts +15 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +245 -118
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +21 -11
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +132 -25
- package/corpus/templates/content/actions/_database-property-input.ts +211 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +151 -26
- package/corpus/templates/content/actions/add-database-item.ts +15 -5
- package/corpus/templates/content/actions/update-database-item.ts +15 -7
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +15 -5
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/parity/README.md +11 -2
- package/corpus/templates/content/parity/eval-scenarios.ts +52 -0
- package/corpus/templates/content/parity/run-database-create-property-preservation.ts +140 -0
- package/corpus/templates/content/parity/scenario-to-eval.ts +390 -0
- package/dist/agent/run-store.js +0 -1
- package/dist/client/agent-engine-key.js +26 -4
- package/dist/client/settings/ProviderSetupForm.js +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/eval/agent-runner.js +23 -0
- package/dist/eval/types.d.ts +11 -0
- package/dist/observability/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +7 -0
- package/dist/server/agent-engine-api-key-route.js +38 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* Route: GET /api/uploads/:recordingId/resume
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
|
|
14
16
|
import {
|
|
15
17
|
compareAndSetAppState,
|
|
16
18
|
readAppState,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
} from "@agent-native/core/application-state";
|
|
19
21
|
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
20
22
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
21
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
23
|
+
import { and, eq, isNull, lte } from "drizzle-orm";
|
|
22
24
|
import {
|
|
23
25
|
createError,
|
|
24
26
|
defineEventHandler,
|
|
@@ -40,12 +42,32 @@ import {
|
|
|
40
42
|
getEventOwnerContext,
|
|
41
43
|
ownerEmailMatches,
|
|
42
44
|
} from "../../../../lib/recordings.js";
|
|
43
|
-
import { getResumableSession } from "../../../../lib/resumable-session.js";
|
|
44
45
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} from "../../../../lib/
|
|
48
|
-
import {
|
|
46
|
+
deleteResumableSession,
|
|
47
|
+
getResumableSession,
|
|
48
|
+
} from "../../../../lib/resumable-session.js";
|
|
49
|
+
import { abortResumableUploadSession } from "../../../../lib/resumable-upload-cleanup.js";
|
|
50
|
+
import { isRetryableUploadInterruption } from "../../../../lib/upload-interruption.js";
|
|
51
|
+
import {
|
|
52
|
+
UPLOAD_LEASE_MS,
|
|
53
|
+
uploadLeaseExpiry,
|
|
54
|
+
} from "../../../../lib/upload-lease.js";
|
|
55
|
+
|
|
56
|
+
const RETRY_CLAIM_LIVENESS_MS = 30 * 1000;
|
|
57
|
+
const RETRY_CLAIM_RETRY_AFTER_MIN_MS = 250;
|
|
58
|
+
|
|
59
|
+
function retryClaimRetryAfterMs(
|
|
60
|
+
lastHeartbeatMs: number,
|
|
61
|
+
nowMs: number,
|
|
62
|
+
): number {
|
|
63
|
+
return Math.max(
|
|
64
|
+
RETRY_CLAIM_RETRY_AFTER_MIN_MS,
|
|
65
|
+
Math.min(
|
|
66
|
+
RETRY_CLAIM_LIVENESS_MS,
|
|
67
|
+
lastHeartbeatMs + RETRY_CLAIM_LIVENESS_MS - nowMs,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
49
71
|
|
|
50
72
|
export default defineEventHandler(async (event: H3Event) => {
|
|
51
73
|
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
@@ -85,13 +107,51 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
85
107
|
orgId,
|
|
86
108
|
});
|
|
87
109
|
if (!recoveryEnabled) {
|
|
88
|
-
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
|
|
111
|
+
const [recording] = await getDb()
|
|
112
|
+
.select({
|
|
113
|
+
status: schema.recordings.status,
|
|
114
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
115
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
116
|
+
})
|
|
117
|
+
.from(schema.recordings)
|
|
118
|
+
.where(
|
|
119
|
+
and(
|
|
120
|
+
eq(schema.recordings.id, recordingId),
|
|
121
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
if (!recording) {
|
|
125
|
+
setResponseStatus(event, 404);
|
|
126
|
+
return { error: "Recording not found" };
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
recording.uploadAttemptId &&
|
|
130
|
+
recording.uploadAttemptId !== requestedAttemptId
|
|
131
|
+
) {
|
|
132
|
+
setResponseStatus(event, 409);
|
|
133
|
+
return {
|
|
134
|
+
recoveryEnabled: false,
|
|
135
|
+
resumable: false,
|
|
136
|
+
recordingId,
|
|
137
|
+
status: recording.status,
|
|
138
|
+
reason: "retry_already_active",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
recoveryEnabled: false,
|
|
143
|
+
resumable: false,
|
|
144
|
+
recordingId,
|
|
145
|
+
status: recording.status ?? null,
|
|
146
|
+
reason: "feature_disabled",
|
|
147
|
+
...(recording.uploadAttemptId
|
|
148
|
+
? { attemptId: recording.uploadAttemptId }
|
|
149
|
+
: {}),
|
|
150
|
+
...(recording.uploadGenerationId
|
|
151
|
+
? { uploadGenerationId: recording.uploadGenerationId }
|
|
152
|
+
: {}),
|
|
153
|
+
};
|
|
154
|
+
});
|
|
95
155
|
}
|
|
96
156
|
|
|
97
157
|
return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
|
|
@@ -104,6 +164,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
104
164
|
uploadProgress: schema.recordings.uploadProgress,
|
|
105
165
|
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
106
166
|
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
167
|
+
uploadLeaseExpiresAt: schema.recordings.uploadLeaseExpiresAt,
|
|
107
168
|
})
|
|
108
169
|
.from(schema.recordings)
|
|
109
170
|
.where(
|
|
@@ -121,19 +182,38 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
121
182
|
// Legacy rows keep their null generation and unscoped scratch. A reset
|
|
122
183
|
// upgrades them by installing a fresh generation before it deletes data.
|
|
123
184
|
const existingGenerationId = recording.uploadGenerationId ?? null;
|
|
124
|
-
|
|
125
|
-
|
|
185
|
+
let generationId = existingGenerationId;
|
|
186
|
+
let session = generationId
|
|
126
187
|
? await getResumableSession(recordingId, generationId)
|
|
127
188
|
: await getResumableSession(recordingId);
|
|
128
189
|
const retryableFailure =
|
|
129
190
|
recording.status === "failed" &&
|
|
130
191
|
isRetryableUploadInterruption(recording.failureReason);
|
|
131
192
|
const existingAttemptId = recording.uploadAttemptId ?? null;
|
|
132
|
-
|
|
193
|
+
const nowMs = Date.now();
|
|
194
|
+
const now = new Date(nowMs).toISOString();
|
|
195
|
+
const staleLeaseThreshold = new Date(
|
|
196
|
+
nowMs + UPLOAD_LEASE_MS - RETRY_CLAIM_LIVENESS_MS,
|
|
197
|
+
).toISOString();
|
|
198
|
+
const claimLeaseExpiryMs = Date.parse(recording.uploadLeaseExpiresAt ?? "");
|
|
199
|
+
const claimHeartbeatMs = claimLeaseExpiryMs - UPLOAD_LEASE_MS;
|
|
200
|
+
const differentRetryClaim =
|
|
133
201
|
recording.status === "uploading" &&
|
|
134
202
|
existingAttemptId !== null &&
|
|
135
|
-
existingAttemptId !== requestedAttemptId
|
|
136
|
-
) {
|
|
203
|
+
existingAttemptId !== requestedAttemptId;
|
|
204
|
+
if (differentRetryClaim && !Number.isFinite(claimHeartbeatMs)) {
|
|
205
|
+
setResponseStatus(event, 409);
|
|
206
|
+
return {
|
|
207
|
+
resumable: false,
|
|
208
|
+
recoveryEnabled: true,
|
|
209
|
+
recordingId,
|
|
210
|
+
status: "uploading",
|
|
211
|
+
reason: "retry_claim_liveness_unavailable",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const differentLiveRetryClaim =
|
|
215
|
+
differentRetryClaim && claimHeartbeatMs > nowMs - RETRY_CLAIM_LIVENESS_MS;
|
|
216
|
+
if (differentLiveRetryClaim) {
|
|
137
217
|
setResponseStatus(event, 409);
|
|
138
218
|
return {
|
|
139
219
|
resumable: false,
|
|
@@ -141,6 +221,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
141
221
|
recordingId,
|
|
142
222
|
status: "uploading",
|
|
143
223
|
reason: "retry_already_active",
|
|
224
|
+
retryAfterMs: retryClaimRetryAfterMs(claimHeartbeatMs, nowMs),
|
|
144
225
|
};
|
|
145
226
|
}
|
|
146
227
|
if (recording.status !== "uploading" && !retryableFailure) {
|
|
@@ -158,15 +239,21 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
158
239
|
const uploadStateRaw = await readAppState(uploadStateKey);
|
|
159
240
|
const uploadState = uploadStateRaw ?? {};
|
|
160
241
|
const attemptId = requestedAttemptId;
|
|
161
|
-
const
|
|
242
|
+
const takingOverStaleRetryClaim = differentRetryClaim;
|
|
243
|
+
const claimedGenerationId = takingOverStaleRetryClaim
|
|
244
|
+
? randomUUID()
|
|
245
|
+
: generationId;
|
|
246
|
+
const claimedLeaseExpiry = uploadLeaseExpiry(nowMs);
|
|
162
247
|
const claimed = await getDb()
|
|
163
248
|
.update(schema.recordings)
|
|
164
249
|
.set({
|
|
165
250
|
status: "uploading",
|
|
166
251
|
failureReason: null,
|
|
167
252
|
uploadAttemptId: attemptId,
|
|
168
|
-
...(
|
|
169
|
-
|
|
253
|
+
...(claimedGenerationId
|
|
254
|
+
? { uploadGenerationId: claimedGenerationId }
|
|
255
|
+
: {}),
|
|
256
|
+
uploadLeaseExpiresAt: claimedLeaseExpiry,
|
|
170
257
|
updatedAt: now,
|
|
171
258
|
})
|
|
172
259
|
.where(
|
|
@@ -177,10 +264,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
177
264
|
? eq(schema.recordings.status, "failed")
|
|
178
265
|
: eq(schema.recordings.status, "uploading"),
|
|
179
266
|
retryableFailure
|
|
180
|
-
? eq(
|
|
181
|
-
schema.recordings.failureReason,
|
|
182
|
-
RETRYABLE_UPLOAD_INTERRUPTION_REASON,
|
|
183
|
-
)
|
|
267
|
+
? eq(schema.recordings.failureReason, recording.failureReason!)
|
|
184
268
|
: undefined,
|
|
185
269
|
existingAttemptId === null
|
|
186
270
|
? isNull(schema.recordings.uploadAttemptId)
|
|
@@ -188,6 +272,9 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
188
272
|
existingGenerationId === null
|
|
189
273
|
? isNull(schema.recordings.uploadGenerationId)
|
|
190
274
|
: eq(schema.recordings.uploadGenerationId, existingGenerationId),
|
|
275
|
+
takingOverStaleRetryClaim
|
|
276
|
+
? lte(schema.recordings.uploadLeaseExpiresAt, staleLeaseThreshold)
|
|
277
|
+
: undefined,
|
|
191
278
|
),
|
|
192
279
|
)
|
|
193
280
|
.returning({ id: schema.recordings.id });
|
|
@@ -200,9 +287,29 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
200
287
|
recordingId,
|
|
201
288
|
status: "uploading",
|
|
202
289
|
reason: "retry_already_active",
|
|
290
|
+
retryAfterMs: 250,
|
|
203
291
|
};
|
|
204
292
|
}
|
|
205
293
|
|
|
294
|
+
if (takingOverStaleRetryClaim && session) {
|
|
295
|
+
const invalidated = await abortResumableUploadSession(session, {
|
|
296
|
+
label: `upload-resume-takeover-${recordingId}`,
|
|
297
|
+
});
|
|
298
|
+
if (!invalidated) {
|
|
299
|
+
setResponseStatus(event, 409);
|
|
300
|
+
return {
|
|
301
|
+
resumable: false,
|
|
302
|
+
recoveryEnabled: true,
|
|
303
|
+
recordingId,
|
|
304
|
+
status: "uploading",
|
|
305
|
+
reason: "stale_provider_session_invalidation_failed",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
await deleteResumableSession(recordingId, generationId);
|
|
309
|
+
session = null;
|
|
310
|
+
}
|
|
311
|
+
generationId = claimedGenerationId;
|
|
312
|
+
|
|
206
313
|
const uploadStateUpdated = await compareAndSetAppState(
|
|
207
314
|
uploadStateKey,
|
|
208
315
|
uploadStateRaw,
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { ActionContractError } from "@agent-native/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const nullable = <T extends z.ZodTypeAny>(schema: T) =>
|
|
5
|
+
z.union([schema, z.null()]);
|
|
6
|
+
|
|
7
|
+
const propertyIdSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.describe("Exact immutable property definition ID");
|
|
11
|
+
|
|
12
|
+
const stringPropertyEntry = (
|
|
13
|
+
propertyType: "text" | "place" | "phone" | "url" | "email",
|
|
14
|
+
valueDescription: string,
|
|
15
|
+
) =>
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
propertyId: propertyIdSchema,
|
|
19
|
+
propertyType: z.literal(propertyType),
|
|
20
|
+
value: nullable(z.string()).describe(valueDescription),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
|
|
24
|
+
const optionPropertyEntry = (propertyType: "select" | "status") =>
|
|
25
|
+
z
|
|
26
|
+
.object({
|
|
27
|
+
propertyId: propertyIdSchema,
|
|
28
|
+
propertyType: z.literal(propertyType),
|
|
29
|
+
value: nullable(z.string()).describe(
|
|
30
|
+
"Exact option ID or exact option label from the discovered property contract; null explicitly clears the value",
|
|
31
|
+
),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
|
|
35
|
+
export const databasePropertyEntrySchema = z.discriminatedUnion(
|
|
36
|
+
"propertyType",
|
|
37
|
+
[
|
|
38
|
+
stringPropertyEntry("text", "Text value; null explicitly clears the value"),
|
|
39
|
+
stringPropertyEntry(
|
|
40
|
+
"place",
|
|
41
|
+
"Place text; null explicitly clears the value",
|
|
42
|
+
),
|
|
43
|
+
stringPropertyEntry(
|
|
44
|
+
"phone",
|
|
45
|
+
"Phone text; null explicitly clears the value",
|
|
46
|
+
),
|
|
47
|
+
stringPropertyEntry(
|
|
48
|
+
"url",
|
|
49
|
+
"Absolute http/https URL; null explicitly clears the value",
|
|
50
|
+
),
|
|
51
|
+
stringPropertyEntry(
|
|
52
|
+
"email",
|
|
53
|
+
"Email address; null explicitly clears the value",
|
|
54
|
+
),
|
|
55
|
+
z
|
|
56
|
+
.object({
|
|
57
|
+
propertyId: propertyIdSchema,
|
|
58
|
+
propertyType: z.literal("number"),
|
|
59
|
+
value: nullable(z.number().finite()).describe(
|
|
60
|
+
"Finite number; use a JSON number rather than numeric text, or null to explicitly clear",
|
|
61
|
+
),
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
64
|
+
z
|
|
65
|
+
.object({
|
|
66
|
+
propertyId: propertyIdSchema,
|
|
67
|
+
propertyType: z.literal("checkbox"),
|
|
68
|
+
value: nullable(z.boolean()).describe(
|
|
69
|
+
"Boolean; use true or false rather than text, or null to explicitly clear",
|
|
70
|
+
),
|
|
71
|
+
})
|
|
72
|
+
.strict(),
|
|
73
|
+
optionPropertyEntry("select"),
|
|
74
|
+
optionPropertyEntry("status"),
|
|
75
|
+
z
|
|
76
|
+
.object({
|
|
77
|
+
propertyId: propertyIdSchema,
|
|
78
|
+
propertyType: z.literal("multi_select"),
|
|
79
|
+
value: nullable(z.array(z.string())).describe(
|
|
80
|
+
"Option IDs or exact option labels from the discovered property contract; null explicitly clears the value",
|
|
81
|
+
),
|
|
82
|
+
})
|
|
83
|
+
.strict(),
|
|
84
|
+
z
|
|
85
|
+
.object({
|
|
86
|
+
propertyId: propertyIdSchema,
|
|
87
|
+
propertyType: z.literal("date"),
|
|
88
|
+
value: nullable(
|
|
89
|
+
z.union([
|
|
90
|
+
z.string(),
|
|
91
|
+
z
|
|
92
|
+
.object({
|
|
93
|
+
start: z.string(),
|
|
94
|
+
end: z.string().optional(),
|
|
95
|
+
includeTime: z.boolean().optional(),
|
|
96
|
+
})
|
|
97
|
+
.strict(),
|
|
98
|
+
]),
|
|
99
|
+
).describe(
|
|
100
|
+
"ISO date/date-time string or { start, end?, includeTime? }; null explicitly clears the value",
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
.strict(),
|
|
104
|
+
z
|
|
105
|
+
.object({
|
|
106
|
+
propertyId: propertyIdSchema,
|
|
107
|
+
propertyType: z.literal("person"),
|
|
108
|
+
value: nullable(z.array(z.string())).describe(
|
|
109
|
+
"Person identifiers from the discovered property contract; null explicitly clears the value",
|
|
110
|
+
),
|
|
111
|
+
})
|
|
112
|
+
.strict(),
|
|
113
|
+
z
|
|
114
|
+
.object({
|
|
115
|
+
propertyId: propertyIdSchema,
|
|
116
|
+
propertyType: z.literal("files_media"),
|
|
117
|
+
value: nullable(z.array(z.string())).describe(
|
|
118
|
+
"Absolute http/https file URLs; null explicitly clears the value",
|
|
119
|
+
),
|
|
120
|
+
})
|
|
121
|
+
.strict(),
|
|
122
|
+
],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export type DatabasePropertyEntry = z.infer<typeof databasePropertyEntrySchema>;
|
|
126
|
+
|
|
127
|
+
export const databasePropertyValuesSchema = z
|
|
128
|
+
.record(z.string(), z.unknown())
|
|
129
|
+
.optional()
|
|
130
|
+
.describe(
|
|
131
|
+
"Programmatic property values keyed by exact property definition ID.",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
export const databasePropertyEntriesSchema = z
|
|
135
|
+
.array(databasePropertyEntrySchema)
|
|
136
|
+
.min(1)
|
|
137
|
+
.max(1_000)
|
|
138
|
+
.optional()
|
|
139
|
+
.describe(
|
|
140
|
+
"Typed property values as explicit entries. Copy each propertyType from the discovered mutation contract and include one entry for every writable property value the user requested, using the exact immutable property definition ID. When at least one value was requested, never pass an empty array. Do not invent or clear unmentioned properties.",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
export function normalizeDatabasePropertyInput(input: {
|
|
144
|
+
propertyEntries?: DatabasePropertyEntry[];
|
|
145
|
+
propertyValues?: Record<string, unknown>;
|
|
146
|
+
}): {
|
|
147
|
+
propertyValues: Record<string, unknown> | undefined;
|
|
148
|
+
propertyTypeAssertions: Record<string, string> | undefined;
|
|
149
|
+
} {
|
|
150
|
+
if (input.propertyEntries && input.propertyValues) {
|
|
151
|
+
throw new ActionContractError(
|
|
152
|
+
"Provide propertyEntries or propertyValues, not both.",
|
|
153
|
+
{ errorCode: "AMBIGUOUS_PROPERTY_INPUT" },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!input.propertyEntries) {
|
|
157
|
+
return {
|
|
158
|
+
propertyValues: input.propertyValues,
|
|
159
|
+
propertyTypeAssertions: undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const values: Record<string, unknown> = Object.create(null) as Record<
|
|
164
|
+
string,
|
|
165
|
+
unknown
|
|
166
|
+
>;
|
|
167
|
+
const propertyTypes: Record<string, string> = Object.create(null) as Record<
|
|
168
|
+
string,
|
|
169
|
+
string
|
|
170
|
+
>;
|
|
171
|
+
for (const entry of input.propertyEntries) {
|
|
172
|
+
if (Object.prototype.hasOwnProperty.call(values, entry.propertyId)) {
|
|
173
|
+
throw new ActionContractError(
|
|
174
|
+
`Property entry ${entry.propertyId} was provided more than once.`,
|
|
175
|
+
{
|
|
176
|
+
errorCode: "DUPLICATE_PROPERTY_INPUT",
|
|
177
|
+
details: { propertyId: entry.propertyId },
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
values[entry.propertyId] = entry.value;
|
|
182
|
+
propertyTypes[entry.propertyId] = entry.propertyType;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
propertyValues: values,
|
|
186
|
+
propertyTypeAssertions: propertyTypes,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function canonicalizeDatabasePropertyInput<
|
|
191
|
+
T extends {
|
|
192
|
+
propertyEntries?: DatabasePropertyEntry[];
|
|
193
|
+
propertyValues?: Record<string, unknown>;
|
|
194
|
+
},
|
|
195
|
+
>(
|
|
196
|
+
input: T,
|
|
197
|
+
): Omit<T, "propertyEntries" | "propertyValues"> & {
|
|
198
|
+
propertyValues?: Record<string, unknown>;
|
|
199
|
+
propertyTypeAssertions?: Record<string, string>;
|
|
200
|
+
} {
|
|
201
|
+
const { propertyEntries, propertyValues, ...canonicalInput } = input;
|
|
202
|
+
const normalized = normalizeDatabasePropertyInput({
|
|
203
|
+
propertyEntries,
|
|
204
|
+
propertyValues,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
...canonicalInput,
|
|
208
|
+
propertyValues: normalized.propertyValues,
|
|
209
|
+
propertyTypeAssertions: normalized.propertyTypeAssertions,
|
|
210
|
+
};
|
|
211
|
+
}
|