@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
package/corpus/README.md
CHANGED
|
@@ -121,8 +121,8 @@ For a named intake workflow:
|
|
|
121
121
|
when the database has no form contract and all required values have already
|
|
122
122
|
been confirmed.
|
|
123
123
|
8. Treat submission as complete only when the successful result includes a
|
|
124
|
-
|
|
125
|
-
from the result. The canonical Content row route is `/page/<
|
|
124
|
+
stable row IDs and verified read-back. Return the exact `url` or `urlPath`
|
|
125
|
+
from the result. The canonical Content row route is `/page/<documentId>`;
|
|
126
126
|
never invent a different path, slug, ID, or host.
|
|
127
127
|
|
|
128
128
|
When the user supplies a complete description in one message, do not force a
|
|
@@ -117,7 +117,9 @@ checkbox filters as initial property values, resolving option labels back to
|
|
|
117
117
|
stable option IDs for select, status, and multi-select filters, so a row
|
|
118
118
|
created under "Status is Published" remains visible instead of immediately
|
|
119
119
|
disappearing. Agents can mirror that behavior by passing
|
|
120
|
-
|
|
120
|
+
the discovered property IDs in `propertyValues` to `add-database-item`. The
|
|
121
|
+
action also requires the exact space/database/backing-page target, current
|
|
122
|
+
schema revision, and a caller-stable idempotency key. Filter
|
|
121
123
|
controls are type-aware: option properties choose from their configured
|
|
122
124
|
options, option value editors can search existing options or create a new
|
|
123
125
|
option from the typed query, and property settings can rename option labels
|
|
@@ -168,8 +170,10 @@ memberships, but they are references: moving one never reparents, transfers,
|
|
|
168
170
|
or changes access to the referenced page. Files sidebar Custom order is
|
|
169
171
|
different again: persist it per user and per database view with
|
|
170
172
|
`update-content-database-personal-view`, without changing the shared Files
|
|
171
|
-
membership order. Creating a database row returns
|
|
172
|
-
|
|
173
|
+
membership order. Creating a database row returns a receipt with stable item
|
|
174
|
+
and document IDs, row link, revisions, affected fields, idempotency outcome,
|
|
175
|
+
and verified read-back, then opens the new row page in the side preview.
|
|
176
|
+
Duplicating a database row
|
|
173
177
|
returns the duplicate item IDs and opens the copied row in the side preview
|
|
174
178
|
so users can continue editing the new page immediately, including from
|
|
175
179
|
table, list, and gallery row action menus. Board, calendar, and timeline
|
|
@@ -227,13 +231,25 @@ property definition.
|
|
|
227
231
|
|
|
228
232
|
Use `create-content-database`, `create-inline-content-database`,
|
|
229
233
|
`get-content-database`, `list-trashed-content-databases`,
|
|
230
|
-
`restore-content-database`, `add-database-item`, `
|
|
234
|
+
`restore-content-database`, `add-database-item`, `update-database-item`,
|
|
235
|
+
`upsert-database-item-by-key`, `duplicate-database-item`,
|
|
231
236
|
`duplicate-database-items`, `remove-database-items`, `move-database-item`,
|
|
232
237
|
`update-content-database-view`, `list-document-properties`,
|
|
233
238
|
`configure-document-property`, `set-document-property`,
|
|
234
239
|
`duplicate-document-property`, and `delete-document-property`; do not edit
|
|
235
240
|
property rows or view config via raw SQL when an action can do it.
|
|
236
241
|
|
|
242
|
+
Read `get-content-database.mutationContract` immediately before a single-row
|
|
243
|
+
mutation. Pass its exact target and schema revision to create, exact item and
|
|
244
|
+
document IDs plus the current row revision to sparse update, and a fresh
|
|
245
|
+
idempotency key for each intended effect. Reusing the same key with the same
|
|
246
|
+
payload replays the durable receipt; reusing it with a different payload fails.
|
|
247
|
+
Configure at most one ordinary text property as the database's natural key,
|
|
248
|
+
then use `upsert-database-item-by-key` with that property. Natural-key upsert
|
|
249
|
+
never accepts an arbitrary property name or silently chooses a field. Blocks,
|
|
250
|
+
computed, system, source-managed, unknown, and relation properties are not
|
|
251
|
+
writable through these actions; use their owning surfaces instead.
|
|
252
|
+
|
|
237
253
|
For a bounded migration that must rewrite every existing row body while adding
|
|
238
254
|
new property definitions and values, use `migrate-content-database-rows` rather
|
|
239
255
|
than looping the single-row actions. Its `validate` phase is read-only; `apply`
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
4
|
+
import {
|
|
5
|
+
blocksContentHash,
|
|
6
|
+
blocksFieldId,
|
|
7
|
+
exposeBlocksFieldIdentity,
|
|
8
|
+
legacyBlocksFieldIdentity,
|
|
9
|
+
materializeLegacyBlocksFieldIdentity,
|
|
10
|
+
reconcileBlocksFieldIdentity,
|
|
11
|
+
type BlocksFieldIdentity,
|
|
12
|
+
type StoredBlocksFieldIdentity,
|
|
13
|
+
} from "../shared/blocks-field-identity.js";
|
|
14
|
+
import { lockDatabaseMemberships } from "./_database-membership-lock.js";
|
|
15
|
+
|
|
16
|
+
type ContentDb = ReturnType<typeof getDb>;
|
|
17
|
+
|
|
18
|
+
export interface PrimaryBlocksField {
|
|
19
|
+
propertyId: string;
|
|
20
|
+
ownerEmail: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BlocksFieldRevisionConflictError extends Error {
|
|
24
|
+
readonly statusCode = 409;
|
|
25
|
+
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "BlocksFieldRevisionConflictError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nanoid(size = 12): string {
|
|
33
|
+
const chars =
|
|
34
|
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
35
|
+
let id = "";
|
|
36
|
+
const bytes = crypto.getRandomValues(new Uint8Array(size));
|
|
37
|
+
for (const byte of bytes) id += chars[byte % chars.length];
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function groups<T>(values: T[], size: number): T[][] {
|
|
42
|
+
const result: T[][] = [];
|
|
43
|
+
for (let index = 0; index < values.length; index += size) {
|
|
44
|
+
result.push(values.slice(index, index + size));
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function lockPrimaryBlocksFields(
|
|
50
|
+
db: ContentDb,
|
|
51
|
+
documentId: string,
|
|
52
|
+
): Promise<PrimaryBlocksField[]> {
|
|
53
|
+
return (
|
|
54
|
+
(await lockPrimaryBlocksFieldsForDocuments(db, [documentId])).get(
|
|
55
|
+
documentId,
|
|
56
|
+
) ?? []
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function lockPrimaryBlocksFieldsForDocuments(
|
|
61
|
+
db: ContentDb,
|
|
62
|
+
documentIds: string[],
|
|
63
|
+
): Promise<Map<string, PrimaryBlocksField[]>> {
|
|
64
|
+
const uniqueDocumentIds = [...new Set(documentIds)];
|
|
65
|
+
const memberships: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
documentId: string;
|
|
68
|
+
}> = [];
|
|
69
|
+
for (const documentIdGroup of groups(uniqueDocumentIds, 90)) {
|
|
70
|
+
memberships.push(
|
|
71
|
+
...(await db
|
|
72
|
+
.select({
|
|
73
|
+
id: schema.contentDatabaseItems.id,
|
|
74
|
+
documentId: schema.contentDatabaseItems.documentId,
|
|
75
|
+
})
|
|
76
|
+
.from(schema.contentDatabaseItems)
|
|
77
|
+
.where(
|
|
78
|
+
inArray(schema.contentDatabaseItems.documentId, documentIdGroup),
|
|
79
|
+
)),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const membershipIds = memberships.map((membership) => membership.id);
|
|
83
|
+
await lockDatabaseMemberships(db, membershipIds);
|
|
84
|
+
if (membershipIds.length === 0) return new Map();
|
|
85
|
+
|
|
86
|
+
const fields: Array<{
|
|
87
|
+
documentId: string;
|
|
88
|
+
propertyId: string | null;
|
|
89
|
+
ownerEmail: string;
|
|
90
|
+
}> = [];
|
|
91
|
+
for (const membershipIdGroup of groups(membershipIds, 90)) {
|
|
92
|
+
fields.push(
|
|
93
|
+
...(await db
|
|
94
|
+
.select({
|
|
95
|
+
documentId: schema.contentDatabaseItems.documentId,
|
|
96
|
+
propertyId: schema.contentDatabases.primaryBlocksPropertyId,
|
|
97
|
+
ownerEmail: schema.contentDatabases.ownerEmail,
|
|
98
|
+
})
|
|
99
|
+
.from(schema.contentDatabaseItems)
|
|
100
|
+
.innerJoin(
|
|
101
|
+
schema.contentDatabases,
|
|
102
|
+
eq(
|
|
103
|
+
schema.contentDatabases.id,
|
|
104
|
+
schema.contentDatabaseItems.databaseId,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
.where(inArray(schema.contentDatabaseItems.id, membershipIdGroup))),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const fieldsByDocument = new Map<string, Map<string, string>>();
|
|
111
|
+
for (const field of fields) {
|
|
112
|
+
if (!field.propertyId) continue;
|
|
113
|
+
const documentFields =
|
|
114
|
+
fieldsByDocument.get(field.documentId) ?? new Map<string, string>();
|
|
115
|
+
documentFields.set(field.propertyId, field.ownerEmail);
|
|
116
|
+
fieldsByDocument.set(field.documentId, documentFields);
|
|
117
|
+
}
|
|
118
|
+
return new Map(
|
|
119
|
+
[...fieldsByDocument].map(([id, documentFields]) => [
|
|
120
|
+
id,
|
|
121
|
+
[...documentFields].map(([propertyId, ownerEmail]) => ({
|
|
122
|
+
propertyId,
|
|
123
|
+
ownerEmail,
|
|
124
|
+
})),
|
|
125
|
+
]),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function loadStoredIdentity(
|
|
130
|
+
db: ContentDb,
|
|
131
|
+
fieldId: string,
|
|
132
|
+
): Promise<StoredBlocksFieldIdentity | null> {
|
|
133
|
+
const [field] = await db
|
|
134
|
+
.select()
|
|
135
|
+
.from(schema.documentBlockFields)
|
|
136
|
+
.where(eq(schema.documentBlockFields.id, fieldId));
|
|
137
|
+
if (!field) return null;
|
|
138
|
+
const blocks = await db
|
|
139
|
+
.select()
|
|
140
|
+
.from(schema.documentBlocks)
|
|
141
|
+
.where(eq(schema.documentBlocks.fieldId, fieldId))
|
|
142
|
+
.orderBy(asc(schema.documentBlocks.sortIndex));
|
|
143
|
+
return {
|
|
144
|
+
fieldId,
|
|
145
|
+
revision: field.revision,
|
|
146
|
+
contentHash: field.contentHash,
|
|
147
|
+
blocks: blocks.map((block) => ({
|
|
148
|
+
id: block.id,
|
|
149
|
+
parentId: block.parentId,
|
|
150
|
+
kind: block.kind,
|
|
151
|
+
position: block.position,
|
|
152
|
+
addressable: block.addressable,
|
|
153
|
+
contentHash: block.contentHash,
|
|
154
|
+
markdown: block.markdown,
|
|
155
|
+
state: block.state === "deleted" ? "deleted" : "live",
|
|
156
|
+
deletedAtRevision: block.deletedAtRevision,
|
|
157
|
+
recoveredAtRevision: block.recoveredAtRevision,
|
|
158
|
+
})),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function readBlocksFieldIdentity(args: {
|
|
163
|
+
db?: ContentDb;
|
|
164
|
+
documentId: string;
|
|
165
|
+
propertyId: string;
|
|
166
|
+
markdown: string;
|
|
167
|
+
}): Promise<BlocksFieldIdentity> {
|
|
168
|
+
const fieldId = blocksFieldId(args.documentId, args.propertyId);
|
|
169
|
+
const stored = await loadStoredIdentity(args.db ?? getDb(), fieldId);
|
|
170
|
+
return stored
|
|
171
|
+
? exposeBlocksFieldIdentity(stored, args.markdown)
|
|
172
|
+
: legacyBlocksFieldIdentity(args);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function readBlocksFieldIdentities(args: {
|
|
176
|
+
db?: ContentDb;
|
|
177
|
+
fields: Array<{
|
|
178
|
+
documentId: string;
|
|
179
|
+
propertyId: string;
|
|
180
|
+
markdown: string;
|
|
181
|
+
}>;
|
|
182
|
+
}): Promise<Map<string, BlocksFieldIdentity>> {
|
|
183
|
+
const db = args.db ?? getDb();
|
|
184
|
+
const inputs = new Map(
|
|
185
|
+
args.fields.map((field) => [
|
|
186
|
+
blocksFieldId(field.documentId, field.propertyId),
|
|
187
|
+
field,
|
|
188
|
+
]),
|
|
189
|
+
);
|
|
190
|
+
const storedFields: Array<typeof schema.documentBlockFields.$inferSelect> =
|
|
191
|
+
[];
|
|
192
|
+
for (const ids of groups([...inputs.keys()], 200)) {
|
|
193
|
+
if (ids.length === 0) continue;
|
|
194
|
+
storedFields.push(
|
|
195
|
+
...(await db
|
|
196
|
+
.select()
|
|
197
|
+
.from(schema.documentBlockFields)
|
|
198
|
+
.where(inArray(schema.documentBlockFields.id, ids))),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const storedById = new Map(storedFields.map((field) => [field.id, field]));
|
|
202
|
+
const storedBlocks: Array<typeof schema.documentBlocks.$inferSelect> = [];
|
|
203
|
+
for (const ids of groups(
|
|
204
|
+
storedFields.map((field) => field.id),
|
|
205
|
+
200,
|
|
206
|
+
)) {
|
|
207
|
+
if (ids.length === 0) continue;
|
|
208
|
+
storedBlocks.push(
|
|
209
|
+
...(await db
|
|
210
|
+
.select()
|
|
211
|
+
.from(schema.documentBlocks)
|
|
212
|
+
.where(inArray(schema.documentBlocks.fieldId, ids))
|
|
213
|
+
.orderBy(asc(schema.documentBlocks.sortIndex))),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const blocksByField = new Map<
|
|
217
|
+
string,
|
|
218
|
+
Array<typeof schema.documentBlocks.$inferSelect>
|
|
219
|
+
>();
|
|
220
|
+
for (const block of storedBlocks) {
|
|
221
|
+
const values = blocksByField.get(block.fieldId) ?? [];
|
|
222
|
+
values.push(block);
|
|
223
|
+
blocksByField.set(block.fieldId, values);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = new Map<string, BlocksFieldIdentity>();
|
|
227
|
+
for (const [fieldId, input] of inputs) {
|
|
228
|
+
const field = storedById.get(fieldId);
|
|
229
|
+
if (!field) {
|
|
230
|
+
result.set(fieldId, legacyBlocksFieldIdentity(input));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
result.set(
|
|
234
|
+
fieldId,
|
|
235
|
+
exposeBlocksFieldIdentity(
|
|
236
|
+
{
|
|
237
|
+
fieldId,
|
|
238
|
+
revision: field.revision,
|
|
239
|
+
contentHash: field.contentHash,
|
|
240
|
+
blocks: (blocksByField.get(fieldId) ?? []).map((block) => ({
|
|
241
|
+
id: block.id,
|
|
242
|
+
parentId: block.parentId,
|
|
243
|
+
kind: block.kind,
|
|
244
|
+
position: block.position,
|
|
245
|
+
addressable: block.addressable,
|
|
246
|
+
contentHash: block.contentHash,
|
|
247
|
+
markdown: block.markdown,
|
|
248
|
+
state: block.state === "deleted" ? "deleted" : "live",
|
|
249
|
+
deletedAtRevision: block.deletedAtRevision,
|
|
250
|
+
recoveredAtRevision: block.recoveredAtRevision,
|
|
251
|
+
})),
|
|
252
|
+
},
|
|
253
|
+
input.markdown,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function persistBlocksFieldIdentity(args: {
|
|
261
|
+
db: ContentDb;
|
|
262
|
+
ownerEmail: string;
|
|
263
|
+
documentId: string;
|
|
264
|
+
propertyId: string;
|
|
265
|
+
previousMarkdown: string;
|
|
266
|
+
markdown: string;
|
|
267
|
+
expectedRevision?: number;
|
|
268
|
+
now: string;
|
|
269
|
+
}): Promise<StoredBlocksFieldIdentity> {
|
|
270
|
+
const fieldId = blocksFieldId(args.documentId, args.propertyId);
|
|
271
|
+
const stored = await loadStoredIdentity(args.db, fieldId);
|
|
272
|
+
const actualRevision = stored?.revision ?? 0;
|
|
273
|
+
if (
|
|
274
|
+
args.expectedRevision !== undefined &&
|
|
275
|
+
args.expectedRevision !== actualRevision
|
|
276
|
+
) {
|
|
277
|
+
throw new BlocksFieldRevisionConflictError(
|
|
278
|
+
`Blocks field revision conflict: expected ${args.expectedRevision}, current ${actualRevision}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let previous =
|
|
283
|
+
stored ??
|
|
284
|
+
materializeLegacyBlocksFieldIdentity({
|
|
285
|
+
documentId: args.documentId,
|
|
286
|
+
propertyId: args.propertyId,
|
|
287
|
+
markdown: args.previousMarkdown,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// A legacy whole-field writer may have changed Markdown without updating the
|
|
291
|
+
// sidecar. Reconcile exact unique blocks first and report the resulting extra
|
|
292
|
+
// revision instead of silently pretending continuity was complete.
|
|
293
|
+
if (previous.contentHash !== blocksContentHash(args.previousMarkdown)) {
|
|
294
|
+
previous = reconcileBlocksFieldIdentity({
|
|
295
|
+
documentId: args.documentId,
|
|
296
|
+
propertyId: args.propertyId,
|
|
297
|
+
previous,
|
|
298
|
+
markdown: args.previousMarkdown,
|
|
299
|
+
createId: () => `block_${nanoid(16)}`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let next = reconcileBlocksFieldIdentity({
|
|
304
|
+
documentId: args.documentId,
|
|
305
|
+
propertyId: args.propertyId,
|
|
306
|
+
previous,
|
|
307
|
+
markdown: args.markdown,
|
|
308
|
+
createId: () => `block_${nanoid(16)}`,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (next.blocks.length > 0) {
|
|
312
|
+
const existingOwners = await args.db
|
|
313
|
+
.select({
|
|
314
|
+
id: schema.documentBlocks.id,
|
|
315
|
+
fieldId: schema.documentBlocks.fieldId,
|
|
316
|
+
})
|
|
317
|
+
.from(schema.documentBlocks)
|
|
318
|
+
.where(
|
|
319
|
+
inArray(
|
|
320
|
+
schema.documentBlocks.id,
|
|
321
|
+
next.blocks.map((block) => block.id),
|
|
322
|
+
),
|
|
323
|
+
);
|
|
324
|
+
const remappedIds = new Map<string, string>();
|
|
325
|
+
const reserved = new Set(next.blocks.map((block) => block.id));
|
|
326
|
+
for (const existing of existingOwners) {
|
|
327
|
+
if (existing.fieldId === fieldId) continue;
|
|
328
|
+
let replacement = `block_${nanoid(16)}`;
|
|
329
|
+
while (reserved.has(replacement)) replacement = `block_${nanoid(16)}`;
|
|
330
|
+
reserved.add(replacement);
|
|
331
|
+
remappedIds.set(existing.id, replacement);
|
|
332
|
+
}
|
|
333
|
+
if (remappedIds.size > 0) {
|
|
334
|
+
next = {
|
|
335
|
+
...next,
|
|
336
|
+
blocks: next.blocks.map((block) => ({
|
|
337
|
+
...block,
|
|
338
|
+
id: remappedIds.get(block.id) ?? block.id,
|
|
339
|
+
parentId: block.parentId
|
|
340
|
+
? (remappedIds.get(block.parentId) ?? block.parentId)
|
|
341
|
+
: null,
|
|
342
|
+
})),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (stored) {
|
|
348
|
+
const applied = await args.db
|
|
349
|
+
.update(schema.documentBlockFields)
|
|
350
|
+
.set({
|
|
351
|
+
revision: next.revision,
|
|
352
|
+
contentHash: next.contentHash,
|
|
353
|
+
updatedAt: args.now,
|
|
354
|
+
})
|
|
355
|
+
.where(
|
|
356
|
+
and(
|
|
357
|
+
eq(schema.documentBlockFields.id, fieldId),
|
|
358
|
+
eq(schema.documentBlockFields.revision, actualRevision),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
.returning({ id: schema.documentBlockFields.id });
|
|
362
|
+
if (applied.length === 0) {
|
|
363
|
+
throw new BlocksFieldRevisionConflictError(
|
|
364
|
+
`Blocks field revision conflict: expected ${actualRevision}, current revision changed`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
const inserted = await args.db
|
|
369
|
+
.insert(schema.documentBlockFields)
|
|
370
|
+
.values({
|
|
371
|
+
id: fieldId,
|
|
372
|
+
ownerEmail: args.ownerEmail,
|
|
373
|
+
documentId: args.documentId,
|
|
374
|
+
propertyId: args.propertyId,
|
|
375
|
+
revision: next.revision,
|
|
376
|
+
contentHash: next.contentHash,
|
|
377
|
+
createdAt: args.now,
|
|
378
|
+
updatedAt: args.now,
|
|
379
|
+
})
|
|
380
|
+
.onConflictDoNothing()
|
|
381
|
+
.returning({ id: schema.documentBlockFields.id });
|
|
382
|
+
if (inserted.length === 0) {
|
|
383
|
+
throw new BlocksFieldRevisionConflictError(
|
|
384
|
+
"Blocks field revision conflict: concurrent first materialization",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
await args.db
|
|
389
|
+
.delete(schema.documentBlocks)
|
|
390
|
+
.where(eq(schema.documentBlocks.fieldId, fieldId));
|
|
391
|
+
if (next.blocks.length > 0) {
|
|
392
|
+
await args.db.insert(schema.documentBlocks).values(
|
|
393
|
+
next.blocks.map((block, sortIndex) => ({
|
|
394
|
+
id: block.id,
|
|
395
|
+
ownerEmail: args.ownerEmail,
|
|
396
|
+
fieldId,
|
|
397
|
+
parentId: block.parentId,
|
|
398
|
+
kind: block.kind,
|
|
399
|
+
position: block.position,
|
|
400
|
+
sortIndex,
|
|
401
|
+
addressable: block.addressable,
|
|
402
|
+
contentHash: block.contentHash,
|
|
403
|
+
markdown: block.markdown,
|
|
404
|
+
state: block.state,
|
|
405
|
+
deletedAtRevision: block.deletedAtRevision,
|
|
406
|
+
recoveredAtRevision: block.recoveredAtRevision,
|
|
407
|
+
createdAt: args.now,
|
|
408
|
+
updatedAt: args.now,
|
|
409
|
+
})),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return next;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function deleteBlocksFieldIdentity(args: {
|
|
416
|
+
db: ContentDb;
|
|
417
|
+
documentId?: string;
|
|
418
|
+
documentIds?: string[];
|
|
419
|
+
propertyId?: string;
|
|
420
|
+
propertyIds?: string[];
|
|
421
|
+
}): Promise<void> {
|
|
422
|
+
if (
|
|
423
|
+
!args.documentId &&
|
|
424
|
+
!args.documentIds?.length &&
|
|
425
|
+
!args.propertyId &&
|
|
426
|
+
!args.propertyIds?.length
|
|
427
|
+
) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const clauses = [];
|
|
431
|
+
if (args.documentId) {
|
|
432
|
+
clauses.push(eq(schema.documentBlockFields.documentId, args.documentId));
|
|
433
|
+
}
|
|
434
|
+
if (args.documentIds?.length) {
|
|
435
|
+
clauses.push(
|
|
436
|
+
inArray(schema.documentBlockFields.documentId, args.documentIds),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (args.propertyId) {
|
|
440
|
+
clauses.push(eq(schema.documentBlockFields.propertyId, args.propertyId));
|
|
441
|
+
}
|
|
442
|
+
if (args.propertyIds?.length) {
|
|
443
|
+
clauses.push(
|
|
444
|
+
inArray(schema.documentBlockFields.propertyId, args.propertyIds),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const fields = await args.db
|
|
448
|
+
.select({ id: schema.documentBlockFields.id })
|
|
449
|
+
.from(schema.documentBlockFields)
|
|
450
|
+
.where(clauses.length === 1 ? clauses[0] : and(...clauses));
|
|
451
|
+
if (fields.length === 0) return;
|
|
452
|
+
await args.db.delete(schema.documentBlocks).where(
|
|
453
|
+
inArray(
|
|
454
|
+
schema.documentBlocks.fieldId,
|
|
455
|
+
fields.map((field) => field.id),
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
await args.db.delete(schema.documentBlockFields).where(
|
|
459
|
+
inArray(
|
|
460
|
+
schema.documentBlockFields.id,
|
|
461
|
+
fields.map((field) => field.id),
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
}
|