@agent-native/core 0.173.1 → 0.173.2-nightly-20260825161643
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/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/eval/agent-runner.js +23 -0
- package/dist/eval/types.d.ts +11 -0
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -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
|
+
}
|
|
@@ -34,11 +34,13 @@ import {
|
|
|
34
34
|
} from "./_position-utils.js";
|
|
35
35
|
import { nanoid } from "./_property-utils.js";
|
|
36
36
|
|
|
37
|
+
const databaseMutationAuthorityScopeSchema = z.discriminatedUnion("kind", [
|
|
38
|
+
z.object({ kind: z.literal("personal"), id: z.string().min(1) }),
|
|
39
|
+
z.object({ kind: z.literal("organization"), id: z.string().min(1) }),
|
|
40
|
+
]);
|
|
41
|
+
|
|
37
42
|
export const databaseMutationTargetSchema = z.object({
|
|
38
|
-
authorityScope:
|
|
39
|
-
z.object({ kind: z.literal("personal"), id: z.string().min(1) }),
|
|
40
|
-
z.object({ kind: z.literal("organization"), id: z.string().min(1) }),
|
|
41
|
-
]),
|
|
43
|
+
authorityScope: databaseMutationAuthorityScopeSchema,
|
|
42
44
|
spaceId: z.string().min(1).describe("Exact Content space ID"),
|
|
43
45
|
databaseId: z.string().min(1).describe("Exact Content database ID"),
|
|
44
46
|
databaseDocumentId: z
|
|
@@ -47,8 +49,35 @@ export const databaseMutationTargetSchema = z.object({
|
|
|
47
49
|
.describe("Exact page ID backing the Content database"),
|
|
48
50
|
});
|
|
49
51
|
|
|
52
|
+
export const databaseMutationTargetInputSchema = z.object({
|
|
53
|
+
authorityScope: databaseMutationAuthorityScopeSchema
|
|
54
|
+
.optional()
|
|
55
|
+
.describe(
|
|
56
|
+
"Optional legacy assertion only. Agents must omit it; the authenticated server derives authority from the selected database.",
|
|
57
|
+
),
|
|
58
|
+
spaceId: z
|
|
59
|
+
.string()
|
|
60
|
+
.min(1)
|
|
61
|
+
.describe("Exact Content space ID returned by database discovery"),
|
|
62
|
+
databaseId: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(1)
|
|
65
|
+
.describe(
|
|
66
|
+
"Exact Content database ID returned by database discovery; never derive it from a title or number in the request",
|
|
67
|
+
),
|
|
68
|
+
databaseDocumentId: z
|
|
69
|
+
.string()
|
|
70
|
+
.min(1)
|
|
71
|
+
.describe(
|
|
72
|
+
"Exact page ID backing the database, returned by database discovery",
|
|
73
|
+
),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const databaseMutationAgentTargetSchema =
|
|
77
|
+
databaseMutationTargetInputSchema.omit({ authorityScope: true });
|
|
78
|
+
|
|
50
79
|
export const databaseMutationEnvelopeSchema = z.object({
|
|
51
|
-
target:
|
|
80
|
+
target: databaseMutationTargetInputSchema,
|
|
52
81
|
expectedSchemaRevision: z
|
|
53
82
|
.string()
|
|
54
83
|
.min(1)
|
|
@@ -59,6 +88,9 @@ export const databaseMutationEnvelopeSchema = z.object({
|
|
|
59
88
|
export type DatabaseMutationTarget = z.infer<
|
|
60
89
|
typeof databaseMutationTargetSchema
|
|
61
90
|
>;
|
|
91
|
+
export type DatabaseMutationTargetInput = z.infer<
|
|
92
|
+
typeof databaseMutationTargetInputSchema
|
|
93
|
+
>;
|
|
62
94
|
|
|
63
95
|
type DatabaseRow = typeof schema.contentDatabases.$inferSelect;
|
|
64
96
|
type DefinitionRow = typeof schema.documentPropertyDefinitions.$inferSelect;
|
|
@@ -82,11 +114,12 @@ export interface RowSnapshot {
|
|
|
82
114
|
export type DatabaseRowMutationOperation = "create" | "update" | "upsert";
|
|
83
115
|
|
|
84
116
|
export interface CreateDatabaseRowMutationInput {
|
|
85
|
-
target:
|
|
117
|
+
target: DatabaseMutationTargetInput;
|
|
86
118
|
expectedSchemaRevision: string;
|
|
87
119
|
idempotencyKey: string;
|
|
88
120
|
title?: string;
|
|
89
121
|
propertyValues?: Record<string, unknown>;
|
|
122
|
+
propertyTypeAssertions?: Record<string, string>;
|
|
90
123
|
}
|
|
91
124
|
|
|
92
125
|
export interface UpdateDatabaseRowMutationInput extends CreateDatabaseRowMutationInput {
|
|
@@ -211,7 +244,7 @@ function acceptedShape(type: DocumentPropertyType): string {
|
|
|
211
244
|
}
|
|
212
245
|
|
|
213
246
|
export async function loadContext(
|
|
214
|
-
target:
|
|
247
|
+
target: DatabaseMutationTargetInput,
|
|
215
248
|
role: "viewer" | "editor",
|
|
216
249
|
db: Db = getDb(),
|
|
217
250
|
accessAlreadyResolved = false,
|
|
@@ -260,8 +293,9 @@ export async function loadContext(
|
|
|
260
293
|
? { kind: "organization" as const, id: database.orgId }
|
|
261
294
|
: { kind: "personal" as const, id: database.ownerEmail };
|
|
262
295
|
if (
|
|
263
|
-
target.authorityScope
|
|
264
|
-
|
|
296
|
+
(target.authorityScope !== undefined &&
|
|
297
|
+
(target.authorityScope.kind !== authorityScope.kind ||
|
|
298
|
+
target.authorityScope.id !== authorityScope.id)) ||
|
|
265
299
|
database.spaceId !== target.spaceId ||
|
|
266
300
|
database.documentId !== target.databaseDocumentId ||
|
|
267
301
|
databaseDocument.spaceId !== target.spaceId ||
|
|
@@ -322,7 +356,12 @@ export async function getDatabaseMutationContract(
|
|
|
322
356
|
);
|
|
323
357
|
return {
|
|
324
358
|
target: {
|
|
325
|
-
|
|
359
|
+
authorityScope: context.database.orgId
|
|
360
|
+
? { kind: "organization", id: context.database.orgId }
|
|
361
|
+
: { kind: "personal", id: context.database.ownerEmail },
|
|
362
|
+
spaceId: context.database.spaceId!,
|
|
363
|
+
databaseId: context.database.id,
|
|
364
|
+
databaseDocumentId: context.database.documentId,
|
|
326
365
|
},
|
|
327
366
|
schemaRevision: context.schemaRevision,
|
|
328
367
|
naturalKeyPropertyId: context.database.naturalKeyPropertyId,
|
|
@@ -695,14 +734,72 @@ export function revisionPropertyIds(context: MutationContext) {
|
|
|
695
734
|
);
|
|
696
735
|
}
|
|
697
736
|
|
|
698
|
-
function
|
|
737
|
+
export function databaseMutationPayloadDigest(
|
|
699
738
|
operation: DatabaseRowMutationOperation,
|
|
700
739
|
input:
|
|
701
740
|
| CreateDatabaseRowMutationInput
|
|
702
741
|
| UpdateDatabaseRowMutationInput
|
|
703
742
|
| UpsertDatabaseRowMutationInput,
|
|
704
743
|
) {
|
|
705
|
-
|
|
744
|
+
const {
|
|
745
|
+
propertyTypeAssertions: _propertyTypeAssertions,
|
|
746
|
+
target,
|
|
747
|
+
...canonicalInput
|
|
748
|
+
} = input;
|
|
749
|
+
const { authorityScope: _authorityScope, ...stableTarget } = target ?? {};
|
|
750
|
+
return digest({ operation, ...canonicalInput, target: stableTarget });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function legacyDatabaseMutationPayloadDigest(
|
|
754
|
+
operation: DatabaseRowMutationOperation,
|
|
755
|
+
input:
|
|
756
|
+
| CreateDatabaseRowMutationInput
|
|
757
|
+
| UpdateDatabaseRowMutationInput
|
|
758
|
+
| UpsertDatabaseRowMutationInput,
|
|
759
|
+
authorityScope = input.target.authorityScope,
|
|
760
|
+
) {
|
|
761
|
+
const { propertyTypeAssertions: _propertyTypeAssertions, ...legacyInput } =
|
|
762
|
+
input;
|
|
763
|
+
return digest({
|
|
764
|
+
operation,
|
|
765
|
+
...legacyInput,
|
|
766
|
+
target: { ...legacyInput.target, authorityScope },
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function authorityScopeForContext(context: MutationContext) {
|
|
771
|
+
return context.database.orgId
|
|
772
|
+
? ({ kind: "organization", id: context.database.orgId } as const)
|
|
773
|
+
: ({ kind: "personal", id: context.database.ownerEmail } as const);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function assertPropertyTypeAssertions(
|
|
777
|
+
context: MutationContext,
|
|
778
|
+
assertions: Record<string, string> | undefined,
|
|
779
|
+
) {
|
|
780
|
+
if (!assertions) return;
|
|
781
|
+
const definitionsById = new Map(
|
|
782
|
+
context.definitions.map((definition) => [definition.id, definition]),
|
|
783
|
+
);
|
|
784
|
+
for (const [propertyId, assertedType] of Object.entries(assertions)) {
|
|
785
|
+
const definition = definitionsById.get(propertyId);
|
|
786
|
+
if (!definition) {
|
|
787
|
+
throw new ActionContractError(
|
|
788
|
+
`Unknown property definition "${propertyId}".`,
|
|
789
|
+
{
|
|
790
|
+
errorCode: "UNKNOWN_PROPERTY",
|
|
791
|
+
details: { propertyId },
|
|
792
|
+
statusCode: 400,
|
|
793
|
+
},
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
if (definition.type !== assertedType) {
|
|
797
|
+
invalidProperty(
|
|
798
|
+
definition,
|
|
799
|
+
`typed entry declared propertyType "${assertedType}" but the discovered property type is "${definition.type}"`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
706
803
|
}
|
|
707
804
|
|
|
708
805
|
function resultForReceipt(
|
|
@@ -721,9 +818,7 @@ function resultForReceipt(
|
|
|
721
818
|
},
|
|
722
819
|
): ContentDatabaseRowMutationResult {
|
|
723
820
|
const target = {
|
|
724
|
-
authorityScope: context
|
|
725
|
-
? ({ kind: "organization", id: context.database.orgId } as const)
|
|
726
|
-
: ({ kind: "personal", id: context.database.ownerEmail } as const),
|
|
821
|
+
authorityScope: authorityScopeForContext(context),
|
|
727
822
|
spaceId: context.database.spaceId!,
|
|
728
823
|
databaseId: context.database.id,
|
|
729
824
|
databaseDocumentId: context.database.documentId,
|
|
@@ -770,7 +865,7 @@ function resultForReceipt(
|
|
|
770
865
|
async function replayReceipt(
|
|
771
866
|
context: MutationContext,
|
|
772
867
|
idempotencyKey: string,
|
|
773
|
-
|
|
868
|
+
expectedPayloadDigests: readonly string[],
|
|
774
869
|
db: Db = getDb(),
|
|
775
870
|
): Promise<ContentDatabaseRowMutationResult | null> {
|
|
776
871
|
const [stored] = await db
|
|
@@ -789,7 +884,7 @@ async function replayReceipt(
|
|
|
789
884
|
),
|
|
790
885
|
);
|
|
791
886
|
if (!stored) return null;
|
|
792
|
-
if (stored.payloadDigest
|
|
887
|
+
if (!expectedPayloadDigests.includes(stored.payloadDigest)) {
|
|
793
888
|
conflict(
|
|
794
889
|
"IDEMPOTENCY_KEY_REUSED",
|
|
795
890
|
"This idempotency key was already used for a different row mutation.",
|
|
@@ -1137,13 +1232,22 @@ export async function createDatabaseRow(
|
|
|
1137
1232
|
input: CreateDatabaseRowMutationInput,
|
|
1138
1233
|
): Promise<ContentDatabaseRowMutationResult> {
|
|
1139
1234
|
const initial = await loadContext(input.target, "editor");
|
|
1140
|
-
const inputDigest =
|
|
1235
|
+
const inputDigest = databaseMutationPayloadDigest("create", input);
|
|
1236
|
+
const replayDigests = [
|
|
1237
|
+
inputDigest,
|
|
1238
|
+
legacyDatabaseMutationPayloadDigest(
|
|
1239
|
+
"create",
|
|
1240
|
+
input,
|
|
1241
|
+
authorityScopeForContext(initial),
|
|
1242
|
+
),
|
|
1243
|
+
];
|
|
1141
1244
|
const replay = await replayReceipt(
|
|
1142
1245
|
initial,
|
|
1143
1246
|
input.idempotencyKey,
|
|
1144
|
-
|
|
1247
|
+
replayDigests,
|
|
1145
1248
|
);
|
|
1146
1249
|
if (replay) return replay;
|
|
1250
|
+
assertPropertyTypeAssertions(initial, input.propertyTypeAssertions);
|
|
1147
1251
|
assertSchema(initial, input.expectedSchemaRevision);
|
|
1148
1252
|
const values = await normalizePatch(initial, input.propertyValues);
|
|
1149
1253
|
const result = await withMutationLocks(initial.database, () =>
|
|
@@ -1160,10 +1264,11 @@ export async function createDatabaseRow(
|
|
|
1160
1264
|
const lockedReplay = await replayReceipt(
|
|
1161
1265
|
locked,
|
|
1162
1266
|
input.idempotencyKey,
|
|
1163
|
-
|
|
1267
|
+
replayDigests,
|
|
1164
1268
|
tx as unknown as Db,
|
|
1165
1269
|
);
|
|
1166
1270
|
if (lockedReplay) return lockedReplay;
|
|
1271
|
+
assertPropertyTypeAssertions(locked, input.propertyTypeAssertions);
|
|
1167
1272
|
assertSchema(locked, input.expectedSchemaRevision);
|
|
1168
1273
|
await touchContentDatabase(
|
|
1169
1274
|
tx as unknown as Db,
|
|
@@ -1206,13 +1311,22 @@ export async function updateDatabaseRow(
|
|
|
1206
1311
|
): Promise<ContentDatabaseRowMutationResult> {
|
|
1207
1312
|
const initial = await loadContext(input.target, "editor");
|
|
1208
1313
|
await assertAccess("document", input.documentId, "editor");
|
|
1209
|
-
const inputDigest =
|
|
1314
|
+
const inputDigest = databaseMutationPayloadDigest("update", input);
|
|
1315
|
+
const replayDigests = [
|
|
1316
|
+
inputDigest,
|
|
1317
|
+
legacyDatabaseMutationPayloadDigest(
|
|
1318
|
+
"update",
|
|
1319
|
+
input,
|
|
1320
|
+
authorityScopeForContext(initial),
|
|
1321
|
+
),
|
|
1322
|
+
];
|
|
1210
1323
|
const replay = await replayReceipt(
|
|
1211
1324
|
initial,
|
|
1212
1325
|
input.idempotencyKey,
|
|
1213
|
-
|
|
1326
|
+
replayDigests,
|
|
1214
1327
|
);
|
|
1215
1328
|
if (replay) return replay;
|
|
1329
|
+
assertPropertyTypeAssertions(initial, input.propertyTypeAssertions);
|
|
1216
1330
|
assertSchema(initial, input.expectedSchemaRevision);
|
|
1217
1331
|
const values = await normalizePatch(initial, input.propertyValues);
|
|
1218
1332
|
const result = await withMutationLocks(initial.database, () =>
|
|
@@ -1229,10 +1343,11 @@ export async function updateDatabaseRow(
|
|
|
1229
1343
|
const lockedReplay = await replayReceipt(
|
|
1230
1344
|
locked,
|
|
1231
1345
|
input.idempotencyKey,
|
|
1232
|
-
|
|
1346
|
+
replayDigests,
|
|
1233
1347
|
tx as unknown as Db,
|
|
1234
1348
|
);
|
|
1235
1349
|
if (lockedReplay) return lockedReplay;
|
|
1350
|
+
assertPropertyTypeAssertions(locked, input.propertyTypeAssertions);
|
|
1236
1351
|
assertSchema(locked, input.expectedSchemaRevision);
|
|
1237
1352
|
const updated = await updateInsideTransaction(
|
|
1238
1353
|
tx as unknown as Db,
|
|
@@ -1305,13 +1420,22 @@ export async function upsertDatabaseRow(
|
|
|
1305
1420
|
"must match the upsert keyValue when provided in propertyValues",
|
|
1306
1421
|
);
|
|
1307
1422
|
}
|
|
1308
|
-
const inputDigest =
|
|
1423
|
+
const inputDigest = databaseMutationPayloadDigest("upsert", input);
|
|
1424
|
+
const replayDigests = [
|
|
1425
|
+
inputDigest,
|
|
1426
|
+
legacyDatabaseMutationPayloadDigest(
|
|
1427
|
+
"upsert",
|
|
1428
|
+
input,
|
|
1429
|
+
authorityScopeForContext(initial),
|
|
1430
|
+
),
|
|
1431
|
+
];
|
|
1309
1432
|
const replay = await replayReceipt(
|
|
1310
1433
|
initial,
|
|
1311
1434
|
input.idempotencyKey,
|
|
1312
|
-
|
|
1435
|
+
replayDigests,
|
|
1313
1436
|
);
|
|
1314
1437
|
if (replay) return replay;
|
|
1438
|
+
assertPropertyTypeAssertions(initial, input.propertyTypeAssertions);
|
|
1315
1439
|
assertSchema(initial, input.expectedSchemaRevision);
|
|
1316
1440
|
const keyPropertyId = initial.database.naturalKeyPropertyId;
|
|
1317
1441
|
if (!keyPropertyId) {
|
|
@@ -1368,10 +1492,11 @@ export async function upsertDatabaseRow(
|
|
|
1368
1492
|
const lockedReplay = await replayReceipt(
|
|
1369
1493
|
locked,
|
|
1370
1494
|
input.idempotencyKey,
|
|
1371
|
-
|
|
1495
|
+
replayDigests,
|
|
1372
1496
|
tx as unknown as Db,
|
|
1373
1497
|
);
|
|
1374
1498
|
if (lockedReplay) return lockedReplay;
|
|
1499
|
+
assertPropertyTypeAssertions(locked, input.propertyTypeAssertions);
|
|
1375
1500
|
assertSchema(locked, input.expectedSchemaRevision);
|
|
1376
1501
|
if (locked.database.naturalKeyPropertyId !== keyPropertyId) {
|
|
1377
1502
|
conflict("SCHEMA_REVISION_CONFLICT", "The natural key changed.");
|
|
@@ -3,8 +3,14 @@ import { buildDeepLink } from "@agent-native/core/server";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import type { ContentDatabaseRowMutationResult } from "../shared/api.js";
|
|
6
|
+
import {
|
|
7
|
+
canonicalizeDatabasePropertyInput,
|
|
8
|
+
databasePropertyEntriesSchema,
|
|
9
|
+
databasePropertyValuesSchema,
|
|
10
|
+
} from "./_database-property-input.js";
|
|
6
11
|
import {
|
|
7
12
|
createDatabaseRow,
|
|
13
|
+
databaseMutationAgentTargetSchema,
|
|
8
14
|
databaseMutationEnvelopeSchema,
|
|
9
15
|
} from "./_database-row-mutation.js";
|
|
10
16
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
@@ -17,15 +23,17 @@ const schema = databaseMutationEnvelopeSchema.extend({
|
|
|
17
23
|
.max(500)
|
|
18
24
|
.optional()
|
|
19
25
|
.describe("New row page title"),
|
|
20
|
-
propertyValues:
|
|
21
|
-
|
|
22
|
-
.optional()
|
|
23
|
-
.describe("Strict property values keyed by property definition ID"),
|
|
26
|
+
propertyValues: databasePropertyValuesSchema,
|
|
27
|
+
propertyEntries: databasePropertyEntriesSchema,
|
|
24
28
|
});
|
|
29
|
+
const agentSchema = schema
|
|
30
|
+
.extend({ target: databaseMutationAgentTargetSchema })
|
|
31
|
+
.omit({ propertyValues: true });
|
|
25
32
|
|
|
26
33
|
export default defineAction({
|
|
27
34
|
description:
|
|
28
35
|
"Create one row in an exact ordinary Content database using its discovered schema revision. Strictly validates every non-Blocks property, applies the side effect once per idempotency key, and returns a verified receipt with stable row identity.",
|
|
36
|
+
agentInputSchema: agentSchema,
|
|
29
37
|
publicAgent: {
|
|
30
38
|
expose: true,
|
|
31
39
|
readOnly: false,
|
|
@@ -52,7 +60,9 @@ export default defineAction({
|
|
|
52
60
|
},
|
|
53
61
|
},
|
|
54
62
|
run: async (args): Promise<ContentDatabaseRowMutationResult> => {
|
|
55
|
-
const result = await createDatabaseRow(
|
|
63
|
+
const result = await createDatabaseRow(
|
|
64
|
+
canonicalizeDatabasePropertyInput(args),
|
|
65
|
+
);
|
|
56
66
|
const response = await getContentDatabaseResponse(
|
|
57
67
|
result.receipt.target.databaseId,
|
|
58
68
|
{
|
|
@@ -4,6 +4,12 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import type { ContentDatabaseRowMutationResult } from "../shared/api.js";
|
|
6
6
|
import {
|
|
7
|
+
canonicalizeDatabasePropertyInput,
|
|
8
|
+
databasePropertyEntriesSchema,
|
|
9
|
+
databasePropertyValuesSchema,
|
|
10
|
+
} from "./_database-property-input.js";
|
|
11
|
+
import {
|
|
12
|
+
databaseMutationAgentTargetSchema,
|
|
7
13
|
databaseMutationEnvelopeSchema,
|
|
8
14
|
updateDatabaseRow,
|
|
9
15
|
} from "./_database-row-mutation.js";
|
|
@@ -16,17 +22,19 @@ const schema = databaseMutationEnvelopeSchema.extend({
|
|
|
16
22
|
.min(1)
|
|
17
23
|
.describe("Row revision returned by get-content-database"),
|
|
18
24
|
title: z.string().trim().min(1).max(500).optional(),
|
|
19
|
-
propertyValues:
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
"Sparse strict patch keyed by property definition ID; omitted fields are preserved and explicit null clears a value",
|
|
24
|
-
),
|
|
25
|
+
propertyValues: databasePropertyValuesSchema,
|
|
26
|
+
propertyEntries: databasePropertyEntriesSchema.describe(
|
|
27
|
+
"Sparse typed property patch as explicit entries; omitted fields are preserved and explicit null clears a value. 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.",
|
|
28
|
+
),
|
|
25
29
|
});
|
|
30
|
+
const agentSchema = schema
|
|
31
|
+
.extend({ target: databaseMutationAgentTargetSchema })
|
|
32
|
+
.omit({ propertyValues: true });
|
|
26
33
|
|
|
27
34
|
export default defineAction({
|
|
28
35
|
description:
|
|
29
36
|
"Sparsely update one exact Content database row by stable item and document IDs. Requires schema and row revisions, validates every non-Blocks property, and returns a verified idempotent receipt.",
|
|
37
|
+
agentInputSchema: agentSchema,
|
|
30
38
|
schema,
|
|
31
39
|
http: { method: "PUT" },
|
|
32
40
|
audit: {
|
|
@@ -44,7 +52,7 @@ export default defineAction({
|
|
|
44
52
|
: "Updated Content database row";
|
|
45
53
|
},
|
|
46
54
|
},
|
|
47
|
-
run: updateDatabaseRow,
|
|
55
|
+
run: (args) => updateDatabaseRow(canonicalizeDatabasePropertyInput(args)),
|
|
48
56
|
link: ({ result }) => {
|
|
49
57
|
const documentId = (result as ContentDatabaseRowMutationResult | null)
|
|
50
58
|
?.receipt.row.documentId;
|
|
@@ -4,6 +4,12 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import type { ContentDatabaseRowMutationResult } from "../shared/api.js";
|
|
6
6
|
import {
|
|
7
|
+
canonicalizeDatabasePropertyInput,
|
|
8
|
+
databasePropertyEntriesSchema,
|
|
9
|
+
databasePropertyValuesSchema,
|
|
10
|
+
} from "./_database-property-input.js";
|
|
11
|
+
import {
|
|
12
|
+
databaseMutationAgentTargetSchema,
|
|
7
13
|
databaseMutationEnvelopeSchema,
|
|
8
14
|
upsertDatabaseRow,
|
|
9
15
|
} from "./_database-row-mutation.js";
|
|
@@ -18,15 +24,19 @@ const schema = databaseMutationEnvelopeSchema.extend({
|
|
|
18
24
|
"Use null to assert the key is absent and create; use the discovered row revision to update an existing key",
|
|
19
25
|
),
|
|
20
26
|
title: z.string().trim().min(1).max(500).optional(),
|
|
21
|
-
propertyValues:
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
27
|
+
propertyValues: databasePropertyValuesSchema,
|
|
28
|
+
propertyEntries: databasePropertyEntriesSchema.describe(
|
|
29
|
+
"Sparse 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.",
|
|
30
|
+
),
|
|
25
31
|
});
|
|
32
|
+
const agentSchema = schema
|
|
33
|
+
.extend({ target: databaseMutationAgentTargetSchema })
|
|
34
|
+
.omit({ propertyValues: true });
|
|
26
35
|
|
|
27
36
|
export default defineAction({
|
|
28
37
|
description:
|
|
29
38
|
"Create or sparsely update one Content database row by that database's explicitly configured natural key. Requires schema and row compare-and-swap revisions and returns a verified idempotent receipt.",
|
|
39
|
+
agentInputSchema: agentSchema,
|
|
30
40
|
schema,
|
|
31
41
|
audit: {
|
|
32
42
|
recordInputs: false,
|
|
@@ -43,7 +53,7 @@ export default defineAction({
|
|
|
43
53
|
: "Upserted Content database row by natural key";
|
|
44
54
|
},
|
|
45
55
|
},
|
|
46
|
-
run: upsertDatabaseRow,
|
|
56
|
+
run: (args) => upsertDatabaseRow(canonicalizeDatabasePropertyInput(args)),
|
|
47
57
|
link: ({ result }) => {
|
|
48
58
|
const documentId = (result as ContentDatabaseRowMutationResult | null)
|
|
49
59
|
?.receipt.row.documentId;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"test:parity": "vitest --run parity",
|
|
13
13
|
"test:parity-capabilities": "vitest --run parity actions/content-database-lifecycle.db.test.ts actions/bind-content-database-source-field.db.test.ts actions/_local-file-documents.test.ts actions/builder-source-review-gates.db.test.ts",
|
|
14
14
|
"eval:parity": "agent-native eval parity",
|
|
15
|
+
"eval:property-preservation": "tsx parity/run-database-create-property-preservation.ts",
|
|
15
16
|
"format.fix": "oxfmt --write .",
|
|
16
17
|
"typecheck": "agent-native typecheck",
|
|
17
18
|
"migrate:production": "tsx scripts/migrate-production.ts",
|