@agent-native/core 0.154.3 → 0.154.5
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/.agents/skills/content/SKILL.md +2 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
- package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
- package/corpus/templates/content/actions/_database-utils.ts +55 -3
- package/corpus/templates/content/actions/_property-utils.ts +141 -31
- package/corpus/templates/content/actions/add-database-item.ts +62 -240
- package/corpus/templates/content/actions/configure-document-property.ts +170 -1
- package/corpus/templates/content/actions/delete-document-property.ts +56 -17
- package/corpus/templates/content/actions/delete-document.ts +3 -0
- package/corpus/templates/content/actions/edit-document.ts +18 -1
- package/corpus/templates/content/actions/export-document.ts +41 -0
- package/corpus/templates/content/actions/remove-database-items.ts +10 -0
- package/corpus/templates/content/actions/set-document-property.ts +140 -3
- package/corpus/templates/content/actions/update-database-item.ts +62 -0
- package/corpus/templates/content/actions/update-document.ts +24 -0
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
- package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
- package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
- package/corpus/templates/content/app/i18n-data.ts +20 -0
- package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
- package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
- package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
- package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/server/db/schema.ts +100 -3
- package/corpus/templates/content/server/plugins/db.ts +85 -0
- package/corpus/templates/content/shared/api.ts +78 -2
- package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
- package/corpus/templates/content/shared/document-export.ts +33 -1
- package/dist/action.d.ts +22 -0
- package/dist/action.js +27 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +7 -2
- package/dist/extensions/url-safety.js +27 -12
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-client/oauth-client.d.ts +16 -1
- package/dist/mcp-client/oauth-client.js +177 -83
- package/dist/mcp-client/remote-store.js +46 -20
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/index.d.ts +2 -1
- package/dist/oauth-tokens/index.js +2 -1
- package/dist/oauth-tokens/lifecycle.d.ts +87 -0
- package/dist/oauth-tokens/lifecycle.js +434 -0
- package/dist/oauth-tokens/migrations.js +10 -0
- package/dist/oauth-tokens/store.d.ts +22 -0
- package/dist/oauth-tokens/store.js +141 -18
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-routes.js +10 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +2 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
normalizePropertyVisibility,
|
|
17
17
|
type DocumentPropertyType,
|
|
18
18
|
} from "../shared/properties.js";
|
|
19
|
+
import { deleteBlocksFieldIdentity } from "./_blocks-field-identity.js";
|
|
19
20
|
import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
|
|
20
21
|
import { lockDatabaseMemberships } from "./_database-membership-lock.js";
|
|
21
22
|
import {
|
|
@@ -51,6 +52,12 @@ export default defineAction({
|
|
|
51
52
|
"Stable guidance describing what this property means and which value belongs here",
|
|
52
53
|
),
|
|
53
54
|
type: z.enum(CREATABLE_DOCUMENT_PROPERTY_TYPES).describe("Property type"),
|
|
55
|
+
naturalKey: z
|
|
56
|
+
.boolean()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(
|
|
59
|
+
"Declare or clear this ordinary text property as the database's single natural key",
|
|
60
|
+
),
|
|
54
61
|
visibility: z
|
|
55
62
|
.enum(DOCUMENT_PROPERTY_VISIBILITIES)
|
|
56
63
|
.optional()
|
|
@@ -103,6 +110,7 @@ export default defineAction({
|
|
|
103
110
|
const now = new Date().toISOString();
|
|
104
111
|
const name = args.name.trim();
|
|
105
112
|
const type = args.type as DocumentPropertyType;
|
|
113
|
+
const propertyId = args.id ?? nanoid();
|
|
106
114
|
const optionsJson = optionsForNewProperty(type, args.options as any);
|
|
107
115
|
const database = await resolvePropertyDatabaseForDocument(
|
|
108
116
|
document,
|
|
@@ -114,6 +122,14 @@ export default defineAction({
|
|
|
114
122
|
"Properties belong to databases. Create or open a database before adding properties.",
|
|
115
123
|
);
|
|
116
124
|
}
|
|
125
|
+
if (args.naturalKey === true && type !== "text") {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"A database natural key must be an ordinary text property.",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (args.naturalKey !== undefined && database.systemRole) {
|
|
131
|
+
throw new Error("System databases cannot configure a natural key.");
|
|
132
|
+
}
|
|
117
133
|
|
|
118
134
|
if (args.id) {
|
|
119
135
|
const [existing] = await db
|
|
@@ -135,6 +151,11 @@ export default defineAction({
|
|
|
135
151
|
tx as unknown as ReturnType<typeof getDb>,
|
|
136
152
|
database.id,
|
|
137
153
|
);
|
|
154
|
+
const [lockedDatabase] = await tx
|
|
155
|
+
.select()
|
|
156
|
+
.from(schema.contentDatabases)
|
|
157
|
+
.where(eq(schema.contentDatabases.id, database.id));
|
|
158
|
+
if (!lockedDatabase) throw new Error("Database not found.");
|
|
138
159
|
let [lockedDefinition] = await tx
|
|
139
160
|
.select()
|
|
140
161
|
.from(schema.documentPropertyDefinitions)
|
|
@@ -150,6 +171,14 @@ export default defineAction({
|
|
|
150
171
|
);
|
|
151
172
|
if (!lockedDefinition)
|
|
152
173
|
throw new Error(`Property "${args.id}" not found`);
|
|
174
|
+
if (
|
|
175
|
+
lockedDatabase.naturalKeyPropertyId === args.id &&
|
|
176
|
+
type !== "text"
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Clear the database natural key before changing this property's type.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
153
182
|
if (lockedDefinition.systemRole) {
|
|
154
183
|
throw new Error("System properties cannot be changed.");
|
|
155
184
|
}
|
|
@@ -233,6 +262,10 @@ export default defineAction({
|
|
|
233
262
|
) &&
|
|
234
263
|
!isBlocksPropertyType(type)
|
|
235
264
|
) {
|
|
265
|
+
await deleteBlocksFieldIdentity({
|
|
266
|
+
db: tx as unknown as ReturnType<typeof getDb>,
|
|
267
|
+
propertyId: args.id!,
|
|
268
|
+
});
|
|
236
269
|
await tx
|
|
237
270
|
.delete(schema.documentBlockFieldContents)
|
|
238
271
|
.where(
|
|
@@ -260,6 +293,13 @@ export default defineAction({
|
|
|
260
293
|
updatedAt: now,
|
|
261
294
|
})
|
|
262
295
|
.where(eq(schema.documentPropertyDefinitions.id, args.id!));
|
|
296
|
+
await configureNaturalKey(tx, {
|
|
297
|
+
database: lockedDatabase,
|
|
298
|
+
propertyId,
|
|
299
|
+
naturalKey: args.naturalKey,
|
|
300
|
+
ownerEmail: document.ownerEmail,
|
|
301
|
+
now,
|
|
302
|
+
});
|
|
263
303
|
});
|
|
264
304
|
} else {
|
|
265
305
|
await withPositionLock(
|
|
@@ -270,6 +310,11 @@ export default defineAction({
|
|
|
270
310
|
tx as unknown as ReturnType<typeof getDb>,
|
|
271
311
|
database.id,
|
|
272
312
|
);
|
|
313
|
+
const [lockedDatabase] = await tx
|
|
314
|
+
.select()
|
|
315
|
+
.from(schema.contentDatabases)
|
|
316
|
+
.where(eq(schema.contentDatabases.id, database.id));
|
|
317
|
+
if (!lockedDatabase) throw new Error("Database not found.");
|
|
273
318
|
const [maxPos] = await tx
|
|
274
319
|
.select({
|
|
275
320
|
max: sql<number>`COALESCE(MAX(position), -1)`,
|
|
@@ -289,7 +334,7 @@ export default defineAction({
|
|
|
289
334
|
);
|
|
290
335
|
|
|
291
336
|
await tx.insert(schema.documentPropertyDefinitions).values({
|
|
292
|
-
id:
|
|
337
|
+
id: propertyId,
|
|
293
338
|
ownerEmail: document.ownerEmail,
|
|
294
339
|
orgId: document.orgId ?? null,
|
|
295
340
|
databaseId: database.id,
|
|
@@ -302,6 +347,13 @@ export default defineAction({
|
|
|
302
347
|
createdAt: now,
|
|
303
348
|
updatedAt: now,
|
|
304
349
|
});
|
|
350
|
+
await configureNaturalKey(tx, {
|
|
351
|
+
database: lockedDatabase,
|
|
352
|
+
propertyId,
|
|
353
|
+
naturalKey: args.naturalKey,
|
|
354
|
+
ownerEmail: document.ownerEmail,
|
|
355
|
+
now,
|
|
356
|
+
});
|
|
305
357
|
});
|
|
306
358
|
},
|
|
307
359
|
);
|
|
@@ -316,3 +368,120 @@ export default defineAction({
|
|
|
316
368
|
};
|
|
317
369
|
},
|
|
318
370
|
});
|
|
371
|
+
|
|
372
|
+
async function configureNaturalKey(
|
|
373
|
+
tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0],
|
|
374
|
+
args: {
|
|
375
|
+
database: typeof schema.contentDatabases.$inferSelect;
|
|
376
|
+
propertyId: string;
|
|
377
|
+
naturalKey: boolean | undefined;
|
|
378
|
+
ownerEmail: string;
|
|
379
|
+
now: string;
|
|
380
|
+
},
|
|
381
|
+
) {
|
|
382
|
+
if (args.naturalKey === undefined) return;
|
|
383
|
+
if (!args.naturalKey) {
|
|
384
|
+
if (args.database.naturalKeyPropertyId === args.propertyId) {
|
|
385
|
+
await tx
|
|
386
|
+
.delete(schema.contentDatabaseItemKeyClaims)
|
|
387
|
+
.where(
|
|
388
|
+
and(
|
|
389
|
+
eq(
|
|
390
|
+
schema.contentDatabaseItemKeyClaims.databaseId,
|
|
391
|
+
args.database.id,
|
|
392
|
+
),
|
|
393
|
+
eq(schema.contentDatabaseItemKeyClaims.propertyId, args.propertyId),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
await tx
|
|
397
|
+
.update(schema.contentDatabases)
|
|
398
|
+
.set({ naturalKeyPropertyId: null, updatedAt: args.now })
|
|
399
|
+
.where(eq(schema.contentDatabases.id, args.database.id));
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (
|
|
404
|
+
args.database.naturalKeyPropertyId &&
|
|
405
|
+
args.database.naturalKeyPropertyId !== args.propertyId
|
|
406
|
+
) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
"Clear the existing database natural key before configuring another one.",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const [sourceField] = await tx
|
|
412
|
+
.select({ id: schema.contentDatabaseSourceFields.id })
|
|
413
|
+
.from(schema.contentDatabaseSourceFields)
|
|
414
|
+
.where(eq(schema.contentDatabaseSourceFields.propertyId, args.propertyId))
|
|
415
|
+
.limit(1);
|
|
416
|
+
if (sourceField) {
|
|
417
|
+
throw new Error("A source-managed property cannot be a natural key.");
|
|
418
|
+
}
|
|
419
|
+
const values = await tx
|
|
420
|
+
.select({
|
|
421
|
+
valueJson: schema.documentPropertyValues.valueJson,
|
|
422
|
+
itemId: schema.contentDatabaseItems.id,
|
|
423
|
+
documentId: schema.contentDatabaseItems.documentId,
|
|
424
|
+
})
|
|
425
|
+
.from(schema.contentDatabaseItems)
|
|
426
|
+
.innerJoin(
|
|
427
|
+
schema.documentPropertyValues,
|
|
428
|
+
and(
|
|
429
|
+
eq(
|
|
430
|
+
schema.documentPropertyValues.documentId,
|
|
431
|
+
schema.contentDatabaseItems.documentId,
|
|
432
|
+
),
|
|
433
|
+
eq(schema.documentPropertyValues.propertyId, args.propertyId),
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
.where(eq(schema.contentDatabaseItems.databaseId, args.database.id));
|
|
437
|
+
const claims = new Map<string, (typeof values)[number]>();
|
|
438
|
+
for (const value of values) {
|
|
439
|
+
let parsed: unknown;
|
|
440
|
+
try {
|
|
441
|
+
parsed = JSON.parse(value.valueJson);
|
|
442
|
+
} catch {
|
|
443
|
+
throw new Error("Natural key values must be readable strings.");
|
|
444
|
+
}
|
|
445
|
+
if (parsed === null || parsed === "") continue;
|
|
446
|
+
if (typeof parsed !== "string") {
|
|
447
|
+
throw new Error("Natural key values must be non-empty strings.");
|
|
448
|
+
}
|
|
449
|
+
if (!parsed.trim()) {
|
|
450
|
+
throw new Error("Natural key values must be non-empty strings.");
|
|
451
|
+
}
|
|
452
|
+
if (claims.has(value.valueJson)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Natural key value ${value.valueJson} belongs to more than one row.`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
claims.set(value.valueJson, value);
|
|
458
|
+
}
|
|
459
|
+
await tx
|
|
460
|
+
.delete(schema.contentDatabaseItemKeyClaims)
|
|
461
|
+
.where(
|
|
462
|
+
and(
|
|
463
|
+
eq(schema.contentDatabaseItemKeyClaims.databaseId, args.database.id),
|
|
464
|
+
eq(schema.contentDatabaseItemKeyClaims.propertyId, args.propertyId),
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
if (claims.size > 0) {
|
|
468
|
+
await tx.insert(schema.contentDatabaseItemKeyClaims).values(
|
|
469
|
+
[...claims.entries()].map(([keyValueJson, value]) => ({
|
|
470
|
+
id: nanoid(),
|
|
471
|
+
ownerEmail: args.ownerEmail,
|
|
472
|
+
orgId: args.database.orgId,
|
|
473
|
+
databaseId: args.database.id,
|
|
474
|
+
propertyId: args.propertyId,
|
|
475
|
+
keyValueJson,
|
|
476
|
+
itemId: value.itemId,
|
|
477
|
+
documentId: value.documentId,
|
|
478
|
+
createdAt: args.now,
|
|
479
|
+
updatedAt: args.now,
|
|
480
|
+
})),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
await tx
|
|
484
|
+
.update(schema.contentDatabases)
|
|
485
|
+
.set({ naturalKeyPropertyId: args.propertyId, updatedAt: args.now })
|
|
486
|
+
.where(eq(schema.contentDatabases.id, args.database.id));
|
|
487
|
+
}
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
parsePropertyOptions,
|
|
12
12
|
type DocumentPropertyType,
|
|
13
13
|
} from "../shared/properties.js";
|
|
14
|
+
import {
|
|
15
|
+
deleteBlocksFieldIdentity,
|
|
16
|
+
lockPrimaryBlocksFieldsForDocuments,
|
|
17
|
+
} from "./_blocks-field-identity.js";
|
|
14
18
|
import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
|
|
15
19
|
import { lockDatabaseMemberships } from "./_database-membership-lock.js";
|
|
16
20
|
import {
|
|
@@ -65,14 +69,14 @@ export default defineAction({
|
|
|
65
69
|
tx as unknown as ReturnType<typeof getDb>,
|
|
66
70
|
database.id,
|
|
67
71
|
);
|
|
68
|
-
const
|
|
69
|
-
.select({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
const [lockedDatabase] = await tx
|
|
73
|
+
.select({
|
|
74
|
+
naturalKeyPropertyId: schema.contentDatabases.naturalKeyPropertyId,
|
|
75
|
+
})
|
|
76
|
+
.from(schema.contentDatabases)
|
|
77
|
+
.where(eq(schema.contentDatabases.id, database.id));
|
|
78
|
+
if (!lockedDatabase)
|
|
79
|
+
throw new Error(`Database "${database.id}" not found`);
|
|
76
80
|
const [lockedDefinition] = await tx
|
|
77
81
|
.select()
|
|
78
82
|
.from(schema.documentPropertyDefinitions)
|
|
@@ -100,6 +104,25 @@ export default defineAction({
|
|
|
100
104
|
isPrimaryBlocksField(
|
|
101
105
|
parsePropertyOptions(lockedDefinition.optionsJson),
|
|
102
106
|
);
|
|
107
|
+
const items = await tx
|
|
108
|
+
.select({
|
|
109
|
+
id: schema.contentDatabaseItems.id,
|
|
110
|
+
documentId: schema.contentDatabaseItems.documentId,
|
|
111
|
+
})
|
|
112
|
+
.from(schema.contentDatabaseItems)
|
|
113
|
+
.where(eq(schema.contentDatabaseItems.databaseId, database.id));
|
|
114
|
+
const primaryFieldsByDocument = isPrimaryBlocks
|
|
115
|
+
? await lockPrimaryBlocksFieldsForDocuments(
|
|
116
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
117
|
+
items.map((item) => item.documentId),
|
|
118
|
+
)
|
|
119
|
+
: new Map();
|
|
120
|
+
if (!isPrimaryBlocks) {
|
|
121
|
+
await lockDatabaseMemberships(
|
|
122
|
+
tx,
|
|
123
|
+
items.map((item) => item.id),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
103
126
|
|
|
104
127
|
await tx
|
|
105
128
|
.delete(schema.documentPropertyValues)
|
|
@@ -116,7 +139,21 @@ export default defineAction({
|
|
|
116
139
|
.delete(schema.documentPropertyDefinitions)
|
|
117
140
|
.where(eq(schema.documentPropertyDefinitions.id, propertyId));
|
|
118
141
|
|
|
142
|
+
if (lockedDatabase.naturalKeyPropertyId === propertyId) {
|
|
143
|
+
await tx
|
|
144
|
+
.update(schema.contentDatabases)
|
|
145
|
+
.set({
|
|
146
|
+
naturalKeyPropertyId: null,
|
|
147
|
+
updatedAt: new Date().toISOString(),
|
|
148
|
+
})
|
|
149
|
+
.where(eq(schema.contentDatabases.id, database.id));
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
if (isBlocks) {
|
|
153
|
+
await deleteBlocksFieldIdentity({
|
|
154
|
+
db: tx as unknown as ReturnType<typeof getDb>,
|
|
155
|
+
propertyId,
|
|
156
|
+
});
|
|
120
157
|
await tx
|
|
121
158
|
.delete(schema.documentBlockFieldContents)
|
|
122
159
|
.where(eq(schema.documentBlockFieldContents.propertyId, propertyId));
|
|
@@ -130,20 +167,22 @@ export default defineAction({
|
|
|
130
167
|
})
|
|
131
168
|
.where(eq(schema.contentDatabases.id, database.id));
|
|
132
169
|
|
|
133
|
-
const
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
const documentsWithoutSurvivingPrimary = items
|
|
171
|
+
.filter(
|
|
172
|
+
(item) =>
|
|
173
|
+
!(primaryFieldsByDocument.get(item.documentId) ?? []).some(
|
|
174
|
+
(field: { propertyId: string }) =>
|
|
175
|
+
field.propertyId !== propertyId,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
.map((item) => item.documentId);
|
|
179
|
+
if (documentsWithoutSurvivingPrimary.length > 0) {
|
|
138
180
|
const now = new Date().toISOString();
|
|
139
181
|
await tx
|
|
140
182
|
.update(schema.documents)
|
|
141
183
|
.set({ content: "", updatedAt: now })
|
|
142
184
|
.where(
|
|
143
|
-
inArray(
|
|
144
|
-
schema.documents.id,
|
|
145
|
-
items.map((item) => item.documentId),
|
|
146
|
-
),
|
|
185
|
+
inArray(schema.documents.id, documentsWithoutSurvivingPrimary),
|
|
147
186
|
);
|
|
148
187
|
}
|
|
149
188
|
}
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
|
|
7
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
8
8
|
import { chunks } from "./_batch-utils.js";
|
|
9
|
+
import { deleteBlocksFieldIdentity } from "./_blocks-field-identity.js";
|
|
9
10
|
import {
|
|
10
11
|
lockContentDatabaseMutation,
|
|
11
12
|
touchContentDatabase,
|
|
@@ -695,6 +696,7 @@ async function deleteCollectedDocuments(
|
|
|
695
696
|
});
|
|
696
697
|
|
|
697
698
|
await deleteWhereIn(propertyDefinitionIds, async (propertyIdBatch) => {
|
|
699
|
+
await deleteBlocksFieldIdentity({ db, propertyIds: propertyIdBatch });
|
|
698
700
|
await db
|
|
699
701
|
.delete(schema.documentPropertyValues)
|
|
700
702
|
.where(
|
|
@@ -730,6 +732,7 @@ async function deleteCollectedDocuments(
|
|
|
730
732
|
});
|
|
731
733
|
|
|
732
734
|
await deleteWhereIn(documentIds, async (documentIdBatch) => {
|
|
735
|
+
await deleteBlocksFieldIdentity({ db, documentIds: documentIdBatch });
|
|
733
736
|
await db
|
|
734
737
|
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
735
738
|
.where(
|
|
@@ -17,6 +17,10 @@ import { eq } from "drizzle-orm";
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
|
|
19
19
|
import { getDb, schema } from "../server/db/index.js";
|
|
20
|
+
import {
|
|
21
|
+
lockPrimaryBlocksFields,
|
|
22
|
+
persistBlocksFieldIdentity,
|
|
23
|
+
} from "./_blocks-field-identity.js";
|
|
20
24
|
|
|
21
25
|
interface TextEdit {
|
|
22
26
|
find: string;
|
|
@@ -235,11 +239,24 @@ export default defineAction({
|
|
|
235
239
|
// Persist. The fresh updatedAt is the signal the open editor uses to tell an
|
|
236
240
|
// intentional external edit apart from a stale autosave echo.
|
|
237
241
|
const db = getDb();
|
|
242
|
+
const now = new Date().toISOString();
|
|
238
243
|
await db.transaction(async (tx: any) => {
|
|
244
|
+
const primaryBlocksFields = await lockPrimaryBlocksFields(tx, id);
|
|
239
245
|
await tx
|
|
240
246
|
.update(schema.documents)
|
|
241
|
-
.set({ content, updatedAt:
|
|
247
|
+
.set({ content, updatedAt: now })
|
|
242
248
|
.where(eq(schema.documents.id, id));
|
|
249
|
+
for (const field of primaryBlocksFields) {
|
|
250
|
+
await persistBlocksFieldIdentity({
|
|
251
|
+
db: tx as unknown as ReturnType<typeof getDb>,
|
|
252
|
+
ownerEmail: field.ownerEmail,
|
|
253
|
+
documentId: id,
|
|
254
|
+
propertyId: field.propertyId,
|
|
255
|
+
previousMarkdown: existing.content ?? "",
|
|
256
|
+
markdown: content,
|
|
257
|
+
now,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
243
260
|
if (creativeContext) {
|
|
244
261
|
await recordGenerationCreativeContext(
|
|
245
262
|
{
|
|
@@ -3,8 +3,14 @@ import { buildDeepLink } from "@agent-native/core/server";
|
|
|
3
3
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
+
import { blocksContentHash } from "../shared/blocks-field-identity.js";
|
|
6
7
|
import { buildDocumentExport } from "../shared/document-export.js";
|
|
8
|
+
import {
|
|
9
|
+
isBlocksPropertyType,
|
|
10
|
+
isPrimaryBlocksField,
|
|
11
|
+
} from "../shared/properties.js";
|
|
7
12
|
import "../server/db/index.js";
|
|
13
|
+
import { listPropertiesForAllDocumentDatabases } from "./_property-utils.js";
|
|
8
14
|
|
|
9
15
|
export default defineAction({
|
|
10
16
|
description:
|
|
@@ -33,12 +39,47 @@ export default defineAction({
|
|
|
33
39
|
if (!access) throw new Error(`Document "${id}" not found`);
|
|
34
40
|
|
|
35
41
|
const doc = access.resource;
|
|
42
|
+
const properties = await listPropertiesForAllDocumentDatabases(doc);
|
|
43
|
+
const blocksFields = properties
|
|
44
|
+
.filter((property) => isBlocksPropertyType(property.definition.type))
|
|
45
|
+
.map((property) => {
|
|
46
|
+
if (!property.definition.databaseId) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Blocks field "${property.definition.id}" is not attached to a database`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!property.blocksField) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Blocks field "${property.definition.id}" has no identity state`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const markdown =
|
|
57
|
+
content !== undefined &&
|
|
58
|
+
isPrimaryBlocksField(property.definition.options)
|
|
59
|
+
? content
|
|
60
|
+
: typeof property.value === "string"
|
|
61
|
+
? property.value
|
|
62
|
+
: "";
|
|
63
|
+
const identity =
|
|
64
|
+
blocksContentHash(markdown) === property.blocksField.contentHash
|
|
65
|
+
? property.blocksField
|
|
66
|
+
: { ...property.blocksField, identityStatus: "stale" as const };
|
|
67
|
+
return {
|
|
68
|
+
databaseId: property.definition.databaseId,
|
|
69
|
+
propertyId: property.definition.id,
|
|
70
|
+
name: property.definition.name,
|
|
71
|
+
position: property.definition.position,
|
|
72
|
+
markdown,
|
|
73
|
+
identity,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
36
76
|
const payload = buildDocumentExport({
|
|
37
77
|
id: doc.id,
|
|
38
78
|
title: title ?? doc.title,
|
|
39
79
|
content: content ?? doc.content,
|
|
40
80
|
updatedAt: doc.updatedAt,
|
|
41
81
|
format,
|
|
82
|
+
blocksFields,
|
|
42
83
|
});
|
|
43
84
|
|
|
44
85
|
return {
|
|
@@ -4,6 +4,7 @@ import { assertAccess } from "@agent-native/core/sharing";
|
|
|
4
4
|
import { and, eq, inArray } from "drizzle-orm";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { deleteBlocksFieldIdentity } from "./_blocks-field-identity.js";
|
|
7
8
|
import {
|
|
8
9
|
lockContentDatabaseMutation,
|
|
9
10
|
touchContentDatabase,
|
|
@@ -132,6 +133,15 @@ export default defineAction({
|
|
|
132
133
|
)
|
|
133
134
|
).map((property) => property.id);
|
|
134
135
|
if (removedDocumentIds.length > 0 && propertyIds.length > 0) {
|
|
136
|
+
for (const removedDocumentId of removedDocumentIds) {
|
|
137
|
+
for (const propertyId of propertyIds) {
|
|
138
|
+
await deleteBlocksFieldIdentity({
|
|
139
|
+
db: tx as unknown as ReturnType<typeof getDb>,
|
|
140
|
+
documentId: removedDocumentId,
|
|
141
|
+
propertyId,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
135
145
|
await tx
|
|
136
146
|
.delete(schema.documentPropertyValues)
|
|
137
147
|
.where(
|