@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
@@ -38,12 +38,17 @@ import {
38
38
  parsePropertyValue,
39
39
  type DocumentPropertyType,
40
40
  } from "../shared/properties.js";
41
+ import { deleteBlocksFieldIdentity } from "./_blocks-field-identity.js";
41
42
  import { favoriteDocumentIds } from "./_content-favorites.js";
42
43
  import {
43
44
  listContentOrganizationMemberships,
44
45
  normalizeContentSpaceEmail,
45
46
  resolveContentSpaceAccess,
46
47
  } from "./_content-space-access.js";
48
+ import {
49
+ databaseRowRevision,
50
+ getDatabaseMutationContract,
51
+ } from "./_database-row-mutation.js";
47
52
  import { getAllContentDatabaseSourceSnapshots } from "./_database-source-utils.js";
48
53
  import {
49
54
  applyFederatedOverlayValues,
@@ -1049,6 +1054,21 @@ export async function getContentDatabasePageResponse(
1049
1054
  queued: bodyHydrationQueued,
1050
1055
  }),
1051
1056
  properties: propertiesByDocumentId.get(document.id) ?? [],
1057
+ rowRevision: databaseRowRevision({
1058
+ itemId: item.id,
1059
+ documentId: document.id,
1060
+ title: document.title,
1061
+ values: (propertiesByDocumentId.get(document.id) ?? [])
1062
+ .filter(
1063
+ (property) =>
1064
+ !isBlocksPropertyType(property.definition.type) &&
1065
+ !isComputedPropertyType(property.definition.type),
1066
+ )
1067
+ .map((property) => ({
1068
+ propertyId: property.definition.id,
1069
+ value: property.value,
1070
+ })),
1071
+ }),
1052
1072
  });
1053
1073
  }
1054
1074
 
@@ -1194,6 +1214,26 @@ export async function getContentDatabaseResponse(
1194
1214
  sources: page.sources,
1195
1215
  pagination: page.pagination,
1196
1216
  tableQueryMode: page.tableQueryMode,
1217
+ mutationContract:
1218
+ page.databaseRecord.spaceId && !page.databaseRecord.systemRole
1219
+ ? await getDatabaseMutationContract(
1220
+ {
1221
+ authorityScope: page.databaseRecord.orgId
1222
+ ? {
1223
+ kind: "organization",
1224
+ id: page.databaseRecord.orgId,
1225
+ }
1226
+ : {
1227
+ kind: "personal",
1228
+ id: page.databaseRecord.ownerEmail,
1229
+ },
1230
+ spaceId: page.databaseRecord.spaceId,
1231
+ databaseId: page.databaseRecord.id,
1232
+ databaseDocumentId: page.databaseRecord.documentId,
1233
+ },
1234
+ { accessAlreadyResolved: true },
1235
+ )
1236
+ : undefined,
1197
1237
  };
1198
1238
  }
1199
1239
 
