@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.
Files changed (64) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
  3. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
  4. package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
  5. package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
  6. package/corpus/templates/content/actions/_database-utils.ts +55 -3
  7. package/corpus/templates/content/actions/_property-utils.ts +141 -31
  8. package/corpus/templates/content/actions/add-database-item.ts +62 -240
  9. package/corpus/templates/content/actions/configure-document-property.ts +170 -1
  10. package/corpus/templates/content/actions/delete-document-property.ts +56 -17
  11. package/corpus/templates/content/actions/delete-document.ts +3 -0
  12. package/corpus/templates/content/actions/edit-document.ts +18 -1
  13. package/corpus/templates/content/actions/export-document.ts +41 -0
  14. package/corpus/templates/content/actions/remove-database-items.ts +10 -0
  15. package/corpus/templates/content/actions/set-document-property.ts +140 -3
  16. package/corpus/templates/content/actions/update-database-item.ts +62 -0
  17. package/corpus/templates/content/actions/update-document.ts +24 -0
  18. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
  19. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
  20. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
  21. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
  22. package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
  23. package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
  24. package/corpus/templates/content/app/i18n-data.ts +20 -0
  25. package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
  26. package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
  27. package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
  28. package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
  29. package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
  30. package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
  31. package/corpus/templates/content/parity/matrix.md +1 -1
  32. package/corpus/templates/content/parity/matrix.ts +2 -0
  33. package/corpus/templates/content/server/db/schema.ts +100 -3
  34. package/corpus/templates/content/server/plugins/db.ts +85 -0
  35. package/corpus/templates/content/shared/api.ts +78 -2
  36. package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
  37. package/corpus/templates/content/shared/document-export.ts +33 -1
  38. package/dist/action.d.ts +22 -0
  39. package/dist/action.js +27 -0
  40. package/dist/collab/awareness.d.ts +2 -2
  41. package/dist/collab/routes.d.ts +1 -1
  42. package/dist/extensions/url-safety.d.ts +7 -2
  43. package/dist/extensions/url-safety.js +27 -12
  44. package/dist/index.browser.d.ts +1 -1
  45. package/dist/index.browser.js +1 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/mcp-client/oauth-client.d.ts +16 -1
  49. package/dist/mcp-client/oauth-client.js +177 -83
  50. package/dist/mcp-client/remote-store.js +46 -20
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/oauth-tokens/index.d.ts +2 -1
  53. package/dist/oauth-tokens/index.js +2 -1
  54. package/dist/oauth-tokens/lifecycle.d.ts +87 -0
  55. package/dist/oauth-tokens/lifecycle.js +434 -0
  56. package/dist/oauth-tokens/migrations.js +10 -0
  57. package/dist/oauth-tokens/store.d.ts +22 -0
  58. package/dist/oauth-tokens/store.js +141 -18
  59. package/dist/observability/routes.d.ts +2 -2
  60. package/dist/progress/routes.d.ts +1 -1
  61. package/dist/secrets/routes.d.ts +6 -6
  62. package/dist/server/action-routes.js +10 -2
  63. package/dist/server/realtime-token.d.ts +1 -1
  64. package/package.json +2 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8208
34
+ - template files: 8215
@@ -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
- `createdDocumentId` and verification. Return the exact `url` or `urlPath`
125
- from the result. The canonical Content row route is `/page/<createdDocumentId>`;
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
- `--propertyValues '{"propertyId":"value"}'` to `add-database-item`. Filter
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 the created item IDs and
172
- opens the new row page in the side preview. Duplicating a database row
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`, `duplicate-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
+ }