alinea 2.0.0-preview.13 → 2.0.0-preview.14

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 (323) hide show
  1. package/dist/.init/content/pages/welcome.json +1 -1
  2. package/dist/.mcp/later/dev.json +1 -1
  3. package/dist/.mcp/online/dev.json +1 -1
  4. package/dist/adapter/next/cms.js +6 -2
  5. package/dist/adapter/next/context.js +1 -4
  6. package/dist/adapter/next/handler.js +12 -1
  7. package/dist/backend/CommitEntries.d.ts +17 -0
  8. package/dist/backend/CommitEntries.js +39 -0
  9. package/dist/backend/Handler.d.ts +9 -0
  10. package/dist/backend/Handler.js +80 -20
  11. package/dist/backend/api/BasicAuth.js +5 -1
  12. package/dist/backend/api/CreateBackend.js +2 -2
  13. package/dist/backend/api/DatabaseApi.js +18 -5
  14. package/dist/backend/api/GithubApi.d.ts +13 -0
  15. package/dist/backend/api/GithubApi.js +170 -30
  16. package/dist/backend/api/database.js +2 -2
  17. package/dist/backend/util/Syncable.d.ts +6 -0
  18. package/dist/backend/util/Syncable.js +3 -1
  19. package/dist/bundled.d.ts +911 -151
  20. package/dist/chunks/{chunk-5H4D5JQL.js → chunk-2PP7WJUJ.js} +1 -11
  21. package/dist/chunks/{chunk-QHHMKJUO.js → chunk-7DDPSWRY.js} +465 -2
  22. package/dist/chunks/{chunk-HOETF6HN.js → chunk-PMGIIYDD.js} +1 -0
  23. package/dist/chunks/{chunk-3QFSGTGF.js → chunk-SCV3TEXM.js} +1 -1
  24. package/dist/chunks/{chunk-UPY2RUZF.js → chunk-TWBREZWC.js} +30 -0
  25. package/dist/cli/Generate.js +20 -7
  26. package/dist/cli/Mcp.js +1 -1
  27. package/dist/cli/Serve.js +1 -1
  28. package/dist/cli/bin.js +1 -1
  29. package/dist/cli/build/BuildEmitter.d.ts +5 -1
  30. package/dist/cli/build/BuildEmitter.js +8 -3
  31. package/dist/cli/generate/CompileConfig.js +10 -6
  32. package/dist/cli/generate/DevDB.d.ts +4 -1
  33. package/dist/cli/generate/DevDB.js +15 -7
  34. package/dist/cli/generate/GenerateDashboard.js +1 -0
  35. package/dist/cli/generate/MediaLocations.d.ts +18 -0
  36. package/dist/cli/generate/MediaLocations.js +69 -0
  37. package/dist/cli/serve/CreateLocalServer.js +1 -0
  38. package/dist/cli/serve/mcp/DevMcp.js +1 -1
  39. package/dist/cloud/CloudRemote.js +21 -7
  40. package/dist/cms.d.ts +4 -2
  41. package/dist/components/Alert.js +2 -2
  42. package/dist/components/AppShell.js +2 -2
  43. package/dist/components/AsyncList.d.ts +52 -0
  44. package/dist/components/AsyncList.js +69 -0
  45. package/dist/components/Breadcrumbs.js +6 -6
  46. package/dist/components/Button.js +4 -4
  47. package/dist/components/Calendar.js +18 -18
  48. package/dist/components/Checkbox.js +10 -10
  49. package/dist/components/CheckboxGroup.js +2 -2
  50. package/dist/components/Collapsible.js +5 -5
  51. package/dist/components/ColorSwatch.js +2 -2
  52. package/dist/components/ColorSwatchPicker.js +4 -4
  53. package/dist/components/ComboBox.d.ts +27 -9
  54. package/dist/components/ComboBox.js +40 -9
  55. package/dist/components/Command.js +13 -13
  56. package/dist/components/ContentCard.js +28 -28
  57. package/dist/components/ContentGrid.js +11 -11
  58. package/dist/components/DataList.js +1 -1
  59. package/dist/components/DateField.d.ts +4 -0
  60. package/dist/components/DateField.js +2 -2
  61. package/dist/components/DatePicker.js +2 -2
  62. package/dist/components/DateRangePicker.js +2 -2
  63. package/dist/components/Dialog.d.ts +14 -3
  64. package/dist/components/Dialog.js +38 -11
  65. package/dist/components/DropZone.js +6 -6
  66. package/dist/components/DropdownMenu.js +8 -8
  67. package/dist/components/Empty.js +4 -4
  68. package/dist/components/Field.d.ts +1 -1
  69. package/dist/components/Field.js +18 -12
  70. package/dist/components/Form.d.ts +23 -0
  71. package/dist/components/Form.js +64 -0
  72. package/dist/components/Link.d.ts +8 -1
  73. package/dist/components/List.js +27 -27
  74. package/dist/components/MediaPreview.js +5 -5
  75. package/dist/components/MultipleSelect.d.ts +13 -8
  76. package/dist/components/MultipleSelect.js +25 -25
  77. package/dist/components/NavRail.js +7 -7
  78. package/dist/components/NumberField.d.ts +5 -1
  79. package/dist/components/NumberField.js +6 -4
  80. package/dist/components/Page.js +6 -6
  81. package/dist/components/Popover.js +2 -2
  82. package/dist/components/PreviewFrame.js +8 -8
  83. package/dist/components/RadioGroup.js +10 -10
  84. package/dist/components/Resizable.js +1 -1
  85. package/dist/components/SearchField.js +8 -8
  86. package/dist/components/Select.d.ts +13 -10
  87. package/dist/components/Select.js +18 -32
  88. package/dist/components/Sheet.js +6 -6
  89. package/dist/components/Sidebar.js +5 -5
  90. package/dist/components/SortableList.js +12 -12
  91. package/dist/components/Spinner.js +4 -4
  92. package/dist/components/Stack.d.ts +20 -0
  93. package/dist/components/Stack.js +45 -0
  94. package/dist/components/Surface.js +1 -1
  95. package/dist/components/Switch.js +5 -5
  96. package/dist/components/Table.d.ts +11 -1
  97. package/dist/components/Table.js +81 -53
  98. package/dist/components/Tabs.js +6 -6
  99. package/dist/components/TagGroup.js +10 -10
  100. package/dist/components/TextField.d.ts +7 -1
  101. package/dist/components/TextField.js +20 -11
  102. package/dist/components/TimeField.d.ts +6 -1
  103. package/dist/components/TimeField.js +51 -26
  104. package/dist/components/Timestamp.js +1 -1
  105. package/dist/components/ToggleGroup.js +2 -2
  106. package/dist/components/Toolbar.js +3 -3
  107. package/dist/components/Tooltip.js +5 -4
  108. package/dist/components/Tree.js +27 -27
  109. package/dist/components/internal/DateControl.js +11 -11
  110. package/dist/components/internal/FormScope.d.ts +2 -0
  111. package/dist/components/internal/FormScope.js +8 -0
  112. package/dist/components/internal/ListBoxGroup.d.ts +12 -0
  113. package/dist/components/internal/ListBoxGroup.js +46 -0
  114. package/dist/components/internal/ListBoxOption.d.ts +2 -2
  115. package/dist/components/internal/ListBoxOption.js +5 -5
  116. package/dist/components/internal/Locale.js +2 -2
  117. package/dist/components/internal/PopoverSurface.js +5 -4
  118. package/dist/components/internal/SelectionCheckbox.js +5 -5
  119. package/dist/components/internal/Slot.js +1 -1
  120. package/dist/components/internal/ToggleButton.js +4 -4
  121. package/dist/components/internal/Trigger.js +2 -2
  122. package/dist/components/internal/useDragDrop.js +2 -2
  123. package/dist/components/types.d.ts +4 -0
  124. package/dist/components.d.ts +3 -0
  125. package/dist/components.js +3 -0
  126. package/dist/core/Client.js +12 -1
  127. package/dist/core/Config.d.ts +6 -0
  128. package/dist/core/Config.js +12 -0
  129. package/dist/core/DashboardEntry.d.ts +19 -0
  130. package/dist/core/DashboardEntry.js +0 -0
  131. package/dist/core/Document.d.ts +2 -2
  132. package/dist/core/Document.js +10 -3
  133. package/dist/core/EntryFields.d.ts +8 -2
  134. package/dist/core/Field.d.ts +5 -0
  135. package/dist/core/Filter.d.ts +19 -2
  136. package/dist/core/Filter.js +5 -0
  137. package/dist/core/Graph.d.ts +10 -2
  138. package/dist/core/Overview.d.ts +74 -5
  139. package/dist/core/Overview.js +1 -0
  140. package/dist/core/Root.js +1 -1
  141. package/dist/core/Scope.d.ts +5 -0
  142. package/dist/core/Scope.js +22 -5
  143. package/dist/core/Type.d.ts +25 -6
  144. package/dist/core/Type.js +25 -7
  145. package/dist/core/db/LocalStore.d.ts +7 -0
  146. package/dist/core/field/CreateField.d.ts +1 -2
  147. package/dist/core/source/Source.d.ts +2 -0
  148. package/dist/core/source/Source.js +11 -0
  149. package/dist/core/util/Async.d.ts +2 -0
  150. package/dist/core/util/Async.js +12 -1
  151. package/dist/core/util/Checks.js +1 -1
  152. package/dist/core/util/Slugs.js +4 -3
  153. package/dist/dashboard/App.css +13 -11
  154. package/dist/dashboard/App.d.ts +1 -0
  155. package/dist/dashboard/App.js +22 -6
  156. package/dist/dashboard/Href.d.ts +37 -0
  157. package/dist/dashboard/Href.js +46 -0
  158. package/dist/dashboard/app/ActivityStatus.js +34 -12
  159. package/dist/dashboard/app/AuthView.js +2 -2
  160. package/dist/dashboard/app/BlockSettings.d.ts +9 -0
  161. package/dist/dashboard/app/BlockSettings.js +16 -0
  162. package/dist/dashboard/app/BlockSheet.js +3 -3
  163. package/dist/dashboard/app/BlockSheetStoryFrame.js +2 -2
  164. package/dist/dashboard/app/CompactField.js +15 -15
  165. package/dist/dashboard/app/CroppedImage.js +2 -2
  166. package/dist/dashboard/app/DashboardLayout.d.ts +1 -1
  167. package/dist/dashboard/app/DashboardLayout.js +12 -0
  168. package/dist/dashboard/app/EntryActions.d.ts +11 -0
  169. package/dist/dashboard/app/EntryActions.js +30 -0
  170. package/dist/dashboard/app/EntryFields.d.ts +15 -2
  171. package/dist/dashboard/app/EntryFields.js +26 -5
  172. package/dist/dashboard/app/EntryHeader.js +17 -7
  173. package/dist/dashboard/app/EntryReferences.js +2 -2
  174. package/dist/dashboard/app/EntrySidebar.js +25 -23
  175. package/dist/dashboard/app/EntrySidebarPreview.js +2 -2
  176. package/dist/dashboard/app/EntryValidationModal.js +3 -3
  177. package/dist/dashboard/app/Explorer.js +84 -45
  178. package/dist/dashboard/app/ExplorerBatchActions.js +4 -4
  179. package/dist/dashboard/app/ExplorerControls.js +5 -5
  180. package/dist/dashboard/app/ExplorerList.js +2 -2
  181. package/dist/dashboard/app/ExplorerModal.js +3 -3
  182. package/dist/dashboard/app/ExplorerOverviewHeader.d.ts +6 -0
  183. package/dist/dashboard/app/ExplorerOverviewHeader.js +28 -0
  184. package/dist/dashboard/app/ExplorerTable.js +7 -7
  185. package/dist/dashboard/app/ExplorerTabs.d.ts +12 -0
  186. package/dist/dashboard/app/ExplorerTabs.js +42 -0
  187. package/dist/dashboard/app/ImageEditor.js +31 -31
  188. package/dist/dashboard/app/LinkPicker.js +3 -3
  189. package/dist/dashboard/app/LocaleMenu.js +9 -9
  190. package/dist/dashboard/app/MediaPreviewCell.js +5 -5
  191. package/dist/dashboard/app/OverviewCell.js +12 -10
  192. package/dist/dashboard/app/RemoveDialog.js +5 -0
  193. package/dist/dashboard/app/SearchBar.js +4 -4
  194. package/dist/dashboard/app/SettingsTab.d.ts +6 -0
  195. package/dist/dashboard/app/SettingsTab.js +27 -0
  196. package/dist/dashboard/app/SidebarTree.js +19 -19
  197. package/dist/dashboard/app/WorkspaceMenu.js +6 -6
  198. package/dist/dashboard/app/WorkspaceRoots.js +102 -98
  199. package/dist/dashboard/app/editor/FileEditor.js +18 -18
  200. package/dist/dashboard/app/pages/EntryPage.js +42 -39
  201. package/dist/dashboard/app/pages/RootPage.js +3 -3
  202. package/dist/dashboard/app/pages/SplashPage.js +94 -79
  203. package/dist/dashboard/app/pages/UsersPage.js +9 -9
  204. package/dist/dashboard/app/ui/DashboardModal.js +14 -14
  205. package/dist/dashboard/atoms/activity.d.ts +1 -0
  206. package/dist/dashboard/atoms/activity.js +8 -5
  207. package/dist/dashboard/atoms/config.d.ts +2 -0
  208. package/dist/dashboard/atoms/dashboard.d.ts +11 -0
  209. package/dist/dashboard/atoms/dashboard.js +8 -0
  210. package/dist/dashboard/atoms/editor.d.ts +7 -1
  211. package/dist/dashboard/atoms/editor.js +14 -2
  212. package/dist/dashboard/atoms/explorer.d.ts +15 -0
  213. package/dist/dashboard/atoms/explorer.js +43 -14
  214. package/dist/dashboard/atoms/graph.d.ts +5 -0
  215. package/dist/dashboard/atoms/graph.js +26 -1
  216. package/dist/dashboard/atoms/nav.d.ts +16 -10
  217. package/dist/dashboard/atoms/nav.js +96 -36
  218. package/dist/dashboard/atoms/overview.d.ts +26 -4
  219. package/dist/dashboard/atoms/overview.js +41 -12
  220. package/dist/dashboard/atoms/root.js +7 -1
  221. package/dist/dashboard/boot/ActivityEvent.d.ts +1 -1
  222. package/dist/dashboard/boot/Boot.js +6 -0
  223. package/dist/dashboard/boot/DashboardWorker.d.ts +6 -0
  224. package/dist/dashboard/boot/DashboardWorker.js +89 -8
  225. package/dist/dashboard/boot/LoadWorker.js +3 -0
  226. package/dist/dashboard/boot/SyncEvent.d.ts +7 -0
  227. package/dist/dashboard/boot/SyncEvent.js +14 -0
  228. package/dist/dashboard/boot/WorkerDB.d.ts +1 -0
  229. package/dist/dashboard/boot/WorkerDB.js +3 -0
  230. package/dist/dashboard/cms/EntryTable.d.ts +4 -3
  231. package/dist/dashboard/cms/EntryTable.js +2 -2
  232. package/dist/dashboard/cms/ViewProps.d.ts +3 -2
  233. package/dist/dashboard/cms/hooks.d.ts +34 -18
  234. package/dist/dashboard/cms/hooks.js +43 -18
  235. package/dist/dashboard/contexts.d.ts +3 -2
  236. package/dist/dashboard/contexts.js +3 -2
  237. package/dist/dashboard/hook/UseEntryEditor.d.ts +1 -1
  238. package/dist/dashboard/hook/UseEntryEditor.js +7 -3
  239. package/dist/dashboard/icons.d.ts +1 -0
  240. package/dist/dashboard/icons.js +15 -0
  241. package/dist/database/BrowserEntryStore.js +35 -13
  242. package/dist/database/DatabaseTables.js +1 -1
  243. package/dist/database/EntryDatabase.d.ts +10 -1
  244. package/dist/database/EntryDatabase.js +31 -10
  245. package/dist/database/EntryStore.js +1 -11
  246. package/dist/database/entry/EntryTable.d.ts +5 -3
  247. package/dist/database/entry/EntryTable.js +25 -8
  248. package/dist/database/query/Condition.d.ts +4 -1
  249. package/dist/database/query/Condition.js +16 -5
  250. package/dist/database/query/EntryQuery.js +59 -7
  251. package/dist/database/query/Relation.d.ts +13 -1
  252. package/dist/database/query/Relation.js +19 -1
  253. package/dist/database/query/Search.d.ts +7 -1
  254. package/dist/database/query/Search.js +54 -9
  255. package/dist/database/sync/EntrySyncer.d.ts +2 -0
  256. package/dist/database/sync/EntrySyncer.js +4 -2
  257. package/dist/database/sync/Ingest.d.ts +2 -1
  258. package/dist/database/sync/Ingest.js +20 -6
  259. package/dist/field/code/CodeField.view.js +12 -12
  260. package/dist/field/link/EntryLink.d.ts +4 -5
  261. package/dist/field/link/FileLink.d.ts +2 -2
  262. package/dist/field/link/ImageLink.d.ts +2 -5
  263. package/dist/field/link/ImageLink.js +0 -1
  264. package/dist/field/link/LinkField.view.js +18 -15
  265. package/dist/field/list/ListField.view.js +50 -35
  266. package/dist/field/metadata/MetadataField.view.js +14 -14
  267. package/dist/field/richtext/RichTextBlock.js +39 -35
  268. package/dist/field/richtext/RichTextExtensions.js +23 -23
  269. package/dist/field/richtext/RichTextField.view.js +6 -6
  270. package/dist/field/richtext/RichTextInsertMenu.js +5 -5
  271. package/dist/field/richtext/RichTextToolbar.js +26 -1
  272. package/dist/field/richtext/Toolbar.d.ts +9 -1
  273. package/dist/field/richtext/Toolbar.js +12 -2
  274. package/dist/field/select/SelectField.view.js +1 -0
  275. package/dist/index.css +174 -23
  276. package/dist/index.d.ts +1 -0
  277. package/dist/index.js +3 -1
  278. package/dist/next.d.ts +2 -0
  279. package/dist/next.edge.d.ts +2 -0
  280. package/dist/picker/entry/EntryPicker.d.ts +3 -1
  281. package/dist/picker/entry/EntryPicker.js +15 -1
  282. package/dist/preview/react.js +3 -0
  283. package/dist/query.d.ts +26 -14
  284. package/dist/query.js +4 -0
  285. package/dist/types.d.ts +8 -5
  286. package/dist/ui/Stack.d.ts +4 -2
  287. package/docs/ai-setup.md +3 -3
  288. package/docs/cli.md +4 -2
  289. package/docs/components/combo-box.md +105 -16
  290. package/docs/components/date-field.md +1 -1
  291. package/docs/components/dialog.md +51 -0
  292. package/docs/components/form.md +67 -0
  293. package/docs/components/link.md +10 -0
  294. package/docs/components/multiple-select.md +9 -17
  295. package/docs/components/number-field.md +5 -1
  296. package/docs/components/select.md +9 -20
  297. package/docs/components/stack.md +42 -0
  298. package/docs/components/table.md +83 -0
  299. package/docs/components/text-field.md +9 -1
  300. package/docs/components/time-field.md +5 -1
  301. package/docs/components.md +39 -1
  302. package/docs/configuration.md +3 -1
  303. package/docs/custom-fields.md +135 -7
  304. package/docs/deploy/self-host.md +2 -0
  305. package/docs/deploy.md +7 -4
  306. package/docs/fields/image.md +0 -1
  307. package/docs/fields/list.md +19 -0
  308. package/docs/fields/object.md +5 -3
  309. package/docs/fields/rich-text.md +31 -0
  310. package/docs/index.md +8 -6
  311. package/docs/instant-publishing.md +23 -2
  312. package/docs/overviews.md +128 -4
  313. package/docs/query/filtering.md +2 -0
  314. package/docs/query/related.md +58 -9
  315. package/docs/query.md +52 -1
  316. package/docs/quickstart.md +5 -5
  317. package/docs/schema/document.md +2 -0
  318. package/docs/schema/type.md +3 -1
  319. package/docs/upgrading.md +100 -19
  320. package/llms-full.txt +1022 -125
  321. package/package.json +1 -1
  322. package/readme.md +2 -2
  323. package/dist/chunks/{chunk-4ZA6KZHS.js → chunk-6FQ5IMNA.js} +3 -3