@@ -1417,15 +1457,21 @@ export async function deleteDatabaseDataForDocument(
1417
1457
  .from(schema.documentPropertyDefinitions)
1418
1458
  .where(eq(schema.documentPropertyDefinitions.databaseId, database.id));
1419
1459
 
1420
- for (const definition of definitions) {
1460
+ const definitionIds = definitions.map((definition) => definition.id);
1461
+ if (definitionIds.length > 0) {
1462
+ await deleteBlocksFieldIdentity({ db, propertyIds: definitionIds });
1421
1463
  await db
1422
1464
  .delete(schema.documentPropertyValues)
1423
- .where(eq(schema.documentPropertyValues.propertyId, definition.id));
1465
+ .where(
1466
+ inArray(schema.documentPropertyValues.propertyId, definitionIds),
1467
+ );
1424
1468
  // Independent Blocks-field content is keyed by property id; drop it so
1425
1469
  // deleting a database leaves no orphaned document_block_field_contents.
1426
1470
  await db
1427
1471
  .delete(schema.documentBlockFieldContents)
1428
- .where(eq(schema.documentBlockFieldContents.propertyId, definition.id));
1472
+ .where(
1473
+ inArray(schema.documentBlockFieldContents.propertyId, definitionIds),
1474
+ );
1429
1475
  }
1430
1476
  const sources = await db
1431
1477
  .select({ id: schema.contentDatabaseSources.id })
@@ -1463,6 +1509,11 @@ export async function deleteDatabaseDataForDocument(
1463
1509
  .where(
1464
1510
  eq(schema.contentDatabaseMigrationReceipts.databaseId, database.id),
1465
1511
  );
1512
+ await db
1513
+ .delete(schema.contentDatabaseRowMutationReceipts)
1514
+ .where(
1515
+ eq(schema.contentDatabaseRowMutationReceipts.databaseId, database.id),
1516
+ );
1466
1517
  await db
1467
1518
  .delete(schema.documentPropertyDefinitions)
1468
1519
  .where(eq(schema.documentPropertyDefinitions.databaseId, database.id));
@@ -1482,6 +1533,7 @@ export async function deleteDatabaseDataForDocument(
1482
1533
  db,
1483
1534
  );
1484
1535
  if (item) {
1536
+ await deleteBlocksFieldIdentity({ db, documentId });
1485
1537
  await db
1486
1538
  .delete(schema.contentDatabaseItemKeyClaims)
1487
1539
  .where(eq(schema.contentDatabaseItemKeyClaims.documentId, documentId));
@@ -1,10 +1,15 @@
1
- import { accessFilter, assertAccess } from "@agent-native/core/sharing";
1
+ import {
2
+ accessFilter,
3
+ assertAccess,
4
+ resolveAccess,
5
+ } from "@agent-native/core/sharing";
2
6
  import {
3
7
  and,
4
8
  asc,
5
9
  eq,
6
10
  inArray,
7
11
  isNull,
12
+ or,
8
13
  sql,
9
14
  type InferSelectModel,
10
15
  } from "drizzle-orm";
@@ -21,6 +26,7 @@ import type {
21
26
  ContentDatabaseOpenPagesIn,
22
27
  DocumentProperty,
23
28
  } from "../shared/api.js";
29
+ import { blocksFieldId } from "../shared/blocks-field-identity.js";
24
30
  import {
25
31
  DEFAULT_BLOCKS_FIELD_NAME,
26
32
  defaultPropertyOptions,
@@ -42,6 +48,7 @@ import {
42
48
  type DocumentPropertyValue,
43
49
  } from "../shared/properties.js";
44
50
  import { chunks } from "./_batch-utils.js";
51
+ import { readBlocksFieldIdentities } from "./_blocks-field-identity.js";
45
52
  import {
46
53
  propertyDefinitionsPositionScope,
47
54
  withPositionLock,
@@ -58,7 +65,8 @@ type ContentDatabaseSummaryRow = Pick<
58
65
  | "viewConfigJson"
59
66
  | "createdAt"
60
67
  | "updatedAt"
61
- >;
68
+ > &
69
+ Partial<Pick<ContentDatabaseRow, "spaceId" | "naturalKeyPropertyId">>;
62
70
  type ContentDatabaseItemRow = InferSelectModel<
63
71
  typeof schema.contentDatabaseItems
64
72
  >;
@@ -192,8 +200,10 @@ export function serializeDatabase(
192
200
  return {
193
201
  id: database.id,
194
202
  documentId: database.documentId,
203
+ spaceId: database.spaceId,
195
204
  title: database.title,
196
205
  systemRole: database.systemRole,
206
+ naturalKeyPropertyId: database.naturalKeyPropertyId,
197
207
  description,
198
208
  viewConfig: parseDatabaseViewConfig(database.viewConfigJson),
199
209
  createdAt: database.createdAt,
@@ -506,6 +516,44 @@ export async function listPropertiesForDocument(
506
516
  return listPropertiesForDatabase(database.id, document);
507
517
  }
508
518
 
519
+ export async function listPropertiesForAllDocumentDatabases(
520
+ document: DocumentRow,
521
+ ) {
522
+ const db = getDb();
523
+ const memberships = await db
524
+ .select({ databaseId: schema.contentDatabaseItems.databaseId })
525
+ .from(schema.contentDatabaseItems)
526
+ .where(eq(schema.contentDatabaseItems.documentId, document.id));
527
+ const membershipIds = memberships.map((membership) => membership.databaseId);
528
+ const databases = await db
529
+ .select({
530
+ id: schema.contentDatabases.id,
531
+ documentId: schema.contentDatabases.documentId,
532
+ })
533
+ .from(schema.contentDatabases)
534
+ .where(
535
+ and(
536
+ isNull(schema.contentDatabases.deletedAt),
537
+ membershipIds.length > 0
538
+ ? or(
539
+ eq(schema.contentDatabases.documentId, document.id),
540
+ inArray(schema.contentDatabases.id, membershipIds),
541
+ )
542
+ : eq(schema.contentDatabases.documentId, document.id),
543
+ ),
544
+ )
545
+ .orderBy(asc(schema.contentDatabases.id));
546
+
547
+ const properties = [];
548
+ for (const database of databases) {
549
+ if (!(await resolveAccess("document", database.documentId))) continue;
550
+ properties.push(
551
+ ...(await listPropertiesForDatabase(database.id, document)),
552
+ );
553
+ }
554
+ return properties;
555
+ }
556
+
509
557
  export async function listPropertiesForDatabase(
510
558
  databaseId: string,
511
559
  valueDocument?: DocumentRow,
@@ -540,10 +588,46 @@ export async function listPropertiesForDatabase(
540
588
  ? await blockFieldContentsForDocument(valueDocument.id)
541
589
  : new Map<string, string>();
542
590
 
591
+ const blocksFieldIdentityById = valueDocument
592
+ ? await readBlocksFieldIdentities({
593
+ db,
594
+ fields: definitions.flatMap((definition) => {
595
+ const type = definition.type as DocumentPropertyType;
596
+ if (!isBlocksPropertyType(type)) return [];
597
+ const options = parsePropertyOptions(definition.optionsJson);
598
+ return [
599
+ {
600
+ documentId: valueDocument.id,
601
+ propertyId: definition.id,
602
+ markdown: resolveBlocksFieldValue({
603
+ options,
604
+ documentBody: valueDocument.content,
605
+ blockFieldContent: blockContentByPropertyId.get(definition.id),
606
+ }),
607
+ },
608
+ ];
609
+ }),
610
+ })
611
+ : new Map();
612
+
543
613
  const properties = definitions.map((definition) => {
544
614
  const type = definition.type as DocumentPropertyType;
545
615
  const storedValue = valueByPropertyId.get(definition.id);
546
616
  const options = parsePropertyOptions(definition.optionsJson);
617
+ const value =
618
+ valueDocument && isComputedPropertyType(type) && type !== "formula"
619
+ ? computedPropertyValue(type, valueDocument, {
620
+ databaseRowNumber: rowNumberByDocumentId.get(valueDocument.id),
621
+ })
622
+ : valueDocument && isBlocksPropertyType(type)
623
+ ? // Each Blocks field reads from exactly one place: the primary from
624
+ // the document body, additional fields from their own store.
625
+ resolveBlocksFieldValue({
626
+ options,
627
+ documentBody: valueDocument.content,
628
+ blockFieldContent: blockContentByPropertyId.get(definition.id),
629
+ })
630
+ : parsePropertyValue(storedValue?.valueJson);
547
631
  return {
548
632
  definition: {
549
633
  id: definition.id,
@@ -560,21 +644,15 @@ export async function listPropertiesForDatabase(
560
644
  createdAt: definition.createdAt,
561
645
  updatedAt: definition.updatedAt,
562
646
  },
563
- value:
564
- valueDocument && isComputedPropertyType(type) && type !== "formula"
565
- ? computedPropertyValue(type, valueDocument, {
566
- databaseRowNumber: rowNumberByDocumentId.get(valueDocument.id),
567
- })
568
- : valueDocument && isBlocksPropertyType(type)
569
- ? // Each Blocks field reads from exactly one place: the primary from
570
- // the document body, additional fields from their own store.
571
- resolveBlocksFieldValue({
572
- options,
573
- documentBody: valueDocument.content,
574
- blockFieldContent: blockContentByPropertyId.get(definition.id),
575
- })
576
- : parsePropertyValue(storedValue?.valueJson),
647
+ value,
577
648
  editable: !definition.systemRole && !isComputedPropertyType(type),
649
+ ...(valueDocument && isBlocksPropertyType(type)
650
+ ? {
651
+ blocksField: blocksFieldIdentityById.get(
652
+ blocksFieldId(valueDocument.id, definition.id),
653
+ ),
654
+ }
655
+ : {}),
578
656
  };
579
657
  });
580
658
 
@@ -724,32 +802,64 @@ export async function listPropertiesForDatabaseDocuments(
724
802
  }
725
803
  }
726
804
 
805
+ const blocksFieldIdentityById = await readBlocksFieldIdentities({
806
+ db,
807
+ fields: valueDocuments.flatMap((document) =>
808
+ definitions.flatMap((definition) => {
809
+ const type = definition.type as DocumentPropertyType;
810
+ if (!isBlocksPropertyType(type)) return [];
811
+ const options = parsePropertyOptions(definition.optionsJson);
812
+ return [
813
+ {
814
+ documentId: document.id,
815
+ propertyId: definition.id,
816
+ markdown: resolveBlocksFieldValue({
817
+ options,
818
+ documentBody: document.content,
819
+ blockFieldContent: blockContentByDocumentAndProperty.get(
820
+ propertyValueKey(document.id, definition.id),
821
+ ),
822
+ }),
823
+ },
824
+ ];
825
+ }),
826
+ ),
827
+ });
828
+
727
829
  for (const document of valueDocuments) {
728
830
  const properties = definitions.map((definition) => {
729
831
  const propertyDefinition = serializePropertyDefinition(definition);
730
832
  const storedValue = valueByDocumentAndProperty.get(
731
833
  propertyValueKey(document.id, definition.id),
732
834
  );
835
+ const value =
836
+ isComputedPropertyType(propertyDefinition.type) &&
837
+ propertyDefinition.type !== "formula"
838
+ ? computedPropertyValue(propertyDefinition.type, document, {
839
+ databaseRowNumber: rowNumberByDocumentId.get(document.id),
840
+ })
841
+ : isBlocksPropertyType(propertyDefinition.type)
842
+ ? resolveBlocksFieldValue({
843
+ options: propertyDefinition.options,
844
+ documentBody: document.content,
845
+ blockFieldContent: blockContentByDocumentAndProperty.get(
846
+ propertyValueKey(document.id, definition.id),
847
+ ),
848
+ })
849
+ : parsePropertyValue(storedValue?.valueJson);
733
850
  return {
734
851
  definition: propertyDefinition,
735
- value:
736
- isComputedPropertyType(propertyDefinition.type) &&
737
- propertyDefinition.type !== "formula"
738
- ? computedPropertyValue(propertyDefinition.type, document, {
739
- databaseRowNumber: rowNumberByDocumentId.get(document.id),
740
- })
741
- : isBlocksPropertyType(propertyDefinition.type)
742
- ? resolveBlocksFieldValue({
743
- options: propertyDefinition.options,
744
- documentBody: document.content,
745
- blockFieldContent: blockContentByDocumentAndProperty.get(
746
- propertyValueKey(document.id, definition.id),
747
- ),
748
- })
749
- : parsePropertyValue(storedValue?.valueJson),
852
+ value,
750
853
  editable:
751
854
  !definition.systemRole &&
752
855
  !isComputedPropertyType(propertyDefinition.type),
856
+ ...(isBlocksPropertyType(propertyDefinition.type)
857
+ ? {
858
+ blocksField: blocksFieldIdentityById.get(
859
+ blocksFieldId(document.id, definition.id),
860
+ ),
861
+ }
862
+ : {}),
753
863
  };
754
864
  });
755
865
 
@@ -1,255 +1,77 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { writeAppState } from "@agent-native/core/application-state";
3
- import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
- import { assertAccess } from "@agent-native/core/sharing";
5
- import { and, eq, inArray, isNull, sql } from "drizzle-orm";
2
+ import { buildDeepLink } from "@agent-native/core/server";
6
3
  import { z } from "zod";
7
4
 
8
- import { getDb, schema } from "../server/db/index.js";
5
+ import type { ContentDatabaseRowMutationResult } from "../shared/api.js";
9
6
  import {
10
- isComputedPropertyType,
11
- type DocumentPropertyType,
12
- } from "../shared/properties.js";
13
- import {
14
- lockContentDatabaseMutation,
15
- touchContentDatabase,
16
- } from "./_content-database-mutation-lock.js";
17
- import { ensureDocumentFilesMembership } from "./_content-files.js";
7
+ createDatabaseRow,
8
+ databaseMutationEnvelopeSchema,
9
+ } from "./_database-row-mutation.js";
18
10
  import { getContentDatabaseResponse } from "./_database-utils.js";
19
- import {
20
- databaseItemsPositionScope,
21
- documentsPositionScope,
22
- withPositionLock,
23
- } from "./_position-utils.js";
24
- import { nanoid, normalizedValueJson } from "./_property-utils.js";
25
11
 
26
- export default defineAction({
27
- description: "Add a page item to a content database table.",
28
- schema: z.object({
29
- databaseId: z.string().describe("Database ID"),
30
- title: z.string().optional().describe("New row page title"),
31
- propertyValues: z
32
- .record(z.string(), z.unknown())
33
- .optional()
34
- .describe("Initial property values keyed by property definition ID"),
35
- }),
36
- run: async ({ databaseId, title, propertyValues }) => {
37
- const db = getDb();
38
- const [database] = await db
39
- .select()
40
- .from(schema.contentDatabases)
41
- .where(
42
- and(
43
- eq(schema.contentDatabases.id, databaseId),
44
- isNull(schema.contentDatabases.deletedAt),
45
- ),
46
- );
47
- if (!database) throw new Error(`Database "${databaseId}" not found`);
48
- if (database.systemRole === "workspaces") {
49
- throw new Error("Use create-content-space to add a workspace");
50
- }
12
+ const schema = databaseMutationEnvelopeSchema.extend({
13
+ title: z
14
+ .string()
15
+ .trim()
16
+ .min(1)
17
+ .max(500)
18
+ .optional()
19
+ .describe("New row page title"),
20
+ propertyValues: z
21
+ .record(z.string(), z.unknown())
22
+ .optional()
23
+ .describe("Strict property values keyed by property definition ID"),
24
+ });
51
25
 
52
- const access = await assertAccess(
53
- "document",
54
- database.documentId,
55
- "editor",
26
+ export default defineAction({
27
+ description:
28
+ "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.",
29
+ schema,
30
+ audit: {
31
+ recordInputs: false,
32
+ target: (args) => ({
33
+ type: "content-database",
34
+ id: args.target.databaseId,
35
+ visibility: "private",
36
+ }),
37
+ summary: (_args, result) => {
38
+ const receipt = (result as ContentDatabaseRowMutationResult | null)
39
+ ?.receipt;
40
+ return receipt
41
+ ? `Created Content database row ${receipt.row.itemId}`
42
+ : "Created Content database row";
43
+ },
44
+ },
45
+ run: async (args): Promise<ContentDatabaseRowMutationResult> => {
46
+ const result = await createDatabaseRow(args);
47
+ const response = await getContentDatabaseResponse(
48
+ result.receipt.target.databaseId,
49
+ {
50
+ limit: 1,
51
+ offset: 0,
52
+ documentIds: [result.receipt.row.documentId],
53
+ },
56
54
  );
57
- const databaseDocument = access.resource;
58
- if (
59
- database.spaceId &&
60
- databaseDocument.spaceId &&
61
- databaseDocument.spaceId !== database.spaceId
62
- ) {
55
+ const createdItem = response.items[0];
56
+ if (!createdItem || createdItem.id !== result.receipt.row.itemId) {
63
57
  throw new Error(
64
- `Database "${databaseId}" has inconsistent Content space`,
58
+ "Created row receipt did not resolve to its exact read-back.",
65
59
  );
66
60
  }
67
- const now = new Date().toISOString();
68
- const databaseSpaceId =
69
- database.spaceId ?? (databaseDocument.spaceId as string | null);
70
- if (!databaseSpaceId) {
71
- throw new Error("Database does not belong to a Content space.");
72
- }
73
- if (databaseSpaceId && (!database.spaceId || !databaseDocument.spaceId)) {
74
- await db.transaction(async (tx) => {
75
- if (!database.spaceId) {
76
- await tx
77
- .update(schema.contentDatabases)
78
- .set({ spaceId: databaseSpaceId, updatedAt: now })
79
- .where(eq(schema.contentDatabases.id, databaseId));
80
- }
81
- if (!databaseDocument.spaceId) {
82
- await tx
83
- .update(schema.documents)
84
- .set({ spaceId: databaseSpaceId, updatedAt: now })
85
- .where(eq(schema.documents.id, database.documentId));
86
- }
87
- await ensureDocumentFilesMembership(tx, database.documentId, now);
88
- });
89
- }
90
-
91
- const documentId = nanoid();
92
- const itemId = nanoid();
93
-
94
- const inheritedShares = await db
95
- .select({
96
- principalType: schema.documentShares.principalType,
97
- principalId: schema.documentShares.principalId,
98
- role: schema.documentShares.role,
99
- })
100
- .from(schema.documentShares)
101
- .where(eq(schema.documentShares.resourceId, database.documentId));
102
-
103
- const initialValues = Object.entries(propertyValues ?? {});
104
-
105
- await withPositionLock(
106
- documentsPositionScope(database.ownerEmail, database.documentId),
107
- () =>
108
- withPositionLock(databaseItemsPositionScope(databaseId), async () => {
109
- await db.transaction(async (tx) => {
110
- await lockContentDatabaseMutation(
111
- tx as unknown as ReturnType<typeof getDb>,
112
- databaseId,
113
- );
114
- await touchContentDatabase(
115
- tx as unknown as ReturnType<typeof getDb>,
116
- databaseId,
117
- now,
118
- );
119
- const propertyValueRows: Array<
120
- typeof schema.documentPropertyValues.$inferInsert
121
- > = [];
122
- if (initialValues.length > 0) {
123
- const requestedPropertyIds = initialValues.map(
124
- ([propertyId]) => propertyId,
125
- );
126
- const definitions = await tx
127
- .select()
128
- .from(schema.documentPropertyDefinitions)
129
- .where(
130
- and(
131
- eq(
132
- schema.documentPropertyDefinitions.ownerEmail,
133
- database.ownerEmail,
134
- ),
135
- eq(
136
- schema.documentPropertyDefinitions.databaseId,
137
- databaseId,
138
- ),
139
- inArray(
140
- schema.documentPropertyDefinitions.id,
141
- requestedPropertyIds,
142
- ),
143
- ),
144
- );
145
- const definitionById = new Map(
146
- definitions.map((definition) => [definition.id, definition]),
147
- );
148
- for (const [propertyId, value] of initialValues) {
149
- const definition = definitionById.get(propertyId);
150
- const type = definition?.type as
151
- | DocumentPropertyType
152
- | undefined;
153
- if (!definition || !type || isComputedPropertyType(type))
154
- continue;
155
- propertyValueRows.push({
156
- id: nanoid(),
157
- ownerEmail: database.ownerEmail,
158
- documentId,
159
- propertyId,
160
- valueJson: normalizedValueJson(type, value),
161
- createdAt: now,
162
- updatedAt: now,
163
- });
164
- }
165
- }
166
- const [maxDocPos] = await tx
167
- .select({ max: sql<number>`COALESCE(MAX(position), -1)` })
168
- .from(schema.documents)
169
- .where(
170
- and(
171
- eq(schema.documents.ownerEmail, database.ownerEmail),
172
- eq(schema.documents.parentId, database.documentId),
173
- ),
174
- );
175
- const [maxItemPos] = await tx
176
- .select({ max: sql<number>`COALESCE(MAX(position), -1)` })
177
- .from(schema.contentDatabaseItems)
178
- .where(eq(schema.contentDatabaseItems.databaseId, databaseId));
179
-
180
- await tx.insert(schema.documents).values({
181
- id: documentId,
182
- spaceId: databaseSpaceId,
183
- ownerEmail: database.ownerEmail,
184
- orgId: database.orgId,
185
- parentId: database.documentId,
186
- title: title?.trim() ?? "",
187
- content: "",
188
- icon: null,
189
- position: (maxDocPos?.max ?? -1) + 1,
190
- isFavorite: 0,
191
- hideFromSearch: databaseDocument.hideFromSearch ?? 0,
192
- visibility: databaseDocument.visibility ?? "private",
193
- createdAt: now,
194
- updatedAt: now,
195
- });
196
- await tx.insert(schema.contentDatabaseItems).values({
197
- id: itemId,
198
- ownerEmail: database.ownerEmail,
199
- orgId: database.orgId,
200
- databaseId,
201
- documentId,
202
- position: (maxItemPos?.max ?? -1) + 1,
203
- createdAt: now,
204
- updatedAt: now,
205
- });
206
- if (inheritedShares.length > 0) {
207
- await tx.insert(schema.documentShares).values(
208
- inheritedShares.map((share) => ({
209
- id: nanoid(),
210
- resourceId: documentId,
211
- principalType: share.principalType,
212
- principalId: share.principalId,
213
- role: share.role,
214
- createdBy: getRequestUserEmail() ?? database.ownerEmail,
215
- createdAt: now,
216
- })),
217
- );
218
- }
219
- if (propertyValueRows.length > 0) {
220
- await tx
221
- .insert(schema.documentPropertyValues)
222
- .values(propertyValueRows);
223
- }
224
- await ensureDocumentFilesMembership(tx, documentId, now);
225
- });
226
- }),
227
- );
228
-
229
- await writeAppState("refresh-signal", { ts: Date.now() }).catch(() => {
230
- // The row is already committed; polling will reconcile if a concurrent
231
- // SQLite writer briefly blocks this best-effort refresh hint.
232
- });
233
-
234
- const response = await getContentDatabaseResponse(databaseId, {
235
- limit: 100,
236
- offset: 0,
237
- });
238
- const createdItem =
239
- response.items.find((item) => item.id === itemId) ??
240
- (
241
- await getContentDatabaseResponse(databaseId, {
242
- limit: 1,
243
- offset: 0,
244
- documentIds: [documentId],
245
- })
246
- ).items.find((item) => item.id === itemId);
61
+ return { ...result, createdItem };
62
+ },
63
+ link: ({ result }) => {
64
+ const documentId = (result as ContentDatabaseRowMutationResult | null)
65
+ ?.receipt.row.documentId;
66
+ if (!documentId) return null;
247
67
  return {
248
- ...response,
249
- createdItem,
250
- createdItemId: itemId,
251
- createdDocumentId: documentId,
252
- createdDocumentUpdatedAt: now,
68
+ url: buildDeepLink({
69
+ app: "content",
70
+ view: "editor",
71
+ params: { documentId },
72
+ }),
73
+ label: "Open database row",
74
+ view: "editor",
253
75
  };
254
76
  },
255
77
  });