collabmd 0.1.31 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-Br4qyh3r.js} +1 -1
- package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
- package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
- package/dist/client/assets/index-CxSbUTs2.css +1 -0
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-BjHHMlv0.js +1249 -0
- package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +4 -1
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +176 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +2 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +67 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +26 -7
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +103 -28
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/bases/base-definition.js +255 -0
- package/src/server/domain/bases/base-expression-runtime.js +1159 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
- package/src/server/domain/bases/base-query-results.js +205 -0
- package/src/server/domain/bases/base-query-service.js +68 -1965
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +109 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Bia2hI25.js} +0 -0
|
@@ -1,1954 +1,111 @@
|
|
|
1
|
-
import { basename, dirname, extname } from 'node:path';
|
|
2
|
-
|
|
3
|
-
import yaml from 'js-yaml';
|
|
4
|
-
|
|
5
|
-
import { createWikiTargetIndex, resolveWikiTargetWithIndex } from '../../../domain/wiki-link-resolver.js';
|
|
6
1
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function isPlainObject(value) {
|
|
21
|
-
return Object.prototype.toString.call(value) === '[object Object]';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function capitalize(value = '') {
|
|
25
|
-
const normalized = String(value ?? '').trim();
|
|
26
|
-
if (!normalized) {
|
|
27
|
-
return '';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return `${normalized.charAt(0).toUpperCase()}${normalized.slice(1)}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeTags(value) {
|
|
34
|
-
if (Array.isArray(value)) {
|
|
35
|
-
return value
|
|
36
|
-
.map((entry) => String(entry ?? '').trim().replace(/^#/u, ''))
|
|
37
|
-
.filter(Boolean);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const normalized = String(value ?? '').trim().replace(/^#/u, '');
|
|
41
|
-
return normalized ? [normalized] : [];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeVaultPathSegments(pathValue = '') {
|
|
45
|
-
return String(pathValue ?? '')
|
|
46
|
-
.replace(/\\/g, '/')
|
|
47
|
-
.split('/')
|
|
48
|
-
.filter(Boolean);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function resolveVaultRelativePath(fromFilePath, relativePath) {
|
|
52
|
-
const sourceSegments = normalizeVaultPathSegments(fromFilePath);
|
|
53
|
-
const targetSegments = String(relativePath ?? '')
|
|
54
|
-
.split('/')
|
|
55
|
-
.map((segment) => {
|
|
56
|
-
try {
|
|
57
|
-
return decodeURIComponent(segment);
|
|
58
|
-
} catch {
|
|
59
|
-
return segment;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (targetSegments.length === 0) {
|
|
64
|
-
return '';
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
sourceSegments.pop();
|
|
68
|
-
const resolvedSegments = [...sourceSegments];
|
|
69
|
-
|
|
70
|
-
for (const rawSegment of targetSegments) {
|
|
71
|
-
const segment = String(rawSegment ?? '').trim();
|
|
72
|
-
if (!segment || segment === '.') {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (segment === '..') {
|
|
77
|
-
if (resolvedSegments.length === 0) {
|
|
78
|
-
return '';
|
|
79
|
-
}
|
|
80
|
-
resolvedSegments.pop();
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
resolvedSegments.push(segment);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return resolvedSegments.join('/');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function extractInlineTags(markdownText = '') {
|
|
91
|
-
const tags = new Set();
|
|
92
|
-
let match;
|
|
93
|
-
while ((match = INLINE_TAG_RE.exec(String(markdownText ?? ''))) !== null) {
|
|
94
|
-
tags.add(match[2]);
|
|
95
|
-
}
|
|
96
|
-
return [...tags];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function extractReferences(markdownText = '', wikiTargetIndex) {
|
|
100
|
-
const links = [];
|
|
101
|
-
const embeds = [];
|
|
102
|
-
let match;
|
|
103
|
-
while ((match = INTERNAL_LINK_RE.exec(String(markdownText ?? ''))) !== null) {
|
|
104
|
-
const isEmbed = Boolean(match[1]);
|
|
105
|
-
const rawTarget = String(match[2] ?? '').trim();
|
|
106
|
-
if (!rawTarget) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const resolvedPath = resolveWikiTargetWithIndex(rawTarget, wikiTargetIndex);
|
|
111
|
-
const linkValue = createLinkValue(resolvedPath || rawTarget, {
|
|
112
|
-
exists: Boolean(resolvedPath),
|
|
113
|
-
rawTarget,
|
|
114
|
-
});
|
|
115
|
-
links.push(linkValue);
|
|
116
|
-
if (isEmbed) {
|
|
117
|
-
embeds.push(linkValue);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return { embeds, links };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function dedupeLinkValues(values = []) {
|
|
124
|
-
const seen = new Set();
|
|
125
|
-
return values.filter((value) => {
|
|
126
|
-
const key = value?.external
|
|
127
|
-
? `external:${value.url || value.rawTarget || value.display || ''}`
|
|
128
|
-
: `path:${value.path || value.rawTarget || value.display || ''}`;
|
|
129
|
-
if (!key || seen.has(key)) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
seen.add(key);
|
|
133
|
-
return true;
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function createLinkValue(target, {
|
|
138
|
-
display = '',
|
|
139
|
-
exists = false,
|
|
140
|
-
external = false,
|
|
141
|
-
rawTarget = '',
|
|
142
|
-
} = {}) {
|
|
143
|
-
return {
|
|
144
|
-
__baseType: 'link',
|
|
145
|
-
display: display ? String(display) : '',
|
|
146
|
-
exists: Boolean(exists),
|
|
147
|
-
external: Boolean(external),
|
|
148
|
-
path: external ? '' : String(target ?? ''),
|
|
149
|
-
rawTarget: String(rawTarget || target || ''),
|
|
150
|
-
url: external ? String(target ?? '') : '',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function createFormulaNamespace(row, evaluationState) {
|
|
155
|
-
return {
|
|
156
|
-
__baseType: 'formula-namespace',
|
|
157
|
-
evaluationState,
|
|
158
|
-
row,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function createContextFileValue(fileValue) {
|
|
163
|
-
return fileValue ?? null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function compareVaultPaths(left = '', right = '') {
|
|
167
|
-
return String(left).localeCompare(String(right), undefined, { sensitivity: 'base' });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function isWorkspaceFileEntry(entry = {}) {
|
|
171
|
-
return entry?.nodeType === 'file' || entry?.type !== 'directory';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function listWorkspaceFilePaths(workspaceState = {}) {
|
|
175
|
-
if (Array.isArray(workspaceState?.filePaths)) {
|
|
176
|
-
return [...workspaceState.filePaths].sort(compareVaultPaths);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return Array.from(workspaceState?.entries?.values?.() ?? [])
|
|
180
|
-
.filter((entry) => isWorkspaceFileEntry(entry))
|
|
181
|
-
.map((entry) => entry.path)
|
|
182
|
-
.sort(compareVaultPaths);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function normalizeWikiTargetKey(target = '') {
|
|
186
|
-
const normalizedTarget = String(target ?? '').trim();
|
|
187
|
-
if (!normalizedTarget) {
|
|
188
|
-
return '';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return getVaultFileExtension(normalizedTarget)
|
|
192
|
-
? normalizedTarget
|
|
193
|
-
: `${normalizedTarget}.md`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function collectWikiTargetKeysForFilePath(filePath = '') {
|
|
197
|
-
const normalizedPath = String(filePath ?? '').trim();
|
|
198
|
-
if (!normalizedPath) {
|
|
199
|
-
return [];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const segments = normalizedPath.split('/').filter(Boolean);
|
|
203
|
-
const keys = [];
|
|
204
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
205
|
-
keys.push(segments.slice(index).join('/'));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (isMarkdownFilePath(normalizedPath)) {
|
|
209
|
-
const rawSegments = normalizedPath.replace(/\.md$/i, '').split('/').filter(Boolean);
|
|
210
|
-
for (let index = 0; index < rawSegments.length; index += 1) {
|
|
211
|
-
keys.push(rawSegments.slice(index).join('/'));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return [...new Set(keys)];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function normalizeBaseDefinition(source = '') {
|
|
219
|
-
const raw = yaml.load(String(source ?? '')) ?? {};
|
|
220
|
-
const rawObject = isPlainObject(raw) ? raw : {};
|
|
221
|
-
const rawProperties = isPlainObject(rawObject.properties) ? rawObject.properties : {};
|
|
222
|
-
const properties = Object.entries(rawProperties).reduce((acc, [id, config]) => {
|
|
223
|
-
const normalizedConfig = isPlainObject(config) ? { ...config } : {};
|
|
224
|
-
acc[id] = {
|
|
225
|
-
displayName: typeof normalizedConfig.displayName === 'string' ? normalizedConfig.displayName : null,
|
|
226
|
-
formula: typeof normalizedConfig.formula === 'string' ? normalizedConfig.formula : null,
|
|
227
|
-
id,
|
|
228
|
-
raw: normalizedConfig,
|
|
229
|
-
};
|
|
230
|
-
return acc;
|
|
231
|
-
}, {});
|
|
232
|
-
|
|
233
|
-
const rawViews = Array.isArray(rawObject.views) ? rawObject.views : [];
|
|
234
|
-
const views = rawViews.map((rawView, index) => normalizeBaseView(rawView, index));
|
|
235
|
-
if (views.length === 0) {
|
|
236
|
-
views.push(normalizeBaseView({ name: 'Table', type: 'table' }, 0));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
filters: rawObject.filters ?? null,
|
|
241
|
-
properties,
|
|
242
|
-
raw: rawObject,
|
|
243
|
-
views,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function normalizeBaseView(rawView, index) {
|
|
248
|
-
const view = isPlainObject(rawView) ? rawView : {};
|
|
249
|
-
const type = typeof view.type === 'string' ? view.type : 'table';
|
|
250
|
-
const name = typeof view.name === 'string' && view.name.trim()
|
|
251
|
-
? view.name.trim()
|
|
252
|
-
: `${capitalize(type)} ${index + 1}`;
|
|
253
|
-
return {
|
|
254
|
-
extra: Object.entries(view).reduce((acc, [key, value]) => {
|
|
255
|
-
if ([
|
|
256
|
-
'filters',
|
|
257
|
-
'groupBy',
|
|
258
|
-
'group_by',
|
|
259
|
-
'image',
|
|
260
|
-
'limit',
|
|
261
|
-
'name',
|
|
262
|
-
'order',
|
|
263
|
-
'properties',
|
|
264
|
-
'sort',
|
|
265
|
-
'sorts',
|
|
266
|
-
'summaries',
|
|
267
|
-
'type',
|
|
268
|
-
].includes(key)) {
|
|
269
|
-
return acc;
|
|
270
|
-
}
|
|
271
|
-
acc[key] = value;
|
|
272
|
-
return acc;
|
|
273
|
-
}, {}),
|
|
274
|
-
filters: view.filters ?? null,
|
|
275
|
-
groupBy: typeof view.groupBy === 'string'
|
|
276
|
-
? view.groupBy
|
|
277
|
-
: (typeof view.group_by === 'string' ? view.group_by : null),
|
|
278
|
-
id: `view-${index}`,
|
|
279
|
-
image: typeof view.image === 'string' ? view.image : null,
|
|
280
|
-
limit: Number.isFinite(Number(view.limit)) ? Math.max(0, Number(view.limit)) : null,
|
|
281
|
-
name,
|
|
282
|
-
order: normalizeViewOrder(view.order ?? view.properties),
|
|
283
|
-
sort: normalizeViewSort(view.sort ?? view.sorts),
|
|
284
|
-
summaries: normalizeSummaries(view.summaries),
|
|
285
|
-
supported: SUPPORTED_VIEW_TYPES.has(type),
|
|
286
|
-
type,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function normalizeViewOrder(value) {
|
|
291
|
-
if (Array.isArray(value)) {
|
|
292
|
-
return value
|
|
293
|
-
.map((entry) => {
|
|
294
|
-
if (typeof entry === 'string') {
|
|
295
|
-
return entry;
|
|
296
|
-
}
|
|
297
|
-
if (isPlainObject(entry)) {
|
|
298
|
-
return entry.id ?? entry.property ?? entry.name ?? null;
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
})
|
|
302
|
-
.filter(Boolean);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (typeof value === 'string' && value.trim()) {
|
|
306
|
-
return [value.trim()];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return [];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function normalizeViewSort(value) {
|
|
313
|
-
if (typeof value === 'string' && value.trim()) {
|
|
314
|
-
return [{ direction: 'asc', property: value.trim() }];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (!Array.isArray(value)) {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return value
|
|
322
|
-
.map((entry) => {
|
|
323
|
-
if (typeof entry === 'string') {
|
|
324
|
-
return { direction: 'asc', property: entry };
|
|
325
|
-
}
|
|
326
|
-
if (!isPlainObject(entry)) {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const property = entry.property ?? entry.id ?? entry.name ?? null;
|
|
331
|
-
if (typeof property !== 'string' || !property.trim()) {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const direction = String(entry.direction ?? entry.order ?? 'asc').toLowerCase() === 'desc'
|
|
336
|
-
? 'desc'
|
|
337
|
-
: 'asc';
|
|
338
|
-
return { direction, property: property.trim() };
|
|
339
|
-
})
|
|
340
|
-
.filter(Boolean);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function normalizeSummaries(value) {
|
|
344
|
-
if (!value) {
|
|
345
|
-
return [];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (Array.isArray(value)) {
|
|
349
|
-
return value
|
|
350
|
-
.map((entry, index) => normalizeSummary(entry, index))
|
|
351
|
-
.filter(Boolean);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (isPlainObject(value)) {
|
|
355
|
-
const entries = [];
|
|
356
|
-
Object.entries(value).forEach(([property, propertySummaries]) => {
|
|
357
|
-
if (Array.isArray(propertySummaries)) {
|
|
358
|
-
propertySummaries.forEach((entry, index) => {
|
|
359
|
-
const normalized = normalizeSummary(entry, index, property);
|
|
360
|
-
if (normalized) {
|
|
361
|
-
entries.push(normalized);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const normalized = normalizeSummary(propertySummaries, entries.length, property);
|
|
368
|
-
if (normalized) {
|
|
369
|
-
entries.push(normalized);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
return entries;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return [];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function normalizeSummary(entry, index, property = null) {
|
|
379
|
-
if (typeof entry === 'string') {
|
|
380
|
-
return {
|
|
381
|
-
id: `summary-${index}`,
|
|
382
|
-
label: capitalize(entry),
|
|
383
|
-
property,
|
|
384
|
-
type: entry.toLowerCase(),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (!isPlainObject(entry)) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const summaryProperty = typeof (entry.property ?? property) === 'string'
|
|
393
|
-
? String(entry.property ?? property).trim()
|
|
394
|
-
: null;
|
|
395
|
-
const formula = typeof entry.formula === 'string' ? entry.formula : null;
|
|
396
|
-
const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : (formula ? 'custom' : null);
|
|
397
|
-
if (!summaryProperty) {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
formula,
|
|
403
|
-
id: `summary-${index}`,
|
|
404
|
-
label: typeof entry.name === 'string' ? entry.name : capitalize(type || 'Summary'),
|
|
405
|
-
property: summaryProperty,
|
|
406
|
-
type,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function tokenizeExpression(source = '') {
|
|
411
|
-
const tokens = [];
|
|
412
|
-
const text = String(source ?? '');
|
|
413
|
-
let index = 0;
|
|
414
|
-
|
|
415
|
-
while (index < text.length) {
|
|
416
|
-
const character = text[index];
|
|
417
|
-
if (/\s/u.test(character)) {
|
|
418
|
-
index += 1;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const twoChar = text.slice(index, index + 2);
|
|
423
|
-
const threeChar = text.slice(index, index + 3);
|
|
424
|
-
if (['&&', '||', '==', '!=', '<=', '>='].includes(twoChar)) {
|
|
425
|
-
tokens.push({ type: 'operator', value: twoChar });
|
|
426
|
-
index += 2;
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
if (threeChar === '...') {
|
|
430
|
-
tokens.push({ type: 'operator', value: threeChar });
|
|
431
|
-
index += 3;
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
if ('+-*/%!<>()[]{}.,:'.includes(character)) {
|
|
435
|
-
tokens.push({
|
|
436
|
-
type: '()[]{}.,:'.includes(character) ? 'punctuation' : 'operator',
|
|
437
|
-
value: character,
|
|
438
|
-
});
|
|
439
|
-
index += 1;
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
if (character === '"' || character === '\'') {
|
|
443
|
-
let value = '';
|
|
444
|
-
let cursor = index + 1;
|
|
445
|
-
while (cursor < text.length) {
|
|
446
|
-
const nextCharacter = text[cursor];
|
|
447
|
-
if (nextCharacter === '\\') {
|
|
448
|
-
const escaped = text[cursor + 1] ?? '';
|
|
449
|
-
value += escaped;
|
|
450
|
-
cursor += 2;
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (nextCharacter === character) {
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
value += nextCharacter;
|
|
457
|
-
cursor += 1;
|
|
458
|
-
}
|
|
459
|
-
if (text[cursor] !== character) {
|
|
460
|
-
throw new Error(`Unterminated string in expression: ${source}`);
|
|
461
|
-
}
|
|
462
|
-
tokens.push({ type: 'string', value });
|
|
463
|
-
index = cursor + 1;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (/\d/u.test(character)) {
|
|
467
|
-
let cursor = index + 1;
|
|
468
|
-
while (cursor < text.length && /[\d.]/u.test(text[cursor])) {
|
|
469
|
-
cursor += 1;
|
|
470
|
-
}
|
|
471
|
-
tokens.push({ type: 'number', value: text.slice(index, cursor) });
|
|
472
|
-
index = cursor;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
if (/[A-Za-z_$]/u.test(character)) {
|
|
476
|
-
let cursor = index + 1;
|
|
477
|
-
while (cursor < text.length && /[A-Za-z0-9_$]/u.test(text[cursor])) {
|
|
478
|
-
cursor += 1;
|
|
479
|
-
}
|
|
480
|
-
tokens.push({ type: 'identifier', value: text.slice(index, cursor) });
|
|
481
|
-
index = cursor;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
throw new Error(`Unexpected token "${character}" in expression: ${source}`);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return tokens;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const BINARY_PRECEDENCE = {
|
|
492
|
-
'||': 1,
|
|
493
|
-
'&&': 2,
|
|
494
|
-
'==': 3,
|
|
495
|
-
'!=': 3,
|
|
496
|
-
'<': 4,
|
|
497
|
-
'<=': 4,
|
|
498
|
-
'>': 4,
|
|
499
|
-
'>=': 4,
|
|
500
|
-
'+': 5,
|
|
501
|
-
'-': 5,
|
|
502
|
-
'*': 6,
|
|
503
|
-
'/': 6,
|
|
504
|
-
'%': 6,
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
function parseExpression(source = '') {
|
|
508
|
-
const tokens = tokenizeExpression(source);
|
|
509
|
-
let index = 0;
|
|
510
|
-
|
|
511
|
-
const peek = () => tokens[index] ?? null;
|
|
512
|
-
const consume = (expectedValue = null) => {
|
|
513
|
-
const token = tokens[index] ?? null;
|
|
514
|
-
if (!token) {
|
|
515
|
-
throw new Error(`Unexpected end of expression: ${source}`);
|
|
516
|
-
}
|
|
517
|
-
if (expectedValue !== null && token.value !== expectedValue) {
|
|
518
|
-
throw new Error(`Expected "${expectedValue}" in expression: ${source}`);
|
|
519
|
-
}
|
|
520
|
-
index += 1;
|
|
521
|
-
return token;
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
const parsePrimary = () => {
|
|
525
|
-
const token = peek();
|
|
526
|
-
if (!token) {
|
|
527
|
-
throw new Error(`Unexpected end of expression: ${source}`);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (token.type === 'number') {
|
|
531
|
-
consume();
|
|
532
|
-
return { type: 'Literal', value: Number(token.value) };
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (token.type === 'string') {
|
|
536
|
-
consume();
|
|
537
|
-
return { type: 'Literal', value: token.value };
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (token.type === 'identifier') {
|
|
541
|
-
consume();
|
|
542
|
-
if (token.value === 'true') return { type: 'Literal', value: true };
|
|
543
|
-
if (token.value === 'false') return { type: 'Literal', value: false };
|
|
544
|
-
if (token.value === 'null') return { type: 'Literal', value: null };
|
|
545
|
-
return { name: token.value, type: 'Identifier' };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (token.value === '(') {
|
|
549
|
-
consume('(');
|
|
550
|
-
const expression = parseBinaryExpression(0);
|
|
551
|
-
consume(')');
|
|
552
|
-
return expression;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (token.value === '[') {
|
|
556
|
-
consume('[');
|
|
557
|
-
const elements = [];
|
|
558
|
-
while (peek() && peek().value !== ']') {
|
|
559
|
-
elements.push(parseBinaryExpression(0));
|
|
560
|
-
if (peek()?.value === ',') {
|
|
561
|
-
consume(',');
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
consume(']');
|
|
565
|
-
return { elements, type: 'ArrayExpression' };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (token.value === '{') {
|
|
569
|
-
consume('{');
|
|
570
|
-
const properties = [];
|
|
571
|
-
while (peek() && peek().value !== '}') {
|
|
572
|
-
const keyToken = consume();
|
|
573
|
-
const key = keyToken.type === 'identifier' ? keyToken.value : String(keyToken.value ?? '');
|
|
574
|
-
consume(':');
|
|
575
|
-
properties.push({ key, value: parseBinaryExpression(0) });
|
|
576
|
-
if (peek()?.value === ',') {
|
|
577
|
-
consume(',');
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
consume('}');
|
|
581
|
-
return { properties, type: 'ObjectExpression' };
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
throw new Error(`Unexpected token "${token.value}" in expression: ${source}`);
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
const parsePostfix = (baseNode) => {
|
|
588
|
-
let node = baseNode;
|
|
589
|
-
while (peek()) {
|
|
590
|
-
if (peek().value === '.') {
|
|
591
|
-
consume('.');
|
|
592
|
-
const property = consume();
|
|
593
|
-
if (property.type !== 'identifier') {
|
|
594
|
-
throw new Error(`Expected property name in expression: ${source}`);
|
|
595
|
-
}
|
|
596
|
-
node = {
|
|
597
|
-
computed: false,
|
|
598
|
-
object: node,
|
|
599
|
-
property: { name: property.value, type: 'Identifier' },
|
|
600
|
-
type: 'MemberExpression',
|
|
601
|
-
};
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (peek().value === '[') {
|
|
606
|
-
consume('[');
|
|
607
|
-
const property = parseBinaryExpression(0);
|
|
608
|
-
consume(']');
|
|
609
|
-
node = {
|
|
610
|
-
computed: true,
|
|
611
|
-
object: node,
|
|
612
|
-
property,
|
|
613
|
-
type: 'MemberExpression',
|
|
614
|
-
};
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (peek().value === '(') {
|
|
619
|
-
consume('(');
|
|
620
|
-
const args = [];
|
|
621
|
-
while (peek() && peek().value !== ')') {
|
|
622
|
-
args.push(parseBinaryExpression(0));
|
|
623
|
-
if (peek()?.value === ',') {
|
|
624
|
-
consume(',');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
consume(')');
|
|
628
|
-
node = {
|
|
629
|
-
arguments: args,
|
|
630
|
-
callee: node,
|
|
631
|
-
type: 'CallExpression',
|
|
632
|
-
};
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
638
|
-
return node;
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
const parseUnary = () => {
|
|
642
|
-
const token = peek();
|
|
643
|
-
if (token?.type === 'operator' && ['!', '-'].includes(token.value)) {
|
|
644
|
-
consume();
|
|
645
|
-
return {
|
|
646
|
-
argument: parseUnary(),
|
|
647
|
-
operator: token.value,
|
|
648
|
-
type: 'UnaryExpression',
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
return parsePostfix(parsePrimary());
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
const parseBinaryExpression = (minimumPrecedence) => {
|
|
656
|
-
let left = parseUnary();
|
|
657
|
-
while (true) {
|
|
658
|
-
const operator = peek();
|
|
659
|
-
const precedence = operator ? BINARY_PRECEDENCE[operator.value] : null;
|
|
660
|
-
if (!precedence || precedence < minimumPrecedence) {
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
consume();
|
|
665
|
-
const right = parseBinaryExpression(precedence + 1);
|
|
666
|
-
left = {
|
|
667
|
-
left,
|
|
668
|
-
operator: operator.value,
|
|
669
|
-
right,
|
|
670
|
-
type: 'BinaryExpression',
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
return left;
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
const ast = parseBinaryExpression(0);
|
|
677
|
-
if (index < tokens.length) {
|
|
678
|
-
throw new Error(`Unexpected token "${tokens[index].value}" in expression: ${source}`);
|
|
679
|
-
}
|
|
680
|
-
return ast;
|
|
681
|
-
}
|
|
2
|
+
buildColumns,
|
|
3
|
+
collectEvaluatedPropertyIds,
|
|
4
|
+
normalizeBaseDefinition,
|
|
5
|
+
} from './base-definition.js';
|
|
6
|
+
import {
|
|
7
|
+
compareValues,
|
|
8
|
+
createEvaluationRootContext,
|
|
9
|
+
evaluateFilterNode,
|
|
10
|
+
getPropertyValue,
|
|
11
|
+
} from './base-expression-runtime.js';
|
|
12
|
+
import { BaseIndexSnapshotStore } from './base-index-snapshot-store.js';
|
|
13
|
+
import { buildQueryResultPayload, rowMatchesSearch } from './base-query-results.js';
|
|
682
14
|
|
|
683
|
-
|
|
684
|
-
constructor({ locals = {}, parent = null, rootContext }) {
|
|
685
|
-
this.locals = locals;
|
|
686
|
-
this.parent = parent;
|
|
687
|
-
this.rootContext = rootContext;
|
|
688
|
-
}
|
|
15
|
+
export { serializeBaseDefinition } from './base-definition.js';
|
|
689
16
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
17
|
+
export class BaseQueryService {
|
|
18
|
+
constructor({
|
|
19
|
+
vaultFileStore,
|
|
20
|
+
workspaceStateProvider = null,
|
|
21
|
+
workspaceStateSynchronizer = null,
|
|
22
|
+
}) {
|
|
23
|
+
this.vaultFileStore = vaultFileStore;
|
|
24
|
+
this.snapshotStore = new BaseIndexSnapshotStore({
|
|
25
|
+
vaultFileStore,
|
|
26
|
+
workspaceStateProvider,
|
|
27
|
+
workspaceStateSynchronizer,
|
|
695
28
|
});
|
|
696
29
|
}
|
|
697
30
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return true;
|
|
701
|
-
}
|
|
702
|
-
return this.parent?.has(name) ?? false;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
get(name) {
|
|
706
|
-
if (Object.hasOwn(this.locals, name)) {
|
|
707
|
-
return this.locals[name];
|
|
708
|
-
}
|
|
709
|
-
if (this.parent) {
|
|
710
|
-
return this.parent.get(name);
|
|
711
|
-
}
|
|
712
|
-
return this.rootContext.resolveIdentifier(name);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function normalizeDuration(duration) {
|
|
717
|
-
const match = String(duration ?? '').trim().match(/^(-?\d+)\s*([smhdw])$/iu);
|
|
718
|
-
if (!match) {
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const amount = Number(match[1]);
|
|
723
|
-
const unit = match[2].toLowerCase();
|
|
724
|
-
const multiplier = {
|
|
725
|
-
d: 86_400_000,
|
|
726
|
-
h: 3_600_000,
|
|
727
|
-
m: 60_000,
|
|
728
|
-
s: 1_000,
|
|
729
|
-
w: 604_800_000,
|
|
730
|
-
}[unit];
|
|
731
|
-
return Number.isFinite(amount) && multiplier ? amount * multiplier : null;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function parseDateValue(value) {
|
|
735
|
-
if (value instanceof Date) {
|
|
736
|
-
return Number.isFinite(value.getTime()) ? value : null;
|
|
737
|
-
}
|
|
738
|
-
if (typeof value === 'number') {
|
|
739
|
-
const date = new Date(value);
|
|
740
|
-
return Number.isFinite(date.getTime()) ? date : null;
|
|
741
|
-
}
|
|
742
|
-
if (typeof value !== 'string') {
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const normalized = value.trim();
|
|
747
|
-
if (!normalized) {
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (/^\d{8}$/u.test(normalized)) {
|
|
752
|
-
return new Date(`${normalized.slice(0, 4)}-${normalized.slice(4, 6)}-${normalized.slice(6, 8)}T00:00:00`);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (/^\d{8}\d{4}$/u.test(normalized)) {
|
|
756
|
-
return new Date(`${normalized.slice(0, 4)}-${normalized.slice(4, 6)}-${normalized.slice(6, 8)}T${normalized.slice(8, 10)}:${normalized.slice(10, 12)}:00`);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const date = new Date(normalized);
|
|
760
|
-
return Number.isFinite(date.getTime()) ? date : null;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function isDateLikeString(value) {
|
|
764
|
-
if (typeof value !== 'string') {
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const normalized = value.trim();
|
|
769
|
-
if (!normalized) {
|
|
770
|
-
return false;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
return (
|
|
774
|
-
/^\d{8}(?:\d{4})?$/u.test(normalized)
|
|
775
|
-
|| /^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})?)?$/u.test(normalized)
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function coerceDateComparable(value) {
|
|
780
|
-
if (!isDateLikeString(value)) {
|
|
781
|
-
return value;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return parseDateValue(value) ?? value;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function normalizeComparablePair(left, right) {
|
|
788
|
-
if (left instanceof Date || right instanceof Date) {
|
|
789
|
-
return [
|
|
790
|
-
left instanceof Date ? left : coerceDateComparable(left),
|
|
791
|
-
right instanceof Date ? right : coerceDateComparable(right),
|
|
792
|
-
];
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (typeof left === 'string' && typeof right === 'string' && isDateLikeString(left) && isDateLikeString(right)) {
|
|
796
|
-
return [
|
|
797
|
-
parseDateValue(left) ?? left,
|
|
798
|
-
parseDateValue(right) ?? right,
|
|
799
|
-
];
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return [left, right];
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function formatDateValue(value, format = 'YYYY-MM-DD') {
|
|
806
|
-
const date = parseDateValue(value);
|
|
807
|
-
if (!date) {
|
|
808
|
-
return '';
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const padded = (number) => String(number).padStart(2, '0');
|
|
812
|
-
return String(format ?? 'YYYY-MM-DD')
|
|
813
|
-
.replace(/YYYY/g, String(date.getFullYear()))
|
|
814
|
-
.replace(/MM/g, padded(date.getMonth() + 1))
|
|
815
|
-
.replace(/DD/g, padded(date.getDate()))
|
|
816
|
-
.replace(/HH/g, padded(date.getHours()))
|
|
817
|
-
.replace(/mm/g, padded(date.getMinutes()))
|
|
818
|
-
.replace(/ss/g, padded(date.getSeconds()));
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function toComparableValue(value) {
|
|
822
|
-
if (value instanceof Date) {
|
|
823
|
-
return value.getTime();
|
|
824
|
-
}
|
|
825
|
-
return value;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function toDisplayText(value) {
|
|
829
|
-
if (value == null) {
|
|
830
|
-
return '';
|
|
831
|
-
}
|
|
832
|
-
if (value instanceof Date) {
|
|
833
|
-
return formatDateValue(value, 'YYYY-MM-DD HH:mm:ss');
|
|
834
|
-
}
|
|
835
|
-
if (Array.isArray(value)) {
|
|
836
|
-
return value.map((entry) => toDisplayText(entry)).join(', ');
|
|
837
|
-
}
|
|
838
|
-
if (typeof value === 'boolean') {
|
|
839
|
-
return value ? 'true' : 'false';
|
|
840
|
-
}
|
|
841
|
-
if (typeof value === 'number') {
|
|
842
|
-
return Number.isFinite(value) ? String(value) : '';
|
|
843
|
-
}
|
|
844
|
-
if (value?.__baseType === 'file') {
|
|
845
|
-
return value.path || value.name || '';
|
|
846
|
-
}
|
|
847
|
-
if (value?.__baseType === 'link') {
|
|
848
|
-
return value.display || value.path || value.url || '';
|
|
849
|
-
}
|
|
850
|
-
if (isPlainObject(value)) {
|
|
851
|
-
try {
|
|
852
|
-
return JSON.stringify(value);
|
|
853
|
-
} catch {
|
|
854
|
-
return '[object Object]';
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return String(value);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function valuesEqual(left, right) {
|
|
861
|
-
const [normalizedLeft, normalizedRight] = normalizeComparablePair(left, right);
|
|
862
|
-
|
|
863
|
-
left = normalizedLeft;
|
|
864
|
-
right = normalizedRight;
|
|
865
|
-
|
|
866
|
-
if (left?.__baseType === 'file' && right?.__baseType === 'file') {
|
|
867
|
-
return left.path === right.path;
|
|
868
|
-
}
|
|
869
|
-
if (left?.__baseType === 'link' && right?.__baseType === 'link') {
|
|
870
|
-
return left.url === right.url && left.path === right.path;
|
|
871
|
-
}
|
|
872
|
-
if (left instanceof Date || right instanceof Date) {
|
|
873
|
-
return toComparableValue(left) === toComparableValue(right);
|
|
874
|
-
}
|
|
875
|
-
return left === right;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function resolveImageAttachmentPath(value, {
|
|
879
|
-
rowFilePath = '',
|
|
880
|
-
snapshot = null,
|
|
881
|
-
sourcePath = '',
|
|
882
|
-
} = {}) {
|
|
883
|
-
const text = String(value ?? '').trim();
|
|
884
|
-
const rowsByPath = snapshot?.rowsByPath;
|
|
885
|
-
if (!text || !rowsByPath) {
|
|
886
|
-
return null;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const candidates = [];
|
|
890
|
-
const seen = new Set();
|
|
891
|
-
const appendCandidate = (candidate) => {
|
|
892
|
-
const normalized = String(candidate ?? '').trim().replace(/\\/g, '/');
|
|
893
|
-
if (!normalized || seen.has(normalized)) {
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
seen.add(normalized);
|
|
897
|
-
candidates.push(normalized);
|
|
898
|
-
};
|
|
899
|
-
|
|
900
|
-
if (rowFilePath) {
|
|
901
|
-
appendCandidate(resolveVaultRelativePath(rowFilePath, text));
|
|
902
|
-
}
|
|
903
|
-
if (sourcePath) {
|
|
904
|
-
appendCandidate(resolveVaultRelativePath(sourcePath, text));
|
|
905
|
-
}
|
|
906
|
-
appendCandidate(text);
|
|
907
|
-
|
|
908
|
-
return candidates.find((candidate) => (
|
|
909
|
-
isImageAttachmentFilePath(candidate)
|
|
910
|
-
&& rowsByPath.has(candidate)
|
|
911
|
-
)) ?? null;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function serializeBaseValue(value, options = {}) {
|
|
915
|
-
if (value == null || value === '') {
|
|
916
|
-
return { text: '', type: 'empty', value: null };
|
|
917
|
-
}
|
|
918
|
-
if (value instanceof Date) {
|
|
919
|
-
return {
|
|
920
|
-
iso: value.toISOString(),
|
|
921
|
-
text: formatDateValue(value, 'YYYY-MM-DD HH:mm:ss'),
|
|
922
|
-
type: 'date',
|
|
923
|
-
value: value.toISOString(),
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
if (Array.isArray(value)) {
|
|
927
|
-
return {
|
|
928
|
-
items: value.map((entry) => serializeBaseValue(entry, options)),
|
|
929
|
-
text: toDisplayText(value),
|
|
930
|
-
type: 'list',
|
|
931
|
-
value: value.map((entry) => toDisplayText(entry)),
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
if (value?.__baseType === 'file') {
|
|
935
|
-
return {
|
|
936
|
-
path: value.path,
|
|
937
|
-
text: value.path,
|
|
938
|
-
type: 'file',
|
|
939
|
-
value: value.path,
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
if (value?.__baseType === 'link') {
|
|
943
|
-
return {
|
|
944
|
-
external: value.external,
|
|
945
|
-
path: value.path,
|
|
946
|
-
text: value.display || value.path || value.url,
|
|
947
|
-
type: 'link',
|
|
948
|
-
url: value.url,
|
|
949
|
-
value: value.display || value.path || value.url,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
if (typeof value === 'boolean') {
|
|
953
|
-
return { text: value ? 'true' : 'false', type: 'boolean', value };
|
|
954
|
-
}
|
|
955
|
-
if (typeof value === 'number') {
|
|
956
|
-
return { text: Number.isFinite(value) ? String(value) : '', type: 'number', value };
|
|
31
|
+
get workspaceStateProvider() {
|
|
32
|
+
return this.snapshotStore.workspaceStateProvider;
|
|
957
33
|
}
|
|
958
|
-
if (typeof value === 'string') {
|
|
959
|
-
const imagePath = resolveImageAttachmentPath(value, options);
|
|
960
|
-
if (imagePath) {
|
|
961
|
-
return {
|
|
962
|
-
path: imagePath,
|
|
963
|
-
text: value,
|
|
964
|
-
type: 'image',
|
|
965
|
-
value: imagePath,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
return { text: value, type: 'string', value };
|
|
969
|
-
}
|
|
970
|
-
return {
|
|
971
|
-
text: toDisplayText(value),
|
|
972
|
-
type: 'object',
|
|
973
|
-
value,
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function compareValues(left, right) {
|
|
978
|
-
const [normalizedLeft, normalizedRight] = normalizeComparablePair(left, right);
|
|
979
|
-
left = normalizedLeft;
|
|
980
|
-
right = normalizedRight;
|
|
981
34
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (comparableLeft == null && comparableRight == null) return 0;
|
|
985
|
-
if (comparableLeft == null) return 1;
|
|
986
|
-
if (comparableRight == null) return -1;
|
|
987
|
-
if (typeof comparableLeft === 'number' && typeof comparableRight === 'number') {
|
|
988
|
-
return comparableLeft - comparableRight;
|
|
989
|
-
}
|
|
990
|
-
return String(toDisplayText(comparableLeft)).localeCompare(String(toDisplayText(comparableRight)), undefined, { sensitivity: 'base' });
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function applyBinaryOperator(operator, left, right) {
|
|
994
|
-
if (operator === '&&') {
|
|
995
|
-
return Boolean(left) && Boolean(right);
|
|
996
|
-
}
|
|
997
|
-
if (operator === '||') {
|
|
998
|
-
return Boolean(left) || Boolean(right);
|
|
999
|
-
}
|
|
1000
|
-
if (operator === '==') {
|
|
1001
|
-
return valuesEqual(left, right);
|
|
1002
|
-
}
|
|
1003
|
-
if (operator === '!=') {
|
|
1004
|
-
return !valuesEqual(left, right);
|
|
1005
|
-
}
|
|
1006
|
-
if (operator === '<') {
|
|
1007
|
-
return compareValues(left, right) < 0;
|
|
1008
|
-
}
|
|
1009
|
-
if (operator === '<=') {
|
|
1010
|
-
return compareValues(left, right) <= 0;
|
|
1011
|
-
}
|
|
1012
|
-
if (operator === '>') {
|
|
1013
|
-
return compareValues(left, right) > 0;
|
|
1014
|
-
}
|
|
1015
|
-
if (operator === '>=') {
|
|
1016
|
-
return compareValues(left, right) >= 0;
|
|
1017
|
-
}
|
|
1018
|
-
if (operator === '+') {
|
|
1019
|
-
if (left instanceof Date && typeof right === 'string') {
|
|
1020
|
-
const duration = normalizeDuration(right);
|
|
1021
|
-
return duration == null ? left : new Date(left.getTime() + duration);
|
|
1022
|
-
}
|
|
1023
|
-
if (typeof left === 'string' || typeof right === 'string') {
|
|
1024
|
-
return `${toDisplayText(left)}${toDisplayText(right)}`;
|
|
1025
|
-
}
|
|
1026
|
-
return Number(left ?? 0) + Number(right ?? 0);
|
|
1027
|
-
}
|
|
1028
|
-
if (operator === '-') {
|
|
1029
|
-
if (left instanceof Date && typeof right === 'string') {
|
|
1030
|
-
const duration = normalizeDuration(right);
|
|
1031
|
-
return duration == null ? left : new Date(left.getTime() - duration);
|
|
1032
|
-
}
|
|
1033
|
-
if (left instanceof Date && right instanceof Date) {
|
|
1034
|
-
return left.getTime() - right.getTime();
|
|
1035
|
-
}
|
|
1036
|
-
return Number(left ?? 0) - Number(right ?? 0);
|
|
35
|
+
set workspaceStateProvider(value) {
|
|
36
|
+
this.snapshotStore.workspaceStateProvider = value;
|
|
1037
37
|
}
|
|
1038
|
-
if (operator === '*') {
|
|
1039
|
-
return Number(left ?? 0) * Number(right ?? 0);
|
|
1040
|
-
}
|
|
1041
|
-
if (operator === '/') {
|
|
1042
|
-
return Number(right ?? 0) === 0 ? null : Number(left ?? 0) / Number(right ?? 0);
|
|
1043
|
-
}
|
|
1044
|
-
if (operator === '%') {
|
|
1045
|
-
return Number(right ?? 0) === 0 ? null : Number(left ?? 0) % Number(right ?? 0);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
throw new Error(`Unsupported operator: ${operator}`);
|
|
1049
|
-
}
|
|
1050
38
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const condition = evaluate(argNodes[0], scope);
|
|
1054
|
-
return condition ? evaluate(argNodes[1], scope) : evaluate(argNodes[2], scope);
|
|
39
|
+
get workspaceStateSynchronizer() {
|
|
40
|
+
return this.snapshotStore.workspaceStateSynchronizer;
|
|
1055
41
|
}
|
|
1056
42
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
case 'date':
|
|
1060
|
-
return parseDateValue(args[0]);
|
|
1061
|
-
case 'link': {
|
|
1062
|
-
const target = String(args[0] ?? '').trim();
|
|
1063
|
-
if (!target) {
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
const external = /^[a-z]+:/iu.test(target);
|
|
1067
|
-
return createLinkValue(target, {
|
|
1068
|
-
display: typeof args[1] === 'string' ? args[1] : '',
|
|
1069
|
-
exists: external,
|
|
1070
|
-
external,
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
case 'list':
|
|
1074
|
-
return args;
|
|
1075
|
-
case 'max':
|
|
1076
|
-
return args.flat().reduce((acc, value) => (acc == null || compareValues(value, acc) > 0 ? value : acc), null);
|
|
1077
|
-
case 'min':
|
|
1078
|
-
return args.flat().reduce((acc, value) => (acc == null || compareValues(value, acc) < 0 ? value : acc), null);
|
|
1079
|
-
case 'now':
|
|
1080
|
-
return new Date();
|
|
1081
|
-
case 'today': {
|
|
1082
|
-
const date = new Date();
|
|
1083
|
-
date.setHours(0, 0, 0, 0);
|
|
1084
|
-
return date;
|
|
1085
|
-
}
|
|
1086
|
-
default:
|
|
1087
|
-
throw new Error(`Unknown function: ${name}`);
|
|
43
|
+
set workspaceStateSynchronizer(value) {
|
|
44
|
+
this.snapshotStore.workspaceStateSynchronizer = value;
|
|
1088
45
|
}
|
|
1089
|
-
}
|
|
1090
46
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
if (argNodes.length > 0) {
|
|
1094
|
-
throw new Error(`Formula namespace property "${name}" is not callable`);
|
|
1095
|
-
}
|
|
1096
|
-
return rootContext.resolveFormulaValue(name, target.row, target.evaluationState);
|
|
47
|
+
get indexSnapshot() {
|
|
48
|
+
return this.snapshotStore.indexSnapshot;
|
|
1097
49
|
}
|
|
1098
50
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const needle = evaluate(argNodes[0], scope);
|
|
1102
|
-
return target.some((entry) => valuesEqual(entry, needle));
|
|
1103
|
-
}
|
|
1104
|
-
if (name === 'containsAll') {
|
|
1105
|
-
return argNodes.every((node) => target.some((entry) => valuesEqual(entry, evaluate(node, scope))));
|
|
1106
|
-
}
|
|
1107
|
-
if (name === 'containsAny') {
|
|
1108
|
-
return argNodes.some((node) => target.some((entry) => valuesEqual(entry, evaluate(node, scope))));
|
|
1109
|
-
}
|
|
1110
|
-
if (name === 'filter') {
|
|
1111
|
-
const predicate = argNodes[0];
|
|
1112
|
-
return target.filter((value, index) => Boolean(evaluate(predicate, scope.child({ index, value }))));
|
|
1113
|
-
}
|
|
1114
|
-
if (name === 'flat') {
|
|
1115
|
-
return target.flat();
|
|
1116
|
-
}
|
|
1117
|
-
if (name === 'isEmpty') {
|
|
1118
|
-
return target.length === 0;
|
|
1119
|
-
}
|
|
1120
|
-
if (name === 'join') {
|
|
1121
|
-
return target.map((entry) => toDisplayText(entry)).join(String(evaluate(argNodes[0], scope) ?? ''));
|
|
1122
|
-
}
|
|
1123
|
-
if (name === 'map') {
|
|
1124
|
-
const mapper = argNodes[0];
|
|
1125
|
-
return target.map((value, index) => evaluate(mapper, scope.child({ index, value })));
|
|
1126
|
-
}
|
|
1127
|
-
if (name === 'reduce') {
|
|
1128
|
-
const reducer = argNodes[0];
|
|
1129
|
-
let accumulator = argNodes.length > 1 ? evaluate(argNodes[1], scope) : null;
|
|
1130
|
-
target.forEach((value, index) => {
|
|
1131
|
-
accumulator = evaluate(reducer, scope.child({ acc: accumulator, index, value }));
|
|
1132
|
-
});
|
|
1133
|
-
return accumulator;
|
|
1134
|
-
}
|
|
1135
|
-
if (name === 'reverse') {
|
|
1136
|
-
return [...target].reverse();
|
|
1137
|
-
}
|
|
1138
|
-
if (name === 'slice') {
|
|
1139
|
-
return target.slice(Number(evaluate(argNodes[0], scope) ?? 0), argNodes[1] ? Number(evaluate(argNodes[1], scope)) : undefined);
|
|
1140
|
-
}
|
|
1141
|
-
if (name === 'sort') {
|
|
1142
|
-
return [...target].sort(compareValues);
|
|
1143
|
-
}
|
|
1144
|
-
if (name === 'unique') {
|
|
1145
|
-
const uniqueValues = [];
|
|
1146
|
-
target.forEach((value) => {
|
|
1147
|
-
if (!uniqueValues.some((entry) => valuesEqual(entry, value))) {
|
|
1148
|
-
uniqueValues.push(value);
|
|
1149
|
-
}
|
|
1150
|
-
});
|
|
1151
|
-
return uniqueValues;
|
|
1152
|
-
}
|
|
51
|
+
set indexSnapshot(value) {
|
|
52
|
+
this.snapshotStore.indexSnapshot = value;
|
|
1153
53
|
}
|
|
1154
54
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
case 'contains':
|
|
1158
|
-
return target.toLowerCase().includes(String(evaluate(argNodes[0], scope) ?? '').toLowerCase());
|
|
1159
|
-
case 'endsWith':
|
|
1160
|
-
return target.endsWith(String(evaluate(argNodes[0], scope) ?? ''));
|
|
1161
|
-
case 'isEmpty':
|
|
1162
|
-
return target.length === 0;
|
|
1163
|
-
case 'lower':
|
|
1164
|
-
return target.toLowerCase();
|
|
1165
|
-
case 'replace':
|
|
1166
|
-
return target.replaceAll(String(evaluate(argNodes[0], scope) ?? ''), String(evaluate(argNodes[1], scope) ?? ''));
|
|
1167
|
-
case 'split':
|
|
1168
|
-
return target.split(String(evaluate(argNodes[0], scope) ?? ''));
|
|
1169
|
-
case 'startsWith':
|
|
1170
|
-
return target.startsWith(String(evaluate(argNodes[0], scope) ?? ''));
|
|
1171
|
-
case 'title':
|
|
1172
|
-
return target.replace(/\b\p{L}/gu, (match) => match.toUpperCase());
|
|
1173
|
-
case 'trim':
|
|
1174
|
-
return target.trim();
|
|
1175
|
-
case 'upper':
|
|
1176
|
-
return target.toUpperCase();
|
|
1177
|
-
default:
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
55
|
+
get lastWorkspaceState() {
|
|
56
|
+
return this.snapshotStore.lastWorkspaceState;
|
|
1180
57
|
}
|
|
1181
58
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
case 'ceil':
|
|
1185
|
-
return Math.ceil(target);
|
|
1186
|
-
case 'floor':
|
|
1187
|
-
return Math.floor(target);
|
|
1188
|
-
case 'isEmpty':
|
|
1189
|
-
return false;
|
|
1190
|
-
case 'round':
|
|
1191
|
-
return Number.isFinite(evaluate(argNodes[0], scope))
|
|
1192
|
-
? Number(target.toFixed(Number(evaluate(argNodes[0], scope))))
|
|
1193
|
-
: Math.round(target);
|
|
1194
|
-
case 'toFixed':
|
|
1195
|
-
return target.toFixed(Number(evaluate(argNodes[0], scope) ?? 0));
|
|
1196
|
-
default:
|
|
1197
|
-
break;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const asDate = parseDateValue(target);
|
|
1202
|
-
if (asDate) {
|
|
1203
|
-
switch (name) {
|
|
1204
|
-
case 'format':
|
|
1205
|
-
return formatDateValue(asDate, String(evaluate(argNodes[0], scope) ?? 'YYYY-MM-DD'));
|
|
1206
|
-
case 'isEmpty':
|
|
1207
|
-
return false;
|
|
1208
|
-
default:
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
if (target?.__baseType === 'file') {
|
|
1214
|
-
switch (name) {
|
|
1215
|
-
case 'asLink':
|
|
1216
|
-
return createLinkValue(target.path, {
|
|
1217
|
-
display: typeof evaluate(argNodes[0], scope) === 'string' ? evaluate(argNodes[0], scope) : '',
|
|
1218
|
-
exists: true,
|
|
1219
|
-
});
|
|
1220
|
-
case 'hasLink': {
|
|
1221
|
-
const needle = evaluate(argNodes[0], scope);
|
|
1222
|
-
const needlePath = needle?.__baseType === 'file'
|
|
1223
|
-
? needle.path
|
|
1224
|
-
: (needle?.__baseType === 'link' ? needle.path : String(needle ?? ''));
|
|
1225
|
-
return target.links.some((link) => link.path === needlePath || link.rawTarget === needlePath);
|
|
1226
|
-
}
|
|
1227
|
-
case 'hasProperty':
|
|
1228
|
-
return Object.hasOwn(target.properties, String(evaluate(argNodes[0], scope) ?? ''));
|
|
1229
|
-
case 'hasTag':
|
|
1230
|
-
return argNodes.some((node) => {
|
|
1231
|
-
const value = String(evaluate(node, scope) ?? '').replace(/^#/u, '');
|
|
1232
|
-
return target.tags.some((tag) => tag === value || tag.startsWith(`${value}/`));
|
|
1233
|
-
});
|
|
1234
|
-
case 'inFolder': {
|
|
1235
|
-
const folder = String(evaluate(argNodes[0], scope) ?? '').replace(/\/+$/u, '');
|
|
1236
|
-
return !folder || target.folder === folder || target.folder.startsWith(`${folder}/`);
|
|
1237
|
-
}
|
|
1238
|
-
default:
|
|
1239
|
-
break;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
if (target?.__baseType === 'link') {
|
|
1244
|
-
switch (name) {
|
|
1245
|
-
case 'asFile':
|
|
1246
|
-
return target.path ? rootContext.snapshot.rowsByPath.get(target.path)?.file ?? null : null;
|
|
1247
|
-
case 'linksTo': {
|
|
1248
|
-
const other = evaluate(argNodes[0], scope);
|
|
1249
|
-
const otherPath = other?.__baseType === 'file'
|
|
1250
|
-
? other.path
|
|
1251
|
-
: (other?.__baseType === 'link' ? other.path : String(other ?? ''));
|
|
1252
|
-
return target.path === otherPath;
|
|
1253
|
-
}
|
|
1254
|
-
default:
|
|
1255
|
-
break;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
if (isPlainObject(target)) {
|
|
1260
|
-
switch (name) {
|
|
1261
|
-
case 'isEmpty':
|
|
1262
|
-
return Object.keys(target).length === 0;
|
|
1263
|
-
case 'keys':
|
|
1264
|
-
return Object.keys(target);
|
|
1265
|
-
case 'values':
|
|
1266
|
-
return Object.values(target);
|
|
1267
|
-
default:
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
throw new Error(`Unsupported method "${name}"`);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
function evaluateAst(ast, scope) {
|
|
1276
|
-
const rootContext = scope.rootContext;
|
|
1277
|
-
const evaluate = (node, nextScope = scope) => evaluateAst(node, nextScope);
|
|
1278
|
-
|
|
1279
|
-
switch (ast?.type) {
|
|
1280
|
-
case 'ArrayExpression':
|
|
1281
|
-
return ast.elements.map((element) => evaluate(element, scope));
|
|
1282
|
-
case 'BinaryExpression':
|
|
1283
|
-
return applyBinaryOperator(ast.operator, evaluate(ast.left, scope), evaluate(ast.right, scope));
|
|
1284
|
-
case 'CallExpression': {
|
|
1285
|
-
if (ast.callee?.type === 'Identifier') {
|
|
1286
|
-
return invokeGlobalFunction(ast.callee.name, ast.arguments, scope, evaluate);
|
|
1287
|
-
}
|
|
1288
|
-
if (ast.callee?.type === 'MemberExpression') {
|
|
1289
|
-
const target = evaluate(ast.callee.object, scope);
|
|
1290
|
-
const property = ast.callee.computed
|
|
1291
|
-
? evaluate(ast.callee.property, scope)
|
|
1292
|
-
: ast.callee.property.name;
|
|
1293
|
-
return invokeMethod(target, String(property ?? ''), ast.arguments, scope, evaluate, rootContext);
|
|
1294
|
-
}
|
|
1295
|
-
throw new Error('Unsupported call target');
|
|
1296
|
-
}
|
|
1297
|
-
case 'Identifier':
|
|
1298
|
-
return scope.get(ast.name);
|
|
1299
|
-
case 'Literal':
|
|
1300
|
-
return ast.value;
|
|
1301
|
-
case 'MemberExpression': {
|
|
1302
|
-
const target = evaluate(ast.object, scope);
|
|
1303
|
-
const property = ast.computed ? evaluate(ast.property, scope) : ast.property.name;
|
|
1304
|
-
if (target?.__baseType === 'formula-namespace') {
|
|
1305
|
-
return rootContext.resolveFormulaValue(String(property ?? ''), target.row, target.evaluationState);
|
|
1306
|
-
}
|
|
1307
|
-
if (target == null) {
|
|
1308
|
-
return null;
|
|
1309
|
-
}
|
|
1310
|
-
return target[property];
|
|
1311
|
-
}
|
|
1312
|
-
case 'ObjectExpression':
|
|
1313
|
-
return ast.properties.reduce((acc, property) => {
|
|
1314
|
-
acc[property.key] = evaluate(property.value, scope);
|
|
1315
|
-
return acc;
|
|
1316
|
-
}, {});
|
|
1317
|
-
case 'UnaryExpression': {
|
|
1318
|
-
const value = evaluate(ast.argument, scope);
|
|
1319
|
-
return ast.operator === '!' ? !value : -Number(value ?? 0);
|
|
1320
|
-
}
|
|
1321
|
-
default:
|
|
1322
|
-
throw new Error(`Unsupported expression node "${ast?.type}"`);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
function evaluateExpression(expressionSource, rootContext, locals = {}) {
|
|
1327
|
-
const cache = rootContext.astCache;
|
|
1328
|
-
let ast = cache.get(expressionSource);
|
|
1329
|
-
if (!ast) {
|
|
1330
|
-
ast = parseExpression(expressionSource);
|
|
1331
|
-
cache.set(expressionSource, ast);
|
|
1332
|
-
}
|
|
1333
|
-
const scope = new ExpressionScope({ locals, rootContext });
|
|
1334
|
-
return evaluateAst(ast, scope);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
function evaluateFilterNode(filterNode, rootContext) {
|
|
1338
|
-
if (!filterNode) {
|
|
1339
|
-
return true;
|
|
1340
|
-
}
|
|
1341
|
-
if (typeof filterNode === 'string') {
|
|
1342
|
-
return Boolean(evaluateExpression(filterNode, rootContext));
|
|
1343
|
-
}
|
|
1344
|
-
if (!isPlainObject(filterNode)) {
|
|
1345
|
-
return true;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
if (Array.isArray(filterNode.and)) {
|
|
1349
|
-
return filterNode.and.every((entry) => evaluateFilterNode(entry, rootContext));
|
|
1350
|
-
}
|
|
1351
|
-
if (Array.isArray(filterNode.or)) {
|
|
1352
|
-
return filterNode.or.some((entry) => evaluateFilterNode(entry, rootContext));
|
|
1353
|
-
}
|
|
1354
|
-
if (filterNode.not != null) {
|
|
1355
|
-
return !evaluateFilterNode(filterNode.not, rootContext);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
return true;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function normalizeFormulaLookupName(name = '') {
|
|
1362
|
-
return String(name ?? '').replace(/^formula\./u, '');
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
function createEvaluationRootContext({
|
|
1366
|
-
astCache = new Map(),
|
|
1367
|
-
currentRow,
|
|
1368
|
-
definition,
|
|
1369
|
-
evaluationState = null,
|
|
1370
|
-
identifierResolver = null,
|
|
1371
|
-
snapshot,
|
|
1372
|
-
thisFile,
|
|
1373
|
-
}) {
|
|
1374
|
-
const nextEvaluationState = evaluationState ?? {
|
|
1375
|
-
cache: new Map(),
|
|
1376
|
-
stack: new Set(),
|
|
1377
|
-
};
|
|
1378
|
-
|
|
1379
|
-
return {
|
|
1380
|
-
astCache,
|
|
1381
|
-
currentRow,
|
|
1382
|
-
definition,
|
|
1383
|
-
evaluationState: nextEvaluationState,
|
|
1384
|
-
resolveFormulaValue: (name, row, state = nextEvaluationState) => {
|
|
1385
|
-
const formulaName = normalizeFormulaLookupName(name);
|
|
1386
|
-
const formulaPropertyId = Object.keys(definition.properties).find((propertyId) => {
|
|
1387
|
-
const config = definition.properties[propertyId];
|
|
1388
|
-
return config?.formula && normalizeFormulaLookupName(propertyId) === formulaName;
|
|
1389
|
-
});
|
|
1390
|
-
if (!formulaPropertyId) {
|
|
1391
|
-
return null;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
const cacheKey = `${row.file.path}::${formulaPropertyId}`;
|
|
1395
|
-
if (state.cache.has(cacheKey)) {
|
|
1396
|
-
return state.cache.get(cacheKey);
|
|
1397
|
-
}
|
|
1398
|
-
if (state.stack.has(cacheKey)) {
|
|
1399
|
-
throw new Error(`Circular formula reference: ${formulaPropertyId}`);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
state.stack.add(cacheKey);
|
|
1403
|
-
const nextContext = createEvaluationRootContext({
|
|
1404
|
-
astCache,
|
|
1405
|
-
currentRow: row,
|
|
1406
|
-
definition,
|
|
1407
|
-
evaluationState: state,
|
|
1408
|
-
snapshot,
|
|
1409
|
-
thisFile,
|
|
1410
|
-
});
|
|
1411
|
-
const result = evaluateExpression(definition.properties[formulaPropertyId].formula, nextContext);
|
|
1412
|
-
state.stack.delete(cacheKey);
|
|
1413
|
-
state.cache.set(cacheKey, result);
|
|
1414
|
-
return result;
|
|
1415
|
-
},
|
|
1416
|
-
resolveIdentifier: (name) => {
|
|
1417
|
-
if (typeof identifierResolver === 'function') {
|
|
1418
|
-
const resolved = identifierResolver(name);
|
|
1419
|
-
if (resolved !== undefined) {
|
|
1420
|
-
return resolved;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
if (name === 'file') {
|
|
1424
|
-
return currentRow.file;
|
|
1425
|
-
}
|
|
1426
|
-
if (name === 'formula') {
|
|
1427
|
-
return createFormulaNamespace(currentRow, nextEvaluationState);
|
|
1428
|
-
}
|
|
1429
|
-
if (name === 'note') {
|
|
1430
|
-
return currentRow.noteProperties;
|
|
1431
|
-
}
|
|
1432
|
-
if (name === 'this') {
|
|
1433
|
-
return createContextFileValue(thisFile);
|
|
1434
|
-
}
|
|
1435
|
-
if (name === 'values') {
|
|
1436
|
-
return [];
|
|
1437
|
-
}
|
|
1438
|
-
if (Object.hasOwn(currentRow.noteProperties, name)) {
|
|
1439
|
-
return currentRow.noteProperties[name];
|
|
1440
|
-
}
|
|
1441
|
-
return null;
|
|
1442
|
-
},
|
|
1443
|
-
snapshot,
|
|
1444
|
-
thisFile,
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
function resolvePropertyLabel(propertyId, definition) {
|
|
1449
|
-
const config = definition.properties[propertyId];
|
|
1450
|
-
if (config?.displayName) {
|
|
1451
|
-
return config.displayName;
|
|
1452
|
-
}
|
|
1453
|
-
return propertyId.startsWith('file.')
|
|
1454
|
-
? propertyId.slice(5)
|
|
1455
|
-
: propertyId.replace(/^note\./u, '');
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
function getPropertyValue(propertyId, row, definition, snapshot, thisFile) {
|
|
1459
|
-
if (propertyId.startsWith('file.')) {
|
|
1460
|
-
return propertyId.split('.').slice(1).reduce((acc, segment) => acc?.[segment], row.file);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
if (propertyId.startsWith('note.')) {
|
|
1464
|
-
return row.noteProperties[propertyId.slice(5)] ?? null;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
const propertyConfig = definition.properties[propertyId];
|
|
1468
|
-
if (propertyConfig?.formula) {
|
|
1469
|
-
const context = createEvaluationRootContext({
|
|
1470
|
-
currentRow: row,
|
|
1471
|
-
definition,
|
|
1472
|
-
snapshot,
|
|
1473
|
-
thisFile,
|
|
1474
|
-
});
|
|
1475
|
-
return context.resolveFormulaValue(propertyId, row, context.evaluationState);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
const formulaConfig = definition.properties[`formula.${propertyId}`];
|
|
1479
|
-
if (formulaConfig?.formula) {
|
|
1480
|
-
const context = createEvaluationRootContext({
|
|
1481
|
-
currentRow: row,
|
|
1482
|
-
definition,
|
|
1483
|
-
snapshot,
|
|
1484
|
-
thisFile,
|
|
1485
|
-
});
|
|
1486
|
-
return context.resolveFormulaValue(propertyId, row, context.evaluationState);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
return row.noteProperties[propertyId] ?? null;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
function buildColumns(definition, view) {
|
|
1493
|
-
const order = view.order.length > 0
|
|
1494
|
-
? view.order
|
|
1495
|
-
: ['file.name', ...Object.keys(definition.properties)];
|
|
1496
|
-
return order.map((propertyId) => ({
|
|
1497
|
-
id: propertyId,
|
|
1498
|
-
label: resolvePropertyLabel(propertyId, definition),
|
|
1499
|
-
}));
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function collectEvaluatedPropertyIds(columns, view) {
|
|
1503
|
-
const propertyIds = new Set(columns.map((column) => column.id));
|
|
1504
|
-
|
|
1505
|
-
if (view.groupBy) {
|
|
1506
|
-
propertyIds.add(view.groupBy);
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
view.sort.forEach((sortConfig) => {
|
|
1510
|
-
if (sortConfig?.property) {
|
|
1511
|
-
propertyIds.add(sortConfig.property);
|
|
1512
|
-
}
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
view.summaries.forEach((summary) => {
|
|
1516
|
-
if (summary?.property) {
|
|
1517
|
-
propertyIds.add(summary.property);
|
|
1518
|
-
}
|
|
1519
|
-
});
|
|
1520
|
-
|
|
1521
|
-
return [...propertyIds];
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
function summarizeValues(values = [], summary, definition, snapshot, thisFile) {
|
|
1525
|
-
const meaningfulValues = values.filter((value) => value != null && value !== '');
|
|
1526
|
-
switch (summary.type) {
|
|
1527
|
-
case 'average': {
|
|
1528
|
-
const numbers = meaningfulValues.map(Number).filter(Number.isFinite);
|
|
1529
|
-
return numbers.length > 0 ? numbers.reduce((sum, value) => sum + value, 0) / numbers.length : null;
|
|
1530
|
-
}
|
|
1531
|
-
case 'checked':
|
|
1532
|
-
return values.filter(Boolean).length;
|
|
1533
|
-
case 'custom': {
|
|
1534
|
-
const context = createEvaluationRootContext({
|
|
1535
|
-
astCache: new Map(),
|
|
1536
|
-
currentRow: {
|
|
1537
|
-
file: thisFile,
|
|
1538
|
-
noteProperties: thisFile?.properties ?? {},
|
|
1539
|
-
},
|
|
1540
|
-
definition,
|
|
1541
|
-
identifierResolver: (name) => (name === 'values' ? values : undefined),
|
|
1542
|
-
snapshot,
|
|
1543
|
-
thisFile,
|
|
1544
|
-
});
|
|
1545
|
-
return evaluateExpression(summary.formula, context);
|
|
1546
|
-
}
|
|
1547
|
-
case 'earliest':
|
|
1548
|
-
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) < 0 ? value : acc), null);
|
|
1549
|
-
case 'empty':
|
|
1550
|
-
return values.filter((value) => value == null || value === '').length;
|
|
1551
|
-
case 'filled':
|
|
1552
|
-
return meaningfulValues.length;
|
|
1553
|
-
case 'latest':
|
|
1554
|
-
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) > 0 ? value : acc), null);
|
|
1555
|
-
case 'max':
|
|
1556
|
-
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) > 0 ? value : acc), null);
|
|
1557
|
-
case 'median': {
|
|
1558
|
-
const numbers = meaningfulValues.map(Number).filter(Number.isFinite).sort((left, right) => left - right);
|
|
1559
|
-
if (numbers.length === 0) return null;
|
|
1560
|
-
const middle = Math.floor(numbers.length / 2);
|
|
1561
|
-
return numbers.length % 2 === 0 ? (numbers[middle - 1] + numbers[middle]) / 2 : numbers[middle];
|
|
1562
|
-
}
|
|
1563
|
-
case 'min':
|
|
1564
|
-
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) < 0 ? value : acc), null);
|
|
1565
|
-
case 'range': {
|
|
1566
|
-
const sorted = meaningfulValues.slice().sort(compareValues);
|
|
1567
|
-
if (sorted.length === 0) return null;
|
|
1568
|
-
const first = sorted[0];
|
|
1569
|
-
const last = sorted[sorted.length - 1];
|
|
1570
|
-
if (first instanceof Date && last instanceof Date) {
|
|
1571
|
-
return last.getTime() - first.getTime();
|
|
1572
|
-
}
|
|
1573
|
-
return Number(last ?? 0) - Number(first ?? 0);
|
|
1574
|
-
}
|
|
1575
|
-
case 'stddev': {
|
|
1576
|
-
const numbers = meaningfulValues.map(Number).filter(Number.isFinite);
|
|
1577
|
-
if (numbers.length === 0) return null;
|
|
1578
|
-
const average = numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
1579
|
-
const variance = numbers.reduce((sum, value) => sum + ((value - average) ** 2), 0) / numbers.length;
|
|
1580
|
-
return Math.sqrt(variance);
|
|
1581
|
-
}
|
|
1582
|
-
case 'sum':
|
|
1583
|
-
return meaningfulValues.map(Number).filter(Number.isFinite).reduce((sum, value) => sum + value, 0);
|
|
1584
|
-
case 'unchecked':
|
|
1585
|
-
return values.filter((value) => value === false || value == null).length;
|
|
1586
|
-
case 'unique': {
|
|
1587
|
-
const uniqueValues = [];
|
|
1588
|
-
meaningfulValues.forEach((value) => {
|
|
1589
|
-
if (!uniqueValues.some((entry) => valuesEqual(entry, value))) {
|
|
1590
|
-
uniqueValues.push(value);
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
|
-
return uniqueValues.length;
|
|
1594
|
-
}
|
|
1595
|
-
default:
|
|
1596
|
-
return null;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
function createSummaryPayload(columns, rows, summaries, definition, snapshot, thisFile) {
|
|
1601
|
-
return summaries.map((summary) => {
|
|
1602
|
-
const column = columns.find((entry) => entry.id === summary.property);
|
|
1603
|
-
const values = rows.map((row) => row.rawCells[summary.property]);
|
|
1604
|
-
const value = summarizeValues(values, summary, definition, snapshot, thisFile);
|
|
1605
|
-
return {
|
|
1606
|
-
id: summary.id,
|
|
1607
|
-
label: summary.label,
|
|
1608
|
-
property: summary.property,
|
|
1609
|
-
propertyLabel: column?.label ?? summary.property,
|
|
1610
|
-
value: serializeBaseValue(value, {
|
|
1611
|
-
snapshot,
|
|
1612
|
-
sourcePath: thisFile?.path ?? '',
|
|
1613
|
-
}),
|
|
1614
|
-
};
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
function rowMatchesSearch(row, columns, query) {
|
|
1619
|
-
const normalizedQuery = String(query ?? '').trim().toLowerCase();
|
|
1620
|
-
if (!normalizedQuery) {
|
|
1621
|
-
return true;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
const haystack = [
|
|
1625
|
-
row.file.path,
|
|
1626
|
-
...columns.map((column) => toDisplayText(row.rawCells[column.id])),
|
|
1627
|
-
].join('\n').toLowerCase();
|
|
1628
|
-
return haystack.includes(normalizedQuery);
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
function createCsv(rows, columns) {
|
|
1632
|
-
const escapeCell = (value) => {
|
|
1633
|
-
const text = String(value ?? '');
|
|
1634
|
-
if (/[,"\n]/u.test(text)) {
|
|
1635
|
-
return `"${text.replace(/"/g, '""')}"`;
|
|
1636
|
-
}
|
|
1637
|
-
return text;
|
|
1638
|
-
};
|
|
1639
|
-
|
|
1640
|
-
const lines = [
|
|
1641
|
-
columns.map((column) => escapeCell(column.label)).join(','),
|
|
1642
|
-
...rows.map((row) => columns.map((column) => escapeCell(toDisplayText(row.rawCells[column.id]))).join(',')),
|
|
1643
|
-
];
|
|
1644
|
-
return `${lines.join('\n')}\n`;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
export function serializeBaseDefinition(definition) {
|
|
1648
|
-
const raw = definition?.raw ?? definition ?? {};
|
|
1649
|
-
return `${yaml.dump(raw, { lineWidth: -1, noRefs: true }).trim()}\n`;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
export class BaseQueryService {
|
|
1653
|
-
constructor({
|
|
1654
|
-
vaultFileStore,
|
|
1655
|
-
workspaceStateProvider = null,
|
|
1656
|
-
workspaceStateSynchronizer = null,
|
|
1657
|
-
}) {
|
|
1658
|
-
this.vaultFileStore = vaultFileStore;
|
|
1659
|
-
this.workspaceStateProvider = workspaceStateProvider;
|
|
1660
|
-
this.workspaceStateSynchronizer = workspaceStateSynchronizer;
|
|
1661
|
-
this.indexSnapshot = null;
|
|
1662
|
-
this.lastWorkspaceState = null;
|
|
59
|
+
set lastWorkspaceState(value) {
|
|
60
|
+
this.snapshotStore.lastWorkspaceState = value;
|
|
1663
61
|
}
|
|
1664
62
|
|
|
1665
63
|
async getWorkspaceState() {
|
|
1666
|
-
|
|
1667
|
-
if (workspaceState) {
|
|
1668
|
-
this.lastWorkspaceState = workspaceState;
|
|
1669
|
-
return workspaceState;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (this.lastWorkspaceState) {
|
|
1673
|
-
return this.lastWorkspaceState;
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
const scannedWorkspaceState = await this.vaultFileStore.scanWorkspaceState();
|
|
1677
|
-
this.lastWorkspaceState = scannedWorkspaceState;
|
|
1678
|
-
return scannedWorkspaceState;
|
|
64
|
+
return this.snapshotStore.getWorkspaceState();
|
|
1679
65
|
}
|
|
1680
66
|
|
|
1681
|
-
createSnapshotRow(
|
|
1682
|
-
|
|
1683
|
-
const frontmatter = markdownContent ? extractYamlFrontmatter(markdownContent) : null;
|
|
1684
|
-
const noteProperties = isPlainObject(frontmatter?.data) ? frontmatter.data : {};
|
|
1685
|
-
const bodyMarkdown = frontmatter?.bodyMarkdown ?? markdownContent ?? '';
|
|
1686
|
-
const references = extractReferences(bodyMarkdown, wikiTargetIndex);
|
|
1687
|
-
const tags = [...new Set([
|
|
1688
|
-
...normalizeTags(noteProperties.tags),
|
|
1689
|
-
...extractInlineTags(bodyMarkdown),
|
|
1690
|
-
])];
|
|
1691
|
-
const fileValue = {
|
|
1692
|
-
__baseType: 'file',
|
|
1693
|
-
backlinks: [],
|
|
1694
|
-
basename: stripVaultFileExtension(basename(filePath)),
|
|
1695
|
-
ctime: Number.isFinite(metadata?.ctimeMs) ? new Date(metadata.ctimeMs) : null,
|
|
1696
|
-
embeds: references.embeds,
|
|
1697
|
-
ext: extname(filePath).replace(/^\./u, ''),
|
|
1698
|
-
folder: dirname(filePath) === '.' ? '' : dirname(filePath).replace(/\\/g, '/'),
|
|
1699
|
-
links: references.links,
|
|
1700
|
-
mtime: Number.isFinite(metadata?.mtimeMs) ? new Date(metadata.mtimeMs) : null,
|
|
1701
|
-
name: basename(filePath),
|
|
1702
|
-
path: filePath,
|
|
1703
|
-
properties: noteProperties,
|
|
1704
|
-
size: Number(metadata?.size ?? 0) || 0,
|
|
1705
|
-
tags,
|
|
1706
|
-
};
|
|
1707
|
-
|
|
1708
|
-
return {
|
|
1709
|
-
forwardLinks: new Set(
|
|
1710
|
-
references.links
|
|
1711
|
-
.map((link) => link?.path)
|
|
1712
|
-
.filter((targetPath) => targetPath && targetPath !== filePath),
|
|
1713
|
-
),
|
|
1714
|
-
row: {
|
|
1715
|
-
file: fileValue,
|
|
1716
|
-
noteProperties,
|
|
1717
|
-
path: filePath,
|
|
1718
|
-
},
|
|
1719
|
-
};
|
|
67
|
+
createSnapshotRow(...args) {
|
|
68
|
+
return this.snapshotStore.createSnapshotRow(...args);
|
|
1720
69
|
}
|
|
1721
70
|
|
|
1722
|
-
rebuildBacklinks(
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
1726
|
-
const forwardLinks = snapshot.forwardLinksByPath.get(filePath) ?? new Set();
|
|
1727
|
-
forwardLinks.forEach((targetPath) => {
|
|
1728
|
-
if (!backlinksByTarget.has(targetPath) || targetPath === row.file.path) {
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
backlinksByTarget.get(targetPath).push(createLinkValue(row.file.path, {
|
|
1733
|
-
display: basename(row.file.path),
|
|
1734
|
-
exists: true,
|
|
1735
|
-
}));
|
|
1736
|
-
});
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
snapshot.backlinksByPath = backlinksByTarget;
|
|
1740
|
-
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
1741
|
-
row.file.backlinks = dedupeLinkValues(backlinksByTarget.get(filePath) ?? []);
|
|
1742
|
-
});
|
|
71
|
+
rebuildBacklinks(...args) {
|
|
72
|
+
return this.snapshotStore.rebuildBacklinks(...args);
|
|
1743
73
|
}
|
|
1744
74
|
|
|
1745
|
-
async buildIndexSnapshot(
|
|
1746
|
-
|
|
1747
|
-
const fileEntries = listWorkspaceFilePaths(resolvedWorkspaceState);
|
|
1748
|
-
const wikiTargetIndex = createWikiTargetIndex(fileEntries);
|
|
1749
|
-
const markdownContents = new Map();
|
|
1750
|
-
|
|
1751
|
-
await mapWithConcurrency(resolvedWorkspaceState.markdownPaths ?? [], INDEX_READ_CONCURRENCY, async (filePath) => {
|
|
1752
|
-
markdownContents.set(filePath, await this.vaultFileStore.readMarkdownFile(filePath));
|
|
1753
|
-
});
|
|
1754
|
-
|
|
1755
|
-
const rowsByPath = new Map();
|
|
1756
|
-
const forwardLinksByPath = new Map();
|
|
1757
|
-
fileEntries.forEach((filePath) => {
|
|
1758
|
-
const markdownContent = markdownContents.get(filePath) ?? null;
|
|
1759
|
-
const rowRecord = this.createSnapshotRow(filePath, resolvedWorkspaceState, wikiTargetIndex, markdownContent);
|
|
1760
|
-
rowsByPath.set(filePath, rowRecord.row);
|
|
1761
|
-
forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
|
|
1762
|
-
});
|
|
1763
|
-
|
|
1764
|
-
const snapshot = {
|
|
1765
|
-
backlinksByPath: new Map(),
|
|
1766
|
-
filePaths: fileEntries,
|
|
1767
|
-
forwardLinksByPath,
|
|
1768
|
-
rowsByPath,
|
|
1769
|
-
scannedAt: resolvedWorkspaceState.scannedAt,
|
|
1770
|
-
wikiTargetIndex,
|
|
1771
|
-
workspaceState: resolvedWorkspaceState,
|
|
1772
|
-
};
|
|
1773
|
-
this.rebuildBacklinks(snapshot);
|
|
1774
|
-
this.indexSnapshot = snapshot;
|
|
1775
|
-
this.lastWorkspaceState = resolvedWorkspaceState;
|
|
1776
|
-
return snapshot;
|
|
75
|
+
async buildIndexSnapshot(...args) {
|
|
76
|
+
return this.snapshotStore.buildIndexSnapshot(...args);
|
|
1777
77
|
}
|
|
1778
78
|
|
|
1779
79
|
async synchronizeWorkspaceState() {
|
|
1780
|
-
|
|
80
|
+
return this.snapshotStore.synchronizeWorkspaceState();
|
|
1781
81
|
}
|
|
1782
82
|
|
|
1783
|
-
removeSnapshotPath(
|
|
1784
|
-
|
|
1785
|
-
snapshot.forwardLinksByPath.delete(filePath);
|
|
1786
|
-
snapshot.backlinksByPath.delete(filePath);
|
|
1787
|
-
snapshot.filePaths = snapshot.filePaths.filter((candidatePath) => candidatePath !== filePath);
|
|
83
|
+
removeSnapshotPath(...args) {
|
|
84
|
+
return this.snapshotStore.removeSnapshotPath(...args);
|
|
1788
85
|
}
|
|
1789
86
|
|
|
1790
|
-
upsertSnapshotPath(
|
|
1791
|
-
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
snapshot.filePaths.push(filePath);
|
|
1796
|
-
snapshot.filePaths.sort(compareVaultPaths);
|
|
87
|
+
upsertSnapshotPath(...args) {
|
|
88
|
+
return this.snapshotStore.upsertSnapshotPath(...args);
|
|
1797
89
|
}
|
|
1798
90
|
|
|
1799
|
-
collectImpactedSourcesForMembershipChanges(
|
|
1800
|
-
|
|
1801
|
-
pathValues.forEach((pathValue) => {
|
|
1802
|
-
collectWikiTargetKeysForFilePath(pathValue).forEach((targetKey) => {
|
|
1803
|
-
affectedTargetKeys.add(targetKey);
|
|
1804
|
-
});
|
|
1805
|
-
});
|
|
1806
|
-
|
|
1807
|
-
if (affectedTargetKeys.size === 0) {
|
|
1808
|
-
return new Set();
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
const impactedPaths = new Set();
|
|
1812
|
-
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
1813
|
-
const links = row?.file?.links ?? [];
|
|
1814
|
-
if (links.some((link) => affectedTargetKeys.has(normalizeWikiTargetKey(link?.rawTarget)))) {
|
|
1815
|
-
impactedPaths.add(filePath);
|
|
1816
|
-
}
|
|
1817
|
-
});
|
|
1818
|
-
|
|
1819
|
-
return impactedPaths;
|
|
91
|
+
collectImpactedSourcesForMembershipChanges(...args) {
|
|
92
|
+
return this.snapshotStore.collectImpactedSourcesForMembershipChanges(...args);
|
|
1820
93
|
}
|
|
1821
94
|
|
|
1822
|
-
async refreshSnapshotRows(
|
|
1823
|
-
|
|
1824
|
-
.filter((pathValue) => {
|
|
1825
|
-
const entry = workspaceState?.entries?.get(pathValue);
|
|
1826
|
-
return pathValue && isWorkspaceFileEntry(entry);
|
|
1827
|
-
});
|
|
1828
|
-
|
|
1829
|
-
await mapWithConcurrency(filePathsToRefresh, INDEX_READ_CONCURRENCY, async (filePath) => {
|
|
1830
|
-
const markdownContent = isMarkdownFilePath(filePath)
|
|
1831
|
-
? await this.vaultFileStore.readMarkdownFile(filePath)
|
|
1832
|
-
: null;
|
|
1833
|
-
const rowRecord = this.createSnapshotRow(filePath, workspaceState, snapshot.wikiTargetIndex, markdownContent);
|
|
1834
|
-
snapshot.rowsByPath.set(filePath, rowRecord.row);
|
|
1835
|
-
snapshot.forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
|
|
1836
|
-
});
|
|
95
|
+
async refreshSnapshotRows(...args) {
|
|
96
|
+
return this.snapshotStore.refreshSnapshotRows(...args);
|
|
1837
97
|
}
|
|
1838
98
|
|
|
1839
|
-
async ensureIndexSnapshot(
|
|
1840
|
-
|
|
1841
|
-
let workspaceState = await this.getWorkspaceState();
|
|
1842
|
-
const requiresFreshScan = [basePath, sourcePath]
|
|
1843
|
-
.filter(Boolean)
|
|
1844
|
-
.some((pathValue) => !workspaceState?.entries?.has?.(pathValue));
|
|
1845
|
-
if (requiresFreshScan) {
|
|
1846
|
-
workspaceState = await this.vaultFileStore.scanWorkspaceState();
|
|
1847
|
-
this.lastWorkspaceState = workspaceState;
|
|
1848
|
-
if (this.indexSnapshot?.scannedAt !== workspaceState?.scannedAt) {
|
|
1849
|
-
this.indexSnapshot = null;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
if (this.indexSnapshot?.scannedAt === workspaceState?.scannedAt) {
|
|
1854
|
-
return this.indexSnapshot;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
return this.buildIndexSnapshot(workspaceState);
|
|
99
|
+
async ensureIndexSnapshot(...args) {
|
|
100
|
+
return this.snapshotStore.ensureIndexSnapshot(...args);
|
|
1858
101
|
}
|
|
1859
102
|
|
|
1860
|
-
async initializeFromWorkspaceState(
|
|
1861
|
-
this.
|
|
1862
|
-
if (this.indexSnapshot && workspaceState && this.indexSnapshot.scannedAt !== workspaceState.scannedAt) {
|
|
1863
|
-
this.indexSnapshot = null;
|
|
1864
|
-
}
|
|
1865
|
-
return this.indexSnapshot;
|
|
103
|
+
async initializeFromWorkspaceState(...args) {
|
|
104
|
+
return this.snapshotStore.initializeFromWorkspaceState(...args);
|
|
1866
105
|
}
|
|
1867
106
|
|
|
1868
|
-
async applyWorkspaceChange(
|
|
1869
|
-
|
|
1870
|
-
nextState = null,
|
|
1871
|
-
} = {}) {
|
|
1872
|
-
this.lastWorkspaceState = nextState ?? this.lastWorkspaceState;
|
|
1873
|
-
if (!this.indexSnapshot) {
|
|
1874
|
-
return null;
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
if (!previousState || this.indexSnapshot.scannedAt !== previousState.scannedAt) {
|
|
1878
|
-
this.indexSnapshot = null;
|
|
1879
|
-
return null;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
const changedPaths = Array.from(new Set(workspaceChange.changedPaths ?? []));
|
|
1883
|
-
const deletedFilePaths = (workspaceChange.deletedPaths ?? []).filter((pathValue) => (
|
|
1884
|
-
isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
1885
|
-
));
|
|
1886
|
-
const renamedFileEntries = (workspaceChange.renamedPaths ?? []).filter((entry) => (
|
|
1887
|
-
isWorkspaceFileEntry(previousState?.entries?.get(entry?.oldPath))
|
|
1888
|
-
|| isWorkspaceFileEntry(nextState?.entries?.get(entry?.newPath))
|
|
1889
|
-
));
|
|
1890
|
-
const createdFilePaths = changedPaths.filter((pathValue) => (
|
|
1891
|
-
isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
|
|
1892
|
-
&& !isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
1893
|
-
));
|
|
1894
|
-
const removedChangedFilePaths = changedPaths.filter((pathValue) => (
|
|
1895
|
-
!isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
|
|
1896
|
-
&& isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
1897
|
-
));
|
|
1898
|
-
const membershipChanged = deletedFilePaths.length > 0
|
|
1899
|
-
|| renamedFileEntries.length > 0
|
|
1900
|
-
|| createdFilePaths.length > 0
|
|
1901
|
-
|| removedChangedFilePaths.length > 0;
|
|
1902
|
-
|
|
1903
|
-
const markdownPathsToRefresh = changedPaths.filter((pathValue) => (
|
|
1904
|
-
isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
1905
|
-
&& isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
|
|
1906
|
-
));
|
|
1907
|
-
|
|
1908
|
-
if (!membershipChanged && markdownPathsToRefresh.length === 0) {
|
|
1909
|
-
this.indexSnapshot.workspaceState = nextState ?? this.indexSnapshot.workspaceState;
|
|
1910
|
-
this.indexSnapshot.scannedAt = nextState?.scannedAt ?? this.indexSnapshot.scannedAt;
|
|
1911
|
-
return this.indexSnapshot;
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
const snapshot = this.indexSnapshot;
|
|
1915
|
-
if (membershipChanged) {
|
|
1916
|
-
const membershipAffectedPaths = [
|
|
1917
|
-
...deletedFilePaths,
|
|
1918
|
-
...removedChangedFilePaths,
|
|
1919
|
-
...createdFilePaths,
|
|
1920
|
-
...renamedFileEntries.flatMap((entry) => [entry.oldPath, entry.newPath]),
|
|
1921
|
-
];
|
|
1922
|
-
const impactedSourcePaths = this.collectImpactedSourcesForMembershipChanges(snapshot, membershipAffectedPaths);
|
|
1923
|
-
|
|
1924
|
-
[...deletedFilePaths, ...removedChangedFilePaths].forEach((pathValue) => {
|
|
1925
|
-
this.removeSnapshotPath(snapshot, pathValue);
|
|
1926
|
-
});
|
|
1927
|
-
renamedFileEntries.forEach((entry) => {
|
|
1928
|
-
this.removeSnapshotPath(snapshot, entry.oldPath);
|
|
1929
|
-
});
|
|
1930
|
-
[...createdFilePaths, ...renamedFileEntries.map((entry) => entry.newPath)].forEach((pathValue) => {
|
|
1931
|
-
const nextEntry = nextState?.entries?.get(pathValue);
|
|
1932
|
-
if (isWorkspaceFileEntry(nextEntry)) {
|
|
1933
|
-
this.upsertSnapshotPath(snapshot, pathValue);
|
|
1934
|
-
}
|
|
1935
|
-
});
|
|
1936
|
-
|
|
1937
|
-
snapshot.wikiTargetIndex = createWikiTargetIndex(snapshot.filePaths);
|
|
1938
|
-
await this.refreshSnapshotRows(snapshot, nextState, [
|
|
1939
|
-
...markdownPathsToRefresh,
|
|
1940
|
-
...createdFilePaths,
|
|
1941
|
-
...renamedFileEntries.map((entry) => entry.newPath),
|
|
1942
|
-
...impactedSourcePaths,
|
|
1943
|
-
]);
|
|
1944
|
-
} else {
|
|
1945
|
-
await this.refreshSnapshotRows(snapshot, nextState, markdownPathsToRefresh);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
snapshot.workspaceState = nextState;
|
|
1949
|
-
snapshot.scannedAt = nextState?.scannedAt ?? snapshot.scannedAt;
|
|
1950
|
-
this.rebuildBacklinks(snapshot);
|
|
1951
|
-
return snapshot;
|
|
107
|
+
async applyWorkspaceChange(...args) {
|
|
108
|
+
return this.snapshotStore.applyWorkspaceChange(...args);
|
|
1952
109
|
}
|
|
1953
110
|
|
|
1954
111
|
async query({
|
|
@@ -2021,71 +178,17 @@ export class BaseQueryService {
|
|
|
2021
178
|
|
|
2022
179
|
rows = rows.filter((row) => rowMatchesSearch(row, columns, search));
|
|
2023
180
|
|
|
2024
|
-
const groupsByKey = new Map();
|
|
2025
|
-
if (activeView.groupBy) {
|
|
2026
|
-
rows.forEach((row) => {
|
|
2027
|
-
const groupValue = row.rawCells[activeView.groupBy];
|
|
2028
|
-
const groupKey = toDisplayText(groupValue) || 'Empty';
|
|
2029
|
-
const entry = groupsByKey.get(groupKey) ?? {
|
|
2030
|
-
key: groupKey,
|
|
2031
|
-
label: groupKey,
|
|
2032
|
-
rows: [],
|
|
2033
|
-
value: serializeBaseValue(groupValue, {
|
|
2034
|
-
rowFilePath: row.file.path,
|
|
2035
|
-
snapshot,
|
|
2036
|
-
sourcePath: thisFile?.path ?? '',
|
|
2037
|
-
}),
|
|
2038
|
-
};
|
|
2039
|
-
entry.rows.push(row);
|
|
2040
|
-
groupsByKey.set(groupKey, entry);
|
|
2041
|
-
});
|
|
2042
|
-
} else {
|
|
2043
|
-
groupsByKey.set('All', {
|
|
2044
|
-
key: 'All',
|
|
2045
|
-
label: 'All',
|
|
2046
|
-
rows,
|
|
2047
|
-
value: serializeBaseValue(null),
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
const serializedRows = rows.map((row) => ({
|
|
2052
|
-
cells: columns.reduce((acc, column) => {
|
|
2053
|
-
acc[column.id] = serializeBaseValue(row.rawCells[column.id], {
|
|
2054
|
-
rowFilePath: row.file.path,
|
|
2055
|
-
snapshot,
|
|
2056
|
-
sourcePath: thisFile?.path ?? '',
|
|
2057
|
-
});
|
|
2058
|
-
return acc;
|
|
2059
|
-
}, {}),
|
|
2060
|
-
file: serializeBaseValue(row.file, {
|
|
2061
|
-
rowFilePath: row.file.path,
|
|
2062
|
-
snapshot,
|
|
2063
|
-
sourcePath: thisFile?.path ?? '',
|
|
2064
|
-
}),
|
|
2065
|
-
path: row.file.path,
|
|
2066
|
-
}));
|
|
2067
|
-
const rowsByPath = new Map(serializedRows.map((row) => [row.path, row]));
|
|
2068
|
-
|
|
2069
|
-
const groups = Array.from(groupsByKey.values()).map((group) => ({
|
|
2070
|
-
key: group.key,
|
|
2071
|
-
label: group.label,
|
|
2072
|
-
summaries: createSummaryPayload(columns, group.rows, activeView.summaries, definition, snapshot, thisFile),
|
|
2073
|
-
value: group.value,
|
|
2074
|
-
rows: group.rows.map((row) => rowsByPath.get(row.file.path)),
|
|
2075
|
-
}));
|
|
2076
|
-
|
|
2077
181
|
return {
|
|
2078
182
|
columns,
|
|
2079
|
-
csv: createCsv(rows, columns),
|
|
2080
183
|
definition,
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
184
|
+
...buildQueryResultPayload({
|
|
185
|
+
activeView,
|
|
186
|
+
columns,
|
|
187
|
+
definition,
|
|
188
|
+
rows,
|
|
2085
189
|
snapshot,
|
|
2086
|
-
|
|
190
|
+
thisFile,
|
|
2087
191
|
}),
|
|
2088
|
-
totalRows: rows.length,
|
|
2089
192
|
view: {
|
|
2090
193
|
...activeView,
|
|
2091
194
|
supported: activeView.supported,
|