@@ -20,13 +20,14 @@ function useEditor() {
20
20
  }
21
21
  function useFieldInfo(field) {
22
22
  const editor = useEditor();
23
+ if (typeof field === "string")
24
+ throw new Error(`Pass the field instead of its key: ${field}`);
23
25
  const info = editor.get(field);
24
26
  assert(info, "Field info not found in editor");
25
27
  return info;
26
28
  }
27
29
  function useFieldNode(field) {
28
- const { key } = useFieldInfo(field);
29
- const editor = useEditor();
30
+ const { key, editor } = useFieldInfo(field);
30
31
  const nodes = useAtomValueRaw(editor.node.nodes);
31
32
  assert(nodes[key], `Node not found for field key: ${key}`);
32
33
  return nodes[key];
@@ -1,13 +1,13 @@
1
1
  import type { EntryStatus } from 'alinea/core/Entry';
2
2
  export interface EntryEditorVersionData {
3
3
  path: string;
4
- status: EntryStatus;
5
4
  }
6
5
  export interface EntryEditorVersion {
7
6
  data: EntryEditorVersionData;
8
7
  id: string;
9
8
  locale: string | null;
10
9
  path: string;
10
+ status: EntryStatus;
11
11
  title: string;
12
12
  type: string;
13
13
  }
@@ -6,13 +6,15 @@ function useEntryEditor() {
6
6
  const entry = useEntry();
7
7
  if (!entry) return void 0;
8
8
  const version = strictObject("entry editor activeVersion", {
9
+ // In 1.x data held the field values: only path is kept here, reading
10
+ // another field (a custom `status` too) throws instead of guessing
9
11
  data: strictObject("entry editor activeVersion.data", {
10
- path: entry.path,
11
- status: entry.status
12
+ path: entry.path
12
13
  }),
13
14
  id: entry.id,
14
15
  locale: entry.locale,
15
16
  path: entry.path,
17
+ status: entry.status,
16
18
  title: entry.title,
17
19
  type: entry.type
18
20
  });
@@ -32,7 +34,9 @@ function strictObject(label, value) {
32
34
  if (typeof property === "symbol")
33
35
  return Reflect.get(target, property, receiver);
34
36
  if (property in target) return Reflect.get(target, property, receiver);
35
- throw new Error(`${label}.${property} is not supported in dashboard v2`);
37
+ throw new Error(
38
+ `${label}.${property} is not supported in dashboard v2, read the entry with useEntry() from alinea/cms`
39
+ );
36
40
  }
37
41
  });
