collabmd 0.1.31 → 0.1.33
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-BMTwP5mD.js} +1 -1
- package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
- 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-DeowJOSp.js +1368 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/assets/vault-api-client-p7h7cXKM.js +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 +8 -5
- 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 +182 -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 +6 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -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 +115 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +1413 -34
- package/src/client/presentation/comments-panel.js +50 -5
- 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/components/scrollbars.css +5 -0
- 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 +559 -39
- 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/backlink-index.js +177 -5
- package/src/server/domain/bases/base-definition.js +382 -0
- package/src/server/domain/bases/base-expression-runtime.js +1196 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +215 -0
- package/src/server/domain/bases/base-query-service.js +406 -1992
- package/src/server/domain/bases/base-transform.js +116 -0
- 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 +161 -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-Zfwq1XVx.js} +0 -0
|
@@ -1,1652 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|
|
682
|
-
|
|
683
|
-
class ExpressionScope {
|
|
684
|
-
constructor({ locals = {}, parent = null, rootContext }) {
|
|
685
|
-
this.locals = locals;
|
|
686
|
-
this.parent = parent;
|
|
687
|
-
this.rootContext = rootContext;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
child(locals = {}) {
|
|
691
|
-
return new ExpressionScope({
|
|
692
|
-
locals,
|
|
693
|
-
parent: this,
|
|
694
|
-
rootContext: this.rootContext,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
has(name) {
|
|
699
|
-
if (Object.hasOwn(this.locals, name)) {
|
|
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 };
|
|
957
|
-
}
|
|
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
|
-
|
|
982
|
-
const comparableLeft = toComparableValue(left);
|
|
983
|
-
const comparableRight = toComparableValue(right);
|
|
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);
|
|
1037
|
-
}
|
|
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
|
-
|
|
1051
|
-
function invokeGlobalFunction(name, argNodes, scope, evaluate) {
|
|
1052
|
-
if (name === 'if') {
|
|
1053
|
-
const condition = evaluate(argNodes[0], scope);
|
|
1054
|
-
return condition ? evaluate(argNodes[1], scope) : evaluate(argNodes[2], scope);
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
const args = argNodes.map((node) => evaluate(node, scope));
|
|
1058
|
-
switch (name) {
|
|
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}`);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function invokeMethod(target, name, argNodes, scope, evaluate, rootContext) {
|
|
1092
|
-
if (target?.__baseType === 'formula-namespace') {
|
|
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);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
if (Array.isArray(target)) {
|
|
1100
|
-
if (name === 'contains') {
|
|
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
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
if (typeof target === 'string') {
|
|
1156
|
-
switch (name) {
|
|
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
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
if (typeof target === 'number') {
|
|
1183
|
-
switch (name) {
|
|
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
|
-
}));
|
|
1
|
+
import {
|
|
2
|
+
buildColumns,
|
|
3
|
+
collectEvaluatedPropertyIds,
|
|
4
|
+
findView,
|
|
5
|
+
normalizeBaseDefinition,
|
|
6
|
+
} from './base-definition.js';
|
|
7
|
+
import {
|
|
8
|
+
compareValues,
|
|
9
|
+
createEvaluationRootContext,
|
|
10
|
+
evaluateFilterNode,
|
|
11
|
+
getPropertyValue,
|
|
12
|
+
serializeBaseValue,
|
|
13
|
+
} from './base-expression-runtime.js';
|
|
14
|
+
import { BaseIndexSnapshotStore } from './base-index-snapshot-store.js';
|
|
15
|
+
import {
|
|
16
|
+
buildAvailableProperties,
|
|
17
|
+
collectDistinctPropertyValues,
|
|
18
|
+
createPropertyCatalogSnapshot,
|
|
19
|
+
inferValueType,
|
|
20
|
+
} from './base-query-metadata.js';
|
|
21
|
+
import { buildQueryResultPayload, rowMatchesSearch } from './base-query-results.js';
|
|
22
|
+
import { transformBaseSource } from './base-transform.js';
|
|
23
|
+
|
|
24
|
+
export { serializeBaseDefinition } from './base-definition.js';
|
|
25
|
+
|
|
26
|
+
function buildSortChain(activeView) {
|
|
27
|
+
const entries = [];
|
|
28
|
+
(activeView.sort ?? []).forEach((sortConfig) => {
|
|
29
|
+
if (
|
|
30
|
+
sortConfig?.property
|
|
31
|
+
&& !entries.some((entry) => entry.property === sortConfig.property)
|
|
32
|
+
) {
|
|
33
|
+
entries.push(sortConfig);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return entries;
|
|
1500
37
|
}
|
|
1501
38
|
|
|
1502
|
-
function
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (view.groupBy) {
|
|
1506
|
-
propertyIds.add(view.groupBy);
|
|
39
|
+
function sortRows(rows, sortChain = []) {
|
|
40
|
+
if (!Array.isArray(sortChain) || sortChain.length === 0) {
|
|
41
|
+
return rows;
|
|
1507
42
|
}
|
|
1508
43
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
44
|
+
return rows.slice().sort((left, right) => {
|
|
45
|
+
for (const sortConfig of sortChain) {
|
|
46
|
+
const delta = compareValues(left.rawCells[sortConfig.property], right.rawCells[sortConfig.property]);
|
|
47
|
+
if (delta !== 0) {
|
|
48
|
+
return sortConfig.direction === 'desc' ? -delta : delta;
|
|
49
|
+
}
|
|
1512
50
|
}
|
|
1513
|
-
});
|
|
1514
51
|
|
|
1515
|
-
|
|
1516
|
-
if (summary?.property) {
|
|
1517
|
-
propertyIds.add(summary.property);
|
|
1518
|
-
}
|
|
52
|
+
return String(left.file.path ?? '').localeCompare(String(right.file.path ?? ''));
|
|
1519
53
|
});
|
|
54
|
+
}
|
|
1520
55
|
|
|
1521
|
-
|
|
56
|
+
function escapeRegExp(value = '') {
|
|
57
|
+
return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
1522
58
|
}
|
|
1523
59
|
|
|
1524
|
-
function
|
|
1525
|
-
|
|
1526
|
-
|
|
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;
|
|
60
|
+
function pruneFilterNodeForProperty(filterNode, propertyId = '') {
|
|
61
|
+
if (!propertyId || !filterNode) {
|
|
62
|
+
return filterNode;
|
|
1597
63
|
}
|
|
1598
|
-
}
|
|
1599
64
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
property: summary.property,
|
|
1609
|
-
propertyLabel: column?.label ?? summary.property,
|
|
1610
|
-
value: serializeBaseValue(value, {
|
|
1611
|
-
snapshot,
|
|
1612
|
-
sourcePath: thisFile?.path ?? '',
|
|
1613
|
-
}),
|
|
1614
|
-
};
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
65
|
+
if (typeof filterNode === 'string') {
|
|
66
|
+
const escapedPropertyId = escapeRegExp(propertyId);
|
|
67
|
+
const propertyFilterPattern = new RegExp(
|
|
68
|
+
`^!?${escapedPropertyId}(?:\\.(?:contains|startsWith|endsWith|isEmpty)\\(.*\\)|\\s*(?:==|!=|>=|<=|>|<)\\s*.+)$`,
|
|
69
|
+
'u',
|
|
70
|
+
);
|
|
71
|
+
return propertyFilterPattern.test(String(filterNode ?? '').trim()) ? null : filterNode;
|
|
72
|
+
}
|
|
1617
73
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
74
|
+
if (Array.isArray(filterNode?.and)) {
|
|
75
|
+
const children = filterNode.and
|
|
76
|
+
.map((child) => pruneFilterNodeForProperty(child, propertyId))
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
return children.length > 0 ? { and: children } : null;
|
|
1622
79
|
}
|
|
1623
80
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
}
|
|
81
|
+
if (Array.isArray(filterNode?.or)) {
|
|
82
|
+
const children = filterNode.or
|
|
83
|
+
.map((child) => pruneFilterNodeForProperty(child, propertyId))
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
return children.length > 0 ? { or: children } : null;
|
|
86
|
+
}
|
|
1630
87
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
88
|
+
if (filterNode?.not != null) {
|
|
89
|
+
if (Array.isArray(filterNode.not)) {
|
|
90
|
+
const children = filterNode.not
|
|
91
|
+
.map((child) => pruneFilterNodeForProperty(child, propertyId))
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
return children.length > 0 ? { not: children } : null;
|
|
1636
94
|
}
|
|
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
95
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
96
|
+
const child = pruneFilterNodeForProperty(filterNode.not, propertyId);
|
|
97
|
+
return child ? { not: child } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return filterNode;
|
|
1650
101
|
}
|
|
1651
102
|
|
|
1652
103
|
export class BaseQueryService {
|
|
@@ -1656,305 +107,112 @@ export class BaseQueryService {
|
|
|
1656
107
|
workspaceStateSynchronizer = null,
|
|
1657
108
|
}) {
|
|
1658
109
|
this.vaultFileStore = vaultFileStore;
|
|
1659
|
-
this.
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
110
|
+
this.snapshotStore = new BaseIndexSnapshotStore({
|
|
111
|
+
vaultFileStore,
|
|
112
|
+
workspaceStateProvider,
|
|
113
|
+
workspaceStateSynchronizer,
|
|
114
|
+
});
|
|
115
|
+
this.propertyCatalogCache = new Map();
|
|
1663
116
|
}
|
|
1664
117
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
this.lastWorkspaceState = workspaceState;
|
|
1669
|
-
return workspaceState;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (this.lastWorkspaceState) {
|
|
1673
|
-
return this.lastWorkspaceState;
|
|
1674
|
-
}
|
|
118
|
+
get workspaceStateProvider() {
|
|
119
|
+
return this.snapshotStore.workspaceStateProvider;
|
|
120
|
+
}
|
|
1675
121
|
|
|
1676
|
-
|
|
1677
|
-
this.
|
|
1678
|
-
return scannedWorkspaceState;
|
|
122
|
+
set workspaceStateProvider(value) {
|
|
123
|
+
this.snapshotStore.workspaceStateProvider = value;
|
|
1679
124
|
}
|
|
1680
125
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
};
|
|
126
|
+
get workspaceStateSynchronizer() {
|
|
127
|
+
return this.snapshotStore.workspaceStateSynchronizer;
|
|
128
|
+
}
|
|
1707
129
|
|
|
1708
|
-
|
|
1709
|
-
|
|
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
|
-
};
|
|
130
|
+
set workspaceStateSynchronizer(value) {
|
|
131
|
+
this.snapshotStore.workspaceStateSynchronizer = value;
|
|
1720
132
|
}
|
|
1721
133
|
|
|
1722
|
-
|
|
1723
|
-
|
|
134
|
+
get indexSnapshot() {
|
|
135
|
+
return this.snapshotStore.indexSnapshot;
|
|
136
|
+
}
|
|
1724
137
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
if (!backlinksByTarget.has(targetPath) || targetPath === row.file.path) {
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
138
|
+
set indexSnapshot(value) {
|
|
139
|
+
this.snapshotStore.indexSnapshot = value;
|
|
140
|
+
}
|
|
1731
141
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
}));
|
|
1736
|
-
});
|
|
1737
|
-
});
|
|
142
|
+
get lastWorkspaceState() {
|
|
143
|
+
return this.snapshotStore.lastWorkspaceState;
|
|
144
|
+
}
|
|
1738
145
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
row.file.backlinks = dedupeLinkValues(backlinksByTarget.get(filePath) ?? []);
|
|
1742
|
-
});
|
|
146
|
+
set lastWorkspaceState(value) {
|
|
147
|
+
this.snapshotStore.lastWorkspaceState = value;
|
|
1743
148
|
}
|
|
1744
149
|
|
|
1745
|
-
async
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
const wikiTargetIndex = createWikiTargetIndex(fileEntries);
|
|
1749
|
-
const markdownContents = new Map();
|
|
150
|
+
async getWorkspaceState() {
|
|
151
|
+
return this.snapshotStore.getWorkspaceState();
|
|
152
|
+
}
|
|
1750
153
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
154
|
+
createSnapshotRow(...args) {
|
|
155
|
+
return this.snapshotStore.createSnapshotRow(...args);
|
|
156
|
+
}
|
|
1754
157
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
-
});
|
|
158
|
+
rebuildBacklinks(...args) {
|
|
159
|
+
return this.snapshotStore.rebuildBacklinks(...args);
|
|
160
|
+
}
|
|
1763
161
|
|
|
1764
|
-
|
|
1765
|
-
|
|
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;
|
|
162
|
+
async buildIndexSnapshot(...args) {
|
|
163
|
+
return this.snapshotStore.buildIndexSnapshot(...args);
|
|
1777
164
|
}
|
|
1778
165
|
|
|
1779
166
|
async synchronizeWorkspaceState() {
|
|
1780
|
-
|
|
167
|
+
return this.snapshotStore.synchronizeWorkspaceState();
|
|
1781
168
|
}
|
|
1782
169
|
|
|
1783
|
-
removeSnapshotPath(
|
|
1784
|
-
|
|
1785
|
-
snapshot.forwardLinksByPath.delete(filePath);
|
|
1786
|
-
snapshot.backlinksByPath.delete(filePath);
|
|
1787
|
-
snapshot.filePaths = snapshot.filePaths.filter((candidatePath) => candidatePath !== filePath);
|
|
170
|
+
removeSnapshotPath(...args) {
|
|
171
|
+
return this.snapshotStore.removeSnapshotPath(...args);
|
|
1788
172
|
}
|
|
1789
173
|
|
|
1790
|
-
upsertSnapshotPath(
|
|
1791
|
-
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
snapshot.filePaths.push(filePath);
|
|
1796
|
-
snapshot.filePaths.sort(compareVaultPaths);
|
|
174
|
+
upsertSnapshotPath(...args) {
|
|
175
|
+
return this.snapshotStore.upsertSnapshotPath(...args);
|
|
1797
176
|
}
|
|
1798
177
|
|
|
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;
|
|
178
|
+
collectImpactedSourcesForMembershipChanges(...args) {
|
|
179
|
+
return this.snapshotStore.collectImpactedSourcesForMembershipChanges(...args);
|
|
1820
180
|
}
|
|
1821
181
|
|
|
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
|
-
});
|
|
182
|
+
async refreshSnapshotRows(...args) {
|
|
183
|
+
return this.snapshotStore.refreshSnapshotRows(...args);
|
|
1837
184
|
}
|
|
1838
185
|
|
|
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);
|
|
186
|
+
async ensureIndexSnapshot(...args) {
|
|
187
|
+
return this.snapshotStore.ensureIndexSnapshot(...args);
|
|
1858
188
|
}
|
|
1859
189
|
|
|
1860
|
-
async initializeFromWorkspaceState(
|
|
1861
|
-
this.
|
|
1862
|
-
if (this.indexSnapshot && workspaceState && this.indexSnapshot.scannedAt !== workspaceState.scannedAt) {
|
|
1863
|
-
this.indexSnapshot = null;
|
|
1864
|
-
}
|
|
1865
|
-
return this.indexSnapshot;
|
|
190
|
+
async initializeFromWorkspaceState(...args) {
|
|
191
|
+
return this.snapshotStore.initializeFromWorkspaceState(...args);
|
|
1866
192
|
}
|
|
1867
193
|
|
|
1868
|
-
async applyWorkspaceChange(
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
-
]);
|
|
194
|
+
async applyWorkspaceChange(...args) {
|
|
195
|
+
const result = await this.snapshotStore.applyWorkspaceChange(...args);
|
|
196
|
+
const scannedAt = this.snapshotStore.indexSnapshot?.scannedAt;
|
|
197
|
+
if (scannedAt) {
|
|
198
|
+
this.invalidatePropertyCatalogsExcept(scannedAt);
|
|
1944
199
|
} else {
|
|
1945
|
-
|
|
200
|
+
this.propertyCatalogCache.clear();
|
|
1946
201
|
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
1947
204
|
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
205
|
+
invalidatePropertyCatalogsExcept(scannedAt = '') {
|
|
206
|
+
Array.from(this.propertyCatalogCache.keys()).forEach((cacheKey) => {
|
|
207
|
+
if (cacheKey !== scannedAt) {
|
|
208
|
+
this.propertyCatalogCache.delete(cacheKey);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
1952
211
|
}
|
|
1953
212
|
|
|
1954
|
-
async
|
|
213
|
+
async resolveQueryContext({
|
|
1955
214
|
activeFilePath = '',
|
|
1956
215
|
basePath = '',
|
|
1957
|
-
search = '',
|
|
1958
216
|
source = null,
|
|
1959
217
|
sourcePath = '',
|
|
1960
218
|
view: requestedView = '',
|
|
@@ -1969,128 +227,195 @@ export class BaseQueryService {
|
|
|
1969
227
|
basePath,
|
|
1970
228
|
sourcePath,
|
|
1971
229
|
});
|
|
230
|
+
this.invalidatePropertyCatalogsExcept(snapshot?.scannedAt ?? '');
|
|
231
|
+
|
|
1972
232
|
const thisFilePath = sourcePath || activeFilePath || basePath || '';
|
|
1973
233
|
const thisFile = snapshot.rowsByPath.get(thisFilePath)?.file ?? null;
|
|
1974
|
-
const activeView = definition
|
|
234
|
+
const activeView = findView(definition, requestedView);
|
|
1975
235
|
const columns = buildColumns(definition, activeView);
|
|
1976
236
|
const evaluatedPropertyIds = collectEvaluatedPropertyIds(columns, activeView);
|
|
1977
237
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
238
|
+
return {
|
|
239
|
+
activeView,
|
|
240
|
+
basePath,
|
|
241
|
+
baseSource,
|
|
242
|
+
columns,
|
|
243
|
+
definition,
|
|
244
|
+
evaluatedPropertyIds,
|
|
245
|
+
snapshot,
|
|
246
|
+
sourcePath,
|
|
247
|
+
thisFile,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
collectCandidateRows({
|
|
252
|
+
activeView,
|
|
253
|
+
astCache = new Map(),
|
|
254
|
+
definition,
|
|
255
|
+
evaluatedPropertyIds,
|
|
256
|
+
snapshot,
|
|
257
|
+
thisFile,
|
|
258
|
+
}) {
|
|
259
|
+
const rows = [];
|
|
260
|
+
|
|
261
|
+
snapshot.filePaths.forEach((filePath) => {
|
|
262
|
+
const row = snapshot.rowsByPath.get(filePath);
|
|
263
|
+
if (!row) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rowContext = this.createRowQueryContext({
|
|
268
|
+
astCache,
|
|
269
|
+
definition,
|
|
270
|
+
row,
|
|
271
|
+
snapshot,
|
|
272
|
+
thisFile,
|
|
273
|
+
});
|
|
274
|
+
if (!evaluateFilterNode(definition.filters, rowContext) || !evaluateFilterNode(activeView.filters, rowContext)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
1991
277
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
snapshot,
|
|
1996
|
-
thisFile,
|
|
1997
|
-
});
|
|
1998
|
-
return evaluateFilterNode(activeView.filters, viewContext);
|
|
1999
|
-
})
|
|
2000
|
-
.map((row) => {
|
|
2001
|
-
const rawCells = {};
|
|
2002
|
-
evaluatedPropertyIds.forEach((propertyId) => {
|
|
2003
|
-
rawCells[propertyId] = getPropertyValue(propertyId, row, definition, snapshot, thisFile);
|
|
2004
|
-
});
|
|
2005
|
-
return {
|
|
2006
|
-
file: row.file,
|
|
2007
|
-
rawCells,
|
|
2008
|
-
};
|
|
278
|
+
const rawCells = {};
|
|
279
|
+
evaluatedPropertyIds.forEach((propertyId) => {
|
|
280
|
+
rawCells[propertyId] = getPropertyValue(propertyId, row, definition, snapshot, thisFile, rowContext);
|
|
2009
281
|
});
|
|
2010
282
|
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
return sortConfig.direction === 'desc' ? -delta : delta;
|
|
283
|
+
rows.push({
|
|
284
|
+
file: row.file,
|
|
285
|
+
rawCells,
|
|
2015
286
|
});
|
|
2016
287
|
});
|
|
2017
288
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
289
|
+
return rows;
|
|
290
|
+
}
|
|
2021
291
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
} else {
|
|
2043
|
-
groupsByKey.set('All', {
|
|
2044
|
-
key: 'All',
|
|
2045
|
-
label: 'All',
|
|
2046
|
-
rows,
|
|
2047
|
-
value: serializeBaseValue(null),
|
|
2048
|
-
});
|
|
292
|
+
createRowQueryContext({
|
|
293
|
+
astCache = new Map(),
|
|
294
|
+
definition,
|
|
295
|
+
row,
|
|
296
|
+
snapshot,
|
|
297
|
+
thisFile,
|
|
298
|
+
}) {
|
|
299
|
+
return createEvaluationRootContext({
|
|
300
|
+
astCache,
|
|
301
|
+
currentRow: row,
|
|
302
|
+
definition,
|
|
303
|
+
snapshot,
|
|
304
|
+
thisFile,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getPropertyCatalog(snapshot) {
|
|
309
|
+
const cacheKey = snapshot?.scannedAt ?? 'unknown';
|
|
310
|
+
if (!this.propertyCatalogCache.has(cacheKey)) {
|
|
311
|
+
this.propertyCatalogCache.set(cacheKey, createPropertyCatalogSnapshot(snapshot));
|
|
2049
312
|
}
|
|
2050
313
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
314
|
+
return this.propertyCatalogCache.get(cacheKey);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
inferFormulaValueTypes(definition, rows = []) {
|
|
318
|
+
const valueTypes = {};
|
|
319
|
+
|
|
320
|
+
Object.keys(definition.formulas ?? {}).forEach((propertyId) => {
|
|
321
|
+
let inferred = 'unknown';
|
|
322
|
+
for (const row of rows) {
|
|
323
|
+
inferred = inferValueType(row?.rawCells?.[propertyId]);
|
|
324
|
+
if (inferred !== 'unknown') {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
valueTypes[propertyId] = inferred;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return valueTypes;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
buildMeta({
|
|
335
|
+
activeView,
|
|
336
|
+
basePath = '',
|
|
337
|
+
columns,
|
|
338
|
+
definition,
|
|
339
|
+
rows,
|
|
340
|
+
snapshot,
|
|
341
|
+
}) {
|
|
342
|
+
const propertyCatalog = this.getPropertyCatalog(snapshot);
|
|
343
|
+
const formulaValueTypes = this.inferFormulaValueTypes(definition, rows);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
activeViewConfig: {
|
|
347
|
+
filters: activeView.filters ?? null,
|
|
348
|
+
groupBy: activeView.groupBy ?? null,
|
|
349
|
+
order: [...(activeView.order ?? [])],
|
|
350
|
+
sort: (activeView.sort ?? []).map((entry) => ({ ...entry })),
|
|
351
|
+
},
|
|
352
|
+
availableProperties: buildAvailableProperties({
|
|
353
|
+
activeView,
|
|
354
|
+
columns,
|
|
355
|
+
definition,
|
|
356
|
+
formulaValueTypes,
|
|
357
|
+
propertyCatalog,
|
|
2064
358
|
}),
|
|
2065
|
-
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
359
|
+
editable: Boolean(basePath),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async query({
|
|
364
|
+
activeFilePath = '',
|
|
365
|
+
basePath = '',
|
|
366
|
+
includeCsv = false,
|
|
367
|
+
search = '',
|
|
368
|
+
source = null,
|
|
369
|
+
sourcePath = '',
|
|
370
|
+
view: requestedView = '',
|
|
371
|
+
} = {}) {
|
|
372
|
+
const context = await this.resolveQueryContext({
|
|
373
|
+
activeFilePath,
|
|
374
|
+
basePath,
|
|
375
|
+
source,
|
|
376
|
+
sourcePath,
|
|
377
|
+
view: requestedView,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const astCache = new Map();
|
|
381
|
+
let rows = this.collectCandidateRows({
|
|
382
|
+
...context,
|
|
383
|
+
astCache,
|
|
384
|
+
});
|
|
385
|
+
rows = rows.filter((row) => rowMatchesSearch(row, context.columns, search));
|
|
386
|
+
rows = sortRows(rows, buildSortChain(context.activeView));
|
|
387
|
+
|
|
388
|
+
if (context.activeView.limit != null) {
|
|
389
|
+
rows = rows.slice(0, context.activeView.limit);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const payload = buildQueryResultPayload({
|
|
393
|
+
activeView: context.activeView,
|
|
394
|
+
columns: context.columns,
|
|
395
|
+
definition: context.definition,
|
|
396
|
+
includeCsv,
|
|
397
|
+
rows,
|
|
398
|
+
snapshot: context.snapshot,
|
|
399
|
+
thisFile: context.thisFile,
|
|
400
|
+
});
|
|
2076
401
|
|
|
2077
402
|
return {
|
|
2078
|
-
columns,
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
403
|
+
columns: context.columns,
|
|
404
|
+
definition: context.definition,
|
|
405
|
+
meta: this.buildMeta({
|
|
406
|
+
activeView: context.activeView,
|
|
407
|
+
basePath,
|
|
408
|
+
columns: context.columns,
|
|
409
|
+
definition: context.definition,
|
|
410
|
+
rows,
|
|
411
|
+
snapshot: context.snapshot,
|
|
2087
412
|
}),
|
|
2088
|
-
|
|
413
|
+
...payload,
|
|
2089
414
|
view: {
|
|
2090
|
-
...activeView,
|
|
2091
|
-
supported: activeView.supported,
|
|
415
|
+
...context.activeView,
|
|
416
|
+
supported: context.activeView.supported,
|
|
2092
417
|
},
|
|
2093
|
-
views: definition.views.map((view) => ({
|
|
418
|
+
views: context.definition.views.map((view) => ({
|
|
2094
419
|
id: view.id,
|
|
2095
420
|
name: view.name,
|
|
2096
421
|
supported: view.supported,
|
|
@@ -2098,4 +423,93 @@ export class BaseQueryService {
|
|
|
2098
423
|
})),
|
|
2099
424
|
};
|
|
2100
425
|
}
|
|
426
|
+
|
|
427
|
+
async propertyValues({
|
|
428
|
+
activeFilePath = '',
|
|
429
|
+
basePath = '',
|
|
430
|
+
propertyId = '',
|
|
431
|
+
query = '',
|
|
432
|
+
source = null,
|
|
433
|
+
sourcePath = '',
|
|
434
|
+
view: requestedView = '',
|
|
435
|
+
} = {}) {
|
|
436
|
+
if (!propertyId) {
|
|
437
|
+
throw new Error('Missing propertyId');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const context = await this.resolveQueryContext({
|
|
441
|
+
activeFilePath,
|
|
442
|
+
basePath,
|
|
443
|
+
source,
|
|
444
|
+
sourcePath,
|
|
445
|
+
view: requestedView,
|
|
446
|
+
});
|
|
447
|
+
const propertyValueContext = {
|
|
448
|
+
...context,
|
|
449
|
+
activeView: {
|
|
450
|
+
...context.activeView,
|
|
451
|
+
filters: pruneFilterNodeForProperty(context.activeView?.filters, propertyId),
|
|
452
|
+
},
|
|
453
|
+
definition: {
|
|
454
|
+
...context.definition,
|
|
455
|
+
filters: pruneFilterNodeForProperty(context.definition?.filters, propertyId),
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
const astCache = new Map();
|
|
459
|
+
const rows = this.collectCandidateRows({
|
|
460
|
+
...propertyValueContext,
|
|
461
|
+
astCache,
|
|
462
|
+
evaluatedPropertyIds: [...new Set([
|
|
463
|
+
...(context.evaluatedPropertyIds ?? []),
|
|
464
|
+
propertyId,
|
|
465
|
+
])],
|
|
466
|
+
});
|
|
467
|
+
const values = collectDistinctPropertyValues(rows, propertyId, query).map((entry) => ({
|
|
468
|
+
count: entry.count,
|
|
469
|
+
text: entry.text,
|
|
470
|
+
value: serializeBaseValue(entry.value, {
|
|
471
|
+
snapshot: context.snapshot,
|
|
472
|
+
sourcePath: context.thisFile?.path ?? '',
|
|
473
|
+
}),
|
|
474
|
+
}));
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
ok: true,
|
|
478
|
+
propertyId,
|
|
479
|
+
values,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async transform({
|
|
484
|
+
activeFilePath = '',
|
|
485
|
+
basePath = '',
|
|
486
|
+
mutation = null,
|
|
487
|
+
source = null,
|
|
488
|
+
sourcePath = '',
|
|
489
|
+
view = '',
|
|
490
|
+
} = {}) {
|
|
491
|
+
if (!basePath) {
|
|
492
|
+
throw new Error('Only .base files can be transformed');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const baseSource = source ?? await this.vaultFileStore.readBaseFile(basePath);
|
|
496
|
+
if (typeof baseSource !== 'string') {
|
|
497
|
+
throw new Error('Base source not found');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const nextSource = transformBaseSource(baseSource, mutation);
|
|
501
|
+
const result = await this.query({
|
|
502
|
+
activeFilePath,
|
|
503
|
+
basePath,
|
|
504
|
+
source: nextSource,
|
|
505
|
+
sourcePath,
|
|
506
|
+
view,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
meta: result.meta,
|
|
511
|
+
result,
|
|
512
|
+
source: nextSource,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
2101
515
|
}
|