38
42
  }
@@ -181,3 +181,4 @@ export declare function IcRoundAudioFile(props: SVGProps<SVGSVGElement>): import
181
181
  export declare function IcRoundRotateRight(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
182
182
  export declare function IcRoundCrop(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
183
183
  export declare function IcRoundDriveFileMove(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
184
+ export declare function IcRoundDownload(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
@@ -2640,6 +2640,20 @@ function IcRoundDriveFileMove(props) {
2640
2640
  }
2641
2641
  ) });
2642
2642
  }
2643
+ function IcRoundDownload(props) {
2644
+ return /* @__PURE__ */ jsx(
2645
+ "svg",
2646
+ {
2647
+ xmlns: "http://www.w3.org/2000/svg",
2648
+ width: "1em",
2649
+ height: "1em",
2650
+ viewBox: "0 0 24 24",
2651
+ fill: "currentColor",
2652
+ ...props,
2653
+ children: /* @__PURE__ */ jsx("path", { d: "M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71M5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1" })
2654
+ }
2655
+ );
2656
+ }
2643
2657
  export {
2644
2658
  IcAlignCenter,
2645
2659
  IcAlignJustify,
@@ -2704,6 +2718,7 @@ export {
2704
2718
  IcRoundDelete,
2705
2719
  IcRoundDescription,
2706
2720
  IcRoundDesktopWindows,
2721
+ IcRoundDownload,
2707
2722
  IcRoundDragHandle,
2708
2723
  IcRoundDragIndicator,
2709
2724
  IcRoundDriveFileMove,
@@ -16,19 +16,32 @@ import { DatabaseSource } from "./DatabaseSource.js";
16
16
  import { wasmHandle, wasmSqlite } from "./driver/WasmDatabase.js";
17
17
  var cacheSize = -65536;
18
18
  var keepBranches = 2;
19
+ var rewriteLimit = 1;
19
20
  var BrowserEntryStore = class _BrowserEntryStore extends EntryStore {
20
21
  #session;
21
22
  #storage;
22
23
  #stored;
23
24
  #written = Promise.resolve();
24
25
  #cleanup = Promise.resolve();
26
+ /**
27
+ * The entries were derived again from a base of another config. Refetching
28
+ * everything after each deploy that changes the config costs more than
29
+ * the layout that deriving leaves, so its first save is not counted.
30
+ */
31
+ #derived = false;
25
32
  static async open(config, options) {
26
33
  const { name, fingerprint } = options;
27
34
  const storage = options.storage ?? indexedDBSnapshots(`${versionedCacheName(name)}-snapshots`);
28
35
  const cleanup = !options.storage && globalThis.indexedDB ? cleanupOldCaches(indexedDB, name).catch(() => {
29
36
  }) : Promise.resolve();
30
37
  const Database = await wasmSqlite();
31
- const session = options.reset ? storage.session(new Database(), { branch: fingerprint }) : await openBase(storage, Database, fingerprint);
38
+ const empty = () => storage.session(new Database(), { branch: fingerprint });
39
+ let session = options.reset ? empty() : await openBase(storage, Database, fingerprint);
40
+ const base = session.snapshot;
41
+ if (base && rewritten(base) > base.size * rewriteLimit) {
42
+ await session.close();
43
+ session = empty();
44
+ }
32
45
  const store = await _BrowserEntryStore.#on(
33
46
  config,
34
47
  session,
@@ -36,12 +49,7 @@ var BrowserEntryStore = class _BrowserEntryStore extends EntryStore {
36
49
  fingerprint
37
50
  ).catch((error) => {
38
51
  if (!session.snapshot) throw error;
39
- return _BrowserEntryStore.#on(
40
- config,
41
- storage.session(new Database(), { branch: fingerprint }),
42
- storage,
43
- fingerprint
44
- );
52
+ return _BrowserEntryStore.#on(config, empty(), storage, fingerprint);
45
53
  });
46
54
  store.#cleanup = cleanup;
47
55
  return store;
@@ -60,9 +68,11 @@ var BrowserEntryStore = class _BrowserEntryStore extends EntryStore {
60
68
  await db.run(sql`pragma page_size = 16384`);
61
69
  await EntryDatabase.createSchema(db, config, ReadonlyTree.EMPTY.sha);
62
70
  const database = new EntryDatabase(config, db, { fork: preview });
63
- if (branch !== void 0 && branch !== fingerprint)
64
- await withoutJournal(sqlite, () => database.reindex(config));
65
- return new _BrowserEntryStore(config, database, session, storage);
71
+ const derived = branch !== void 0 && branch !== fingerprint;
72
+ if (derived) await withoutJournal(sqlite, () => database.reindex(config));
73
+ const store = new _BrowserEntryStore(config, database, session, storage);
74
+ store.#derived = derived;
75
+ return store;
66
76
  } catch (error) {
67
77
  await db.close();
68
78
  throw error;
@@ -89,9 +99,17 @@ var BrowserEntryStore = class _BrowserEntryStore extends EntryStore {
89
99
  const session = this.#session;
90
100
  const storage = this.#storage;
91
101
  const key = `${revision}-${session.branch}`;
92
- const save = (key2) => this.database.whileIdle(async () => ({
93
- saved: session.held ? session.save({ key: key2, meta: { tree: revision } }) : void 0
94
- })).then(({ saved }) => saved);
102
+ const save = (key2) => this.database.whileIdle(async () => {
103
+ if (!session.held) return void 0;
104
+ const base = session.snapshot;
105
+ const added = this.#derived ? 0 : session.held;
106
+ this.#derived = false;
107
+ const meta = {
108
+ tree: revision,
109
+ rewritten: base ? rewritten(base) + added : 0
110
+ };
111
+ return { saved: session.save({ key: key2, meta }) };
112
+ }).then((result) => result?.saved);
95
113
  const written = save(key).then(
96
114
  (result) => result?.status === "mismatch" ? save(`${key}-${crypto.randomUUID()}`) : result
97
115
  ).then(() => storage.retain({ branches: keepBranches })).catch(() => {
@@ -106,6 +124,10 @@ var BrowserEntryStore = class _BrowserEntryStore extends EntryStore {
106
124
  await super.close();
107
125
  }
108
126
  };
127
+ function rewritten(base) {
128
+ const value = base.meta.rewritten;
129
+ return typeof value === "number" ? value : 0;
130
+ }
109
131
  var corruptCodes = /* @__PURE__ */ new Set(["SQLITE_CORRUPT", "SQLITE_NOTADB"]);
110
132
  var corruptMessage = /database disk image is malformed|file is not a database/;
111
133
  function isCorruptDatabaseError(error) {
@@ -4,7 +4,7 @@ import {
4
4
  number,
5
5
  text,
6
6
  varchar
7
- } from "../chunks/chunk-5H4D5JQL.js";
7
+ } from "../chunks/chunk-2PP7WJUJ.js";
8
8
  import {
9
9
  table
10
10
  } from "../chunks/chunk-PYDOMKJZ.js";
@@ -93,5 +93,14 @@ export declare class EntryDatabase extends Graph implements AsyncDisposable {
93
93
  getSourceFileStats(): Promise<Map<string, FileStat>>;
94
94
  /** Replace the recorded file stats after syncing from a filesystem source. */
95
95
  setSourceFileStats(stats: ReadonlyMap<string, FileStat>): Promise<void>;
96
- compact(): Promise<void>;
96
+ /**
97
+ * Optimize the database and rewrite it when it has pages to reclaim, or
98
+ * always with `rewrite`. A file that synced many changes keeps the pages of
99
+ * its tables scattered over it, without any to reclaim: readers that read
100
+ * ahead, such as the dashboard reading a stored copy, then read it page by
101
+ * page. Rewriting lays every table out in order.
102
+ */
103
+ compact(options?: {
104
+ rewrite?: boolean;
105
+ }): Promise<void>;
97
106
  }
@@ -67,6 +67,11 @@ var EntryDatabase = class _EntryDatabase extends Graph {
67
67
  #tree;
68
68
  #initialTree;
69
69
  #recordsTree;
70
+ /**
71
+ * Overlays answer one preview payload and are thrown away; they keep the
72
+ * statistics of the database they fork.
73
+ */
74
+ #analyzes = true;
70
75
  /** The connection is already inside a transaction: never open nested ones. */
71
76
  #transactional = false;
72
77
  #changeListeners = /* @__PURE__ */ new Set();
@@ -124,6 +129,7 @@ var EntryDatabase = class _EntryDatabase extends Graph {
124
129
  recordsTree: false
125
130
  });
126
131
  overlay.#tree = tree;
132
+ overlay.#analyzes = false;
127
133
  try {
128
134
  await overlay.syncWith(source);
129
135
  return overlay;
@@ -172,7 +178,7 @@ var EntryDatabase = class _EntryDatabase extends Graph {
172
178
  tree: revision === ReadonlyTree.EMPTY.sha ? ReadonlyTree.EMPTY : null
173
179
  });
174
180
  await syncFieldIndexes(db, config);
175
- await analyze(db);
181
+ if (!await hasStatistics(db)) await analyze(db);
176
182
  }
177
183
  /**
178
184
  * Synchronize a source through this database's single prepared syncer,
@@ -193,7 +199,8 @@ var EntryDatabase = class _EntryDatabase extends Graph {
193
199
  previousTree: this.#tree?.sha === current ? this.#tree : void 0,
194
200
  withinTransaction: this.#transactional,
195
201
  validate: options?.validate ?? true,
196
- recordsTree: this.#recordsTree
202
+ recordsTree: this.#recordsTree,
203
+ onProgress: options?.onProgress
197
204
  });
198
205
  this.#tree = tree;
199
206
  if (changed.length > 0) await this.#optimize();
@@ -392,7 +399,7 @@ var EntryDatabase = class _EntryDatabase extends Graph {
392
399
  return this.close();
393
400
  }
394
401
  async #optimize() {
395
- if (!this.#transactional) await analyze(this.#db);
402
+ if (this.#analyzes && !this.#transactional) await analyze(this.#db);
396
403
  }
397
404
  getRevision() {
398
405
  return this.#queue.run(() => this.#getRevision(this.#db));
@@ -525,28 +532,42 @@ var EntryDatabase = class _EntryDatabase extends Graph {
525
532
  )
526
533
  );
527
534
  }
528
- async compact() {
535
+ /**
536
+ * Optimize the database and rewrite it when it has pages to reclaim, or
537
+ * always with `rewrite`. A file that synced many changes keeps the pages of
538
+ * its tables scattered over it, without any to reclaim: readers that read
539
+ * ahead, such as the dashboard reading a stored copy, then read it page by
540
+ * page. Rewriting lays every table out in order.
541
+ */
542
+ async compact(options = {}) {
529
543
  await this.#queue.run(async () => {
530
544
  await this.#db.run(sql`pragma optimize`);
531
545
  const free = await this.#db.get(
532
546
  sql`pragma freelist_count`
533
547
  );
534
- if (free?.freelist_count) await this.#db.run(sql`vacuum`);
548
+ if (options.rewrite || free?.freelist_count)
549
+ await this.#db.run(sql`vacuum`);
535
550
  });
536
551
  }
537
552
  };
538
553
  async function analyze(db) {
539
554
  try {
540
555
  await db.run(sql`pragma analysis_limit = 400`);
541
- const entries = getTable(EntryIndexTable).name;
542
- const stats = await db.get(
543
- sql`select 1 from sqlite_master where name = 'sqlite_stat1'`
556
+ await db.run(
557
+ await hasStatistics(db) ? sql`pragma optimize = 0x10002` : sql`analyze`
544
558
  );
545
- const analyzed = stats && await db.get(sql`select 1 from sqlite_stat1 where tbl = ${entries}`);
546
- await db.run(analyzed ? sql`pragma optimize = 0x10002` : sql`analyze`);
547
559
  } catch {
548
560
  }
549
561
  }
562
+ async function hasStatistics(db) {
563
+ const entries = getTable(EntryIndexTable).name;
564
+ const stats = await db.get(
565
+ sql`select 1 from sqlite_master where name = 'sqlite_stat1'`
566
+ );
567
+ return Boolean(
568
+ stats && await db.get(sql`select 1 from sqlite_stat1 where tbl = ${entries}`)
569
+ );
570
+ }
550
571
  export {
551
572
  EntryDatabase
552
573
  };
@@ -1,7 +1,7 @@
1
1
  import "../chunks/chunk-CQ52GEHY.js";
2
2
 
3
3
  // src/database/EntryStore.ts
4
- import { applyChangesFrom } from "alinea/core/source/Source";
4
+ import { applyChangesFrom, sourceAtTree } from "alinea/core/source/Source";
5
5
  import { OverlaySource } from "alinea/core/source/OverlaySource";
6
6
  import { ReadonlyTree } from "alinea/core/source/Tree";
7
7
  import { createRecord } from "alinea/core/EntryRecord";
@@ -215,16 +215,6 @@ async function changesSince(source, from) {
215
215
  await changes.applyChangesFrom(source, from.diff(tree), tree);
216
216
  return changes;
217
217
  }
218
- function sourceAtTree(source, tree) {
219
- return {
220
- async getTreeIfDifferent(revision) {
221
- return revision === tree.sha ? void 0 : tree;
222
- },
223
- getBlobs(shas, options) {
224
- return source.getBlobs(shas, options);
225
- }
226
- };
227
- }
228
218
  export {
229
219
  EntryStore
230
220
  };
@@ -139,9 +139,11 @@ export declare function isOrderedField(field: unknown): boolean;
139
139
  /** Metadata fields whose queries order by their stored value. */
140
140
  export declare const orderedMetadata: Set<string>;
141
141
  /**
142
- * Index the stored value of every ordered field in the config, per type, and
143
- * of the ordered metadata. Field names are identifiers, so the dotted metadata
144
- * names cannot collide with them.
142
+ * Index the stored value of every ordered field in the config, and of the
143
+ * ordered metadata. A field is indexed per type that has it, over the rows of
144
+ * that type only: queries of one type find the index, and the rows of other
145
+ * types don't take up space in it. The metadata is indexed over every row.
146
+ * Index names are `type:field` and `metadata.name`, which can't collide.
145
147
  */
146
148
  export declare function syncFieldIndexes(db: Database, config: Config): Promise<void>;
147
149
  /** An Entry plus the physical-version and local-index fields. */
@@ -4,7 +4,7 @@ import {
4
4
  json,
5
5
  text,
6
6
  varchar
7
- } from "../../chunks/chunk-5H4D5JQL.js";
7
+ } from "../../chunks/chunk-2PP7WJUJ.js";
8
8
  import {
9
9
  primaryKey,
10
10
  table
@@ -118,11 +118,13 @@ function isOrderedField(field) {
118
118
  var fieldIndexPrefix = "alinea_entry_index_by_field_";
119
119
  var orderedMetadata = /* @__PURE__ */ new Set(["createdAt", "updatedAt"]);
120
120
  async function syncFieldIndexes(db, config) {
121
- const wanted = /* @__PURE__ */ new Set();
122
- for (const name of orderedMetadata) wanted.add(`metadata.${name}`);
123
- for (const type of Object.values(config.schema))
121
+ const wanted = /* @__PURE__ */ new Map();
122
+ for (const name of orderedMetadata)
123
+ wanted.set(`metadata.${name}`, { path: ["metadata", name] });
124
+ for (const [typeName, type] of Object.entries(config.schema))
124
125
  for (const [name, field] of Object.entries(Type.fields(type)))
125
- if (isOrderedField(field)) wanted.add(name);
126
+ if (isOrderedField(field))
127
+ wanted.set(`${typeName}:${name}`, { type: typeName, path: [name] });
126
128
  const rows = await db.all(
127
129
  sql`select name from sqlite_master where type = 'index'`
128
130
  );
@@ -133,13 +135,28 @@ async function syncFieldIndexes(db, config) {
133
135
  for (const name of existing)
134
136
  if (!wanted.has(name))
135
137
  await db.run(sql`drop index ${sql.identifier(fieldIndexPrefix + name)}`);
136
- for (const name of wanted) {
138
+ for (const [name, { type, path }] of wanted) {
137
139
  if (existing.has(name)) continue;
138
- const value = jsonField(sql.identifier("data"), name.split("."));
140
+ const value = jsonField(sql.identifier("data"), path);
141
+ const where = type ? sql`where ${sql.identifier("type")} = ${sql.inline(type)}` : sql``;
139
142
  await db.run(sql`create index ${sql.identifier(fieldIndexPrefix + name)}
140
143
  on ${EntryIndexTable}(${sql.identifier("type")}, ${value},
141
- ${sql.identifier("index")}, ${sql.identifier("filePath")})`);
144
+ ${sql.identifier("index")}, ${sql.identifier("filePath")}) ${where}`);
142
145
  }
146
+ await createRecentIndexes(db);
147
+ }
148
+ async function createRecentIndexes(db) {
149
+ const workspace = sql.identifier("workspace");
150
+ const index2 = sql.identifier("index");
151
+ const filePath = sql.identifier("filePath");
152
+ const updatedAt = jsonField(sql.identifier("data"), ["metadata", "updatedAt"]);
153
+ await db.run(sql`create index if not exists
154
+ ${sql.identifier("alinea_entry_index_recent_updated")}
155
+ on ${EntryIndexTable}(${workspace}, ${updatedAt}, ${index2}, ${filePath})`);
156
+ await db.run(sql`create index if not exists
157
+ ${sql.identifier("alinea_entry_index_recent_id")}
158
+ on ${EntryIndexTable}(${workspace}, ${sql.identifier("id")} desc,
159
+ ${index2}, ${filePath})`);
143
160
  }
144
161
  function entryIndexRow(entry) {
145
162
  const payload = entry.payload ?? JSON.stringify(createRecord(entry, entry.versionStatus), null, 2);
@@ -1,5 +1,8 @@
1
+ import { type RelationCondition } from 'alinea/core/Filter';
1
2
  import { type HasSql, type Sql } from 'rado';
2
3
  type FieldSql = (name: string) => HasSql;
4
+ /** Whether a query of entries related to a field finds an entry */
5
+ type RelationSql = (name: string, query: RelationCondition) => Sql<boolean>;
3
6
  export declare function jsonField(target: HasSql, segments: Array<string>): HasSql<unknown>;
4
7
  /**
5
8
  * Match a column against a JSON array parameter, so one prepared statement
@@ -8,6 +11,6 @@ export declare function jsonField(target: HasSql, segments: Array<string>): HasS
8
11
  export declare function inJson(column: HasSql, values: HasSql<string>): Sql<boolean>;
9
12
  /** Compile a condition; `depth` names the array items it iterates. */
10
13
  export declare function compileCondition(field: HasSql, condition: unknown, depth?: number): Sql<boolean>;
11
- export declare function compileFilter(filter: unknown, field: FieldSql): Sql<boolean>;
14
+ export declare function compileFilter(filter: unknown, field: FieldSql, relation?: RelationSql): Sql<boolean>;
12
15
  export declare function arrayIncludes(field: HasSql, predicate: (item: HasSql) => Sql<boolean>, depth?: number): Sql<boolean>;
13
16
  export {};
@@ -49,20 +49,29 @@ function equals(field, value) {
49
49
  function compileCondition(field, condition, depth = 0) {
50
50
  return conditionSql(field, parseCondition(condition), depth);
51
51
  }
52
- function compileFilter(filter, field) {
53
- return filterSql(parseFilter(filter), field, 0);
52
+ function compileFilter(filter, field, relation) {
53
+ return filterSql(parseFilter(filter), field, 0, relation);
54
54
  }
55
- function filterSql(node, field, depth) {
55
+ function filterSql(node, field, depth, relation) {
56
56
  switch (node.op) {
57
57
  case "and": {
58
- const clauses = node.nodes.map((node2) => filterSql(node2, field, depth));
58
+ const clauses = node.nodes.map(
59
+ (node2) => filterSql(node2, field, depth, relation)
60
+ );
59
61
  return clauses.length ? and(...clauses) : sql.value(true);
60
62
  }
61
63
  case "or": {
62
- const clauses = node.nodes.map((node2) => filterSql(node2, field, depth));
64
+ const clauses = node.nodes.map(
65
+ (node2) => filterSql(node2, field, depth, relation)
66
+ );
63
67
  return clauses.length ? or(...clauses) : sql.value(false);
64
68
  }
65
69
  case "field":
70
+ if (node.condition.op === "relation") {
71
+ if (!relation)
72
+ throw new Error("Relation conditions are not supported here");
73
+ return relation(node.name, node.condition.query);
74
+ }
66
75
  return conditionSql(field(node.name), node.condition, depth);
67
76
  }
68
77
  }
@@ -107,6 +116,8 @@ function conditionSql(field, node, depth) {
107
116
  (item) => conditionSql(item, node.item, depth + 1),
108
117
  depth
109
118
  );
119
+ case "relation":
120
+ throw new Error("Relation conditions only apply to entries");
110
121
  }
111
122
  }
112
123
  function arrayIncludes(field, predicate, depth = 0) {
@@ -44,6 +44,7 @@ import {
44
44
  } from "alinea/core/Internal";
45
45
  import { getScope } from "alinea/core/Scope";
46
46
  import { isRecord } from "alinea/core/util/Objects";
47
+ import { LinksField } from "alinea/field/link/LinkField";
47
48
  import {
48
49
  EntryIndexTable,
49
50
  EntryReferenceTable,
@@ -58,7 +59,11 @@ import {
58
59
  jsonField
59
60
  } from "./Condition.js";
60
61
  import { searchableText, searchQuery } from "./Search.js";
61
- import { linkRelation, relationCondition } from "./Relation.js";
62
+ import {
63
+ linkRelation,
64
+ referencedByCondition,
65
+ relationCondition
66
+ } from "./Relation.js";
62
67
  var builder = new Builder();
63
68
  function readJson(value, specs) {
64
69
  if (value === null) return void 0;
@@ -131,10 +136,10 @@ var Expressions = class {
131
136
  case "entryField":
132
137
  return selecting ? this.selection(internal.name, internal.path) : this.index(internal.name, internal.path);
133
138
  case "field": {
134
- const name = this.#scope.nameOf(expression);
135
- if (!name)
139
+ const path = this.#scope.dataPathOf(expression);
140
+ if (!path)
136
141
  throw new Error("Field expression is not in the configured schema");
137
- return this.data([name], selecting);
142
+ return this.data(path, selecting);
138
143
  }
139
144
  case "value":
140
145
  return sql.value(internal.value);
@@ -218,8 +223,24 @@ function compileEntryQuery(config, query, options = {}) {
218
223
  throw new Error("SQL preview requires its dedicated query stage");
219
224
  const scope = getScope(config);
220
225
  function scalar(relationQuery) {
226
+ if (relationQuery.count) {
227
+ const { rows: rows2 } = compileEntryQuery(
228
+ config,
229
+ { ...relationQuery, status: query.status ?? "published" },
230
+ {
231
+ source: entry,
232
+ entry: alias(baseEntry, `alinea_scalar_${depth + 1}`),
233
+ depth: depth + 1,
234
+ baseEntry
235
+ }
236
+ );
237
+ const matches = rows2.as(`alinea_scalar_count_${depth + 1}`);
238
+ return sql`(${getQuery(builder.select(count()).from(matches))})`;
239
+ }
221
240
  if (!isRecord(relationQuery.select) || !hasExpr(relationQuery.select))
222
- throw new Error("A relation expression must select a single expression");
241
+ throw new Error(
242
+ "A relation expression must select a single expression or count"
243
+ );
223
244
  const { rows } = compileEntryQuery(
224
245
  config,
225
246
  { ...relationQuery, status: query.status ?? "published" },
@@ -247,6 +268,14 @@ function compileEntryQuery(config, query, options = {}) {
247
268
  const name = scope.nameOf(edge.field);
248
269
  if (!name) throw new Error("Link field is not in the configured schema");
249
270
  links = linkRelation(source, name, edge.edge === "entryMultiple");
271
+ } else if (edge.edge === "referencedBy") {
272
+ const field = edge.field;
273
+ const path = field && scope.dataPathOf(field);
274
+ if (field && !path)
275
+ throw new Error("Link field is not in the configured schema");
276
+ const via = path && { path, multiple: field instanceof LinksField };
277
+ const ownLocale = query.locale === void 0 && !query.preferredLocale;
278
+ conditions.push(referencedByCondition(entry, source, via, ownLocale));
250
279
  } else conditions.push(relationCondition(entry, edge, source));
251
280
  }
252
281
  conditions.push(statusCondition(entry, query.status));
@@ -290,7 +319,9 @@ function compileEntryQuery(config, query, options = {}) {
290
319
  if (!name) throw new Error("Query type is not in the configured schema");
291
320
  return name;
292
321
  });
293
- conditions.push(compileCondition(membership.index("type"), { in: names }));
322
+ conditions.push(
323
+ names.length === 1 ? eq(entry.type, names[0]) : compileCondition(membership.index("type"), { in: names })
324
+ );
294
325
  }
295
326
  const location = Array.isArray(query.location) ? query.location : query.location && scope.locationOf(query.location);
296
327
  if (location && location.length >= 1 && location.length <= 3) {
@@ -307,8 +338,29 @@ function compileEntryQuery(config, query, options = {}) {
307
338
  (item) => compileCondition(jsonField(item, ["url"]), query.alias, 1)
308
339
  )
309
340
  );
341
+ function related(name, condition) {
342
+ if (name !== "_referenced" || condition.edge !== "referencedBy")
343
+ throw new Error(`Unsupported relation condition on ${name}`);
344
+ const { rows } = compileEntryQuery(
345
+ config,
346
+ {
347
+ ...condition,
348
+ count: true,
349
+ status: query.status ?? "published"
350
+ },
351
+ {
352
+ source: entry,
353
+ entry: alias(baseEntry, `alinea_exists_${depth + 1}`),
354
+ depth: depth + 1,
355
+ baseEntry
356
+ }
357
+ );
358
+ return exists(rows);
359
+ }
310
360
  if (query.filter !== void 0)
311
- conditions.push(compileFilter(query.filter, (name) => membership.field(name)));
361
+ conditions.push(
362
+ compileFilter(query.filter, (name) => membership.field(name), related)
363
+ );
312
364
  if (Array.isArray(query.groupBy))
313
365
  throw new Error("groupBy must be a single field");
314
366
  const grouping = query.groupBy ? [membership.expr(query.groupBy)] : void 0;
@@ -1,6 +1,6 @@
1
1
  import type { EdgeQuery } from 'alinea/core/Graph';
2
2
  import { type Sql } from 'rado';
3
- import type { EntryIndexTarget } from '../entry/EntryTable.js';
3
+ import { type EntryIndexTarget } from '../entry/EntryTable.js';
4
4
  /** Expand the stored references as SQL rows, retaining list order and duplicates. */
5
5
  export declare function linkRelation(source: EntryIndexTarget, field: string, multiple: boolean): {
6
6
  target: Sql<unknown>;
@@ -9,3 +9,15 @@ export declare function linkRelation(source: EntryIndexTarget, field: string, mu
9
9
  };
10
10
  /** Restrict related identities in SQL before the query's own filters/paging. */
11
11
  export declare function relationCondition(entry: EntryIndexTarget, query: EdgeQuery, source: EntryIndexTarget): Sql<boolean>;
12
+ /** A link field the referencing entries link through */
13
+ export interface ReferenceField {
14
+ /** Where the field is stored in the entry data */
15
+ path: Array<string>;
16
+ multiple: boolean;
17
+ }
18
+ /**
19
+ * Entries that link to the source: the reference index finds them, a link
20
+ * field narrows them down to the ones linking through it. They share the
21
+ * locale of the source, unless either has none.
22
+ */
23
+ export declare function referencedByCondition(entry: EntryIndexTarget, source: EntryIndexTarget, field?: ReferenceField, sameLocale?: boolean): Sql<boolean>;
@@ -29,7 +29,10 @@ import {
29
29
  import "../../chunks/chunk-CQ52GEHY.js";
30
30
 
31
31
  // src/database/query/Relation.ts
32
- import { jsonField } from "./Condition.js";
32
+ import {
33
+ EntryReferenceTable
34
+ } from "../entry/EntryTable.js";
35
+ import { arrayIncludes, jsonField } from "./Condition.js";
33
36
  function linkRelation(source, field, multiple) {
34
37
  const value = getSql(jsonField(source.data, [field])).forSelection();
35
38
  const array = multiple ? value : sql`json_array(json(${value}))`;
@@ -117,7 +120,22 @@ function relationCondition(entry, query, source) {
117
120
  throw new Error(`SQL link relation is not implemented: ${query.edge}`);
118
121
  }
119
122
  }
123
+ function referencedByCondition(entry, source, field, sameLocale = true) {
124
+ const references = new Builder().select(EntryReferenceTable.source).from(EntryReferenceTable).where(eq(EntryReferenceTable.targetId, source.id));
125
+ const value = field && jsonField(entry.data, field.path);
126
+ const linked = !value ? sql.value(true) : field.multiple ? arrayIncludes(value, (item) => eq(jsonField(item, ["_entry"]), source.id)) : eq(jsonField(value, ["_entry"]), source.id);
127
+ return and(
128
+ inArray(entry.rowid, references),
129
+ linked,
130
+ sameLocale ? or(
131
+ sql`${source.locale} is null`,
132
+ sql`${entry.locale} is null`,
133
+ eq(entry.locale, source.locale)
134
+ ) : sql.value(true)
135
+ );
136
+ }
120
137
  export {
121
138
  linkRelation,
139
+ referencedByCondition,
122
140
  relationCondition
123
141
  };