@dinoxx/dinox-cli 2.2.0 → 2.4.0
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/README.md +17 -3
- package/dist/cli.js +6 -0
- package/dist/commands/domain/options.d.ts +10 -0
- package/dist/commands/domain/options.js +75 -0
- package/dist/commands/domain/run.d.ts +24 -0
- package/dist/commands/domain/run.js +137 -0
- package/dist/commands/gateway/index.d.ts +4 -0
- package/dist/commands/gateway/index.js +300 -0
- package/dist/commands/notes/actions.js +47 -18
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +9 -4
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/write.d.ts +2 -0
- package/dist/commands/notes/repo/write.js +1 -1
- package/dist/commands/properties/index.d.ts +16 -0
- package/dist/commands/properties/index.js +640 -0
- package/dist/commands/properties/repo.d.ts +150 -0
- package/dist/commands/properties/repo.js +272 -0
- package/dist/commands/savedViews/fields.d.ts +28 -0
- package/dist/commands/savedViews/fields.js +87 -0
- package/dist/commands/savedViews/index.js +203 -164
- package/dist/commands/savedViews/model.d.ts +27 -69
- package/dist/commands/savedViews/model.js +63 -279
- package/dist/commands/savedViews/repo.d.ts +64 -62
- package/dist/commands/savedViews/repo.js +309 -428
- package/dist/commands/schema/renderSkills.js +33 -5
- package/dist/commands/schema/spec.js +6 -0
- package/dist/commands/templates/index.d.ts +4 -0
- package/dist/commands/templates/index.js +290 -0
- package/dist/commands/templates/model.d.ts +12 -34
- package/dist/commands/templates/model.js +39 -83
- package/dist/commands/templates/repo.d.ts +53 -0
- package/dist/commands/templates/repo.js +78 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +31 -0
- package/dist/gateway/adapters/acp.d.ts +42 -0
- package/dist/gateway/adapters/acp.js +285 -0
- package/dist/gateway/adapters/echo.d.ts +12 -0
- package/dist/gateway/adapters/echo.js +51 -0
- package/dist/gateway/adapters/index.d.ts +9 -0
- package/dist/gateway/adapters/index.js +30 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +50 -0
- package/dist/gateway/config.js +253 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +82 -0
- package/dist/gateway/deviceHost.js +530 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +118 -0
- package/dist/gateway/protocol.d.ts +29 -0
- package/dist/gateway/protocol.js +127 -0
- package/dist/gateway/runExecutor.d.ts +67 -0
- package/dist/gateway/runExecutor.js +316 -0
- package/dist/gateway/service.d.ts +26 -0
- package/dist/gateway/service.js +82 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +157 -0
- package/dist/gateway/threadStore.js +430 -0
- package/dist/gateway/types.d.ts +117 -0
- package/dist/gateway/types.js +9 -0
- package/dist/skills/registry.js +7 -0
- package/dist/utils/errorCodes.d.ts +212 -0
- package/dist/utils/errorCodes.js +53 -0
- package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
- package/dist/vendor/electron/domain/note-templates.js +232 -0
- package/dist/vendor/electron/domain/properties.d.ts +10 -0
- package/dist/vendor/electron/domain/properties.js +1549 -0
- package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
- package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
- package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
- package/dist/vendor/electron/domain/saved-views.js +1120 -0
- package/dist/vendor/electron/rpc.d.ts +22 -0
- package/dist/vendor/electron/rpc.js +27 -0
- package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
- package/dist/vendor/electron/shared/ai-errors.js +107 -0
- package/dist/vendor/electron/shared/external-types.d.ts +20 -0
- package/dist/vendor/electron/shared/external-types.js +1 -0
- package/dist/vendor/electron/shared/index.d.ts +9 -0
- package/dist/vendor/electron/shared/index.js +7 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
- package/dist/vendor/electron/shared/note-properties.js +1027 -0
- package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
- package/dist/vendor/electron/shared/note-templates.js +84 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
- package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
- package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
- package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
- package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
- package/dist/vendor/electron/shared/saved-views.js +981 -0
- package/dist/vendor/electron/sql-utils.d.ts +5 -0
- package/dist/vendor/electron/sql-utils.js +32 -0
- package/package.json +10 -4
- package/dist/commands/savedViews/semantics.d.ts +0 -46
- package/dist/commands/savedViews/semantics.js +0 -738
|
@@ -1,512 +1,393 @@
|
|
|
1
|
-
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
2
1
|
import { DinoxError } from '../../utils/errors.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { countSavedViewScopedRows, fetchSavedViewScopedPage, resolveSavedViewQueryScope, } from '../../vendor/electron/domain/saved-views.js';
|
|
3
|
+
import { parseSavedViewZettelBoxDetails, SAVED_VIEW_LAYOUTS, } from '../../vendor/electron/shared/index.js';
|
|
4
|
+
import { fetchDomainReadModel, previewInRolledBackTransaction, runDomainCommand, toDinoxError, withReadTransaction, } from '../domain/run.js';
|
|
5
|
+
import { listPropertyDefinitions, valueTypeForPropertyType } from './fields.js';
|
|
6
|
+
import { createDefaultSavedViewConfig, createDefaultSavedViewFilter, inspectSavedViewRow, parseSavedViewConfig, } from './model.js';
|
|
7
|
+
export { listSavedViewFields } from './fields.js';
|
|
8
|
+
const ACTIVE_VIEW_TYPE_SQL = `view_type IN (${SAVED_VIEW_LAYOUTS.map((layout) => `'${layout}'`).join(', ')})`;
|
|
6
9
|
const SAVED_VIEW_COLUMNS = `
|
|
7
|
-
id,
|
|
8
|
-
|
|
9
|
-
name,
|
|
10
|
-
emoji,
|
|
11
|
-
filter_json,
|
|
12
|
-
view_type,
|
|
13
|
-
config_json,
|
|
14
|
-
template_id,
|
|
15
|
-
group_id,
|
|
16
|
-
sort_rank,
|
|
17
|
-
pinned_at,
|
|
18
|
-
created_at,
|
|
19
|
-
updated_at,
|
|
20
|
-
is_del
|
|
10
|
+
id, user_id, name, emoji, filter_json, view_type, config_json, template_id, group_id,
|
|
11
|
+
sort_rank, pinned_at, created_at, updated_at, is_del
|
|
21
12
|
`;
|
|
22
|
-
function
|
|
23
|
-
return new
|
|
13
|
+
function nowIso() {
|
|
14
|
+
return new Date().toISOString();
|
|
24
15
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
throw new DinoxError(`${label} is required`, { code: 'USAGE_ERROR' });
|
|
28
|
-
}
|
|
29
|
-
const normalized = value.trim();
|
|
30
|
-
if (normalized.length > maxLength) {
|
|
31
|
-
throw new DinoxError(`${label} must be at most ${maxLength} characters`, { code: 'USAGE_ERROR' });
|
|
32
|
-
}
|
|
33
|
-
return normalized;
|
|
34
|
-
}
|
|
35
|
-
function normalizeNullableText(value, label, maxLength) {
|
|
36
|
-
if (value === undefined || value === null)
|
|
37
|
-
return value;
|
|
38
|
-
const normalized = value.trim();
|
|
39
|
-
if (normalized.length > maxLength) {
|
|
40
|
-
throw new DinoxError(`${label} must be at most ${maxLength} characters`, { code: 'USAGE_ERROR' });
|
|
41
|
-
}
|
|
42
|
-
return normalized;
|
|
43
|
-
}
|
|
44
|
-
function normalizeNullableId(value, label) {
|
|
45
|
-
if (value === undefined || value === null)
|
|
46
|
-
return value;
|
|
47
|
-
const normalized = value.trim();
|
|
48
|
-
if (!normalized || normalized.length > 200) {
|
|
49
|
-
throw new DinoxError(`${label} must contain 1-200 characters`, { code: 'USAGE_ERROR' });
|
|
50
|
-
}
|
|
51
|
-
return normalized;
|
|
52
|
-
}
|
|
53
|
-
function normalizeNullableTimestamp(value, label) {
|
|
54
|
-
if (value === undefined || value === null)
|
|
55
|
-
return value;
|
|
56
|
-
const normalized = value.trim();
|
|
57
|
-
if (!normalized || normalized.length > 64) {
|
|
58
|
-
throw new DinoxError(`${label} must contain 1-64 characters`, { code: 'USAGE_ERROR' });
|
|
59
|
-
}
|
|
60
|
-
return normalized;
|
|
16
|
+
function notFound(id) {
|
|
17
|
+
return new DinoxError(`Saved view not found: ${id}`, { code: 'SAVED_VIEW_NOT_FOUND', details: { viewId: id } });
|
|
61
18
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!Number.isSafeInteger(value)) {
|
|
66
|
-
throw new DinoxError('Saved view sort rank must be an integer', { code: 'USAGE_ERROR' });
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
function normalizeLimit(value, defaultValue, maxValue) {
|
|
71
|
-
if (value === undefined)
|
|
72
|
-
return defaultValue;
|
|
73
|
-
if (!Number.isSafeInteger(value) || value <= 0 || value > maxValue) {
|
|
74
|
-
throw new DinoxError(`Limit must be an integer between 1 and ${maxValue}`, { code: 'USAGE_ERROR' });
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
function normalizeOffset(value) {
|
|
79
|
-
if (value === undefined)
|
|
80
|
-
return 0;
|
|
81
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
82
|
-
throw new DinoxError('Offset must be a non-negative integer', { code: 'USAGE_ERROR' });
|
|
83
|
-
}
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
function errorSummary(error) {
|
|
19
|
+
function catalogItem(row) {
|
|
20
|
+
const inspection = inspectSavedViewRow(row);
|
|
21
|
+
const view = inspection.view;
|
|
87
22
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
23
|
+
id: view.id,
|
|
24
|
+
name: view.name,
|
|
25
|
+
emoji: view.emoji,
|
|
26
|
+
layout: inspection.valid ? inspection.view.layout : row.view_type,
|
|
27
|
+
templateId: view.templateId,
|
|
28
|
+
groupId: view.groupId,
|
|
29
|
+
sortRank: view.sortRank,
|
|
30
|
+
pinnedAt: view.pinnedAt,
|
|
31
|
+
createdAt: view.createdAt,
|
|
32
|
+
updatedAt: view.updatedAt,
|
|
33
|
+
valid: inspection.valid,
|
|
34
|
+
...(inspection.valid ? {} : { error: { code: inspection.error.code, message: inspection.error.message } }),
|
|
90
35
|
};
|
|
91
36
|
}
|
|
92
37
|
export async function listSavedViews(db, input) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
WHERE user_id = ?
|
|
99
|
-
AND view_type = 'table'
|
|
100
|
-
AND (is_del IS NULL OR is_del = 0)
|
|
101
|
-
ORDER BY
|
|
102
|
-
CASE WHEN pinned_at IS NULL OR trim(pinned_at) = '' THEN 1 ELSE 0 END ASC,
|
|
103
|
-
pinned_at DESC,
|
|
104
|
-
COALESCE(sort_rank, 0) ASC,
|
|
105
|
-
COALESCE(updated_at, created_at, id) DESC,
|
|
106
|
-
id DESC
|
|
107
|
-
LIMIT ?
|
|
108
|
-
`, [userId, limit]);
|
|
109
|
-
const items = [];
|
|
110
|
-
for (const row of rows) {
|
|
111
|
-
const id = row.id?.trim();
|
|
112
|
-
if (!id)
|
|
113
|
-
continue;
|
|
114
|
-
const base = {
|
|
115
|
-
id,
|
|
116
|
-
name: row.name?.trim() || '',
|
|
117
|
-
emoji: row.emoji,
|
|
118
|
-
templateId: row.template_id,
|
|
119
|
-
groupId: row.group_id,
|
|
120
|
-
sortRank: row.sort_rank,
|
|
121
|
-
pinnedAt: row.pinned_at,
|
|
122
|
-
createdAt: row.created_at,
|
|
123
|
-
updatedAt: row.updated_at,
|
|
124
|
-
};
|
|
125
|
-
try {
|
|
126
|
-
mapSavedViewRow(row);
|
|
127
|
-
items.push({ ...base, valid: true });
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
items.push({ ...base, valid: false, error: errorSummary(error) });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return items;
|
|
38
|
+
const rows = await fetchDomainReadModel(db, 'savedViews.catalog', {
|
|
39
|
+
userId: input.userId,
|
|
40
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
41
|
+
});
|
|
42
|
+
return rows.filter((row) => row.id?.trim()).map(catalogItem);
|
|
134
43
|
}
|
|
135
44
|
async function findSavedViewRow(db, input) {
|
|
136
|
-
|
|
137
|
-
const userId = normalizeRequiredText(input.userId, 'Saved view user id');
|
|
138
|
-
return db.getOptional(`
|
|
45
|
+
return (await db.getOptional(`
|
|
139
46
|
SELECT ${SAVED_VIEW_COLUMNS}
|
|
140
47
|
FROM c_saved_view
|
|
141
48
|
WHERE id = ?
|
|
142
49
|
AND user_id = ?
|
|
143
|
-
AND
|
|
50
|
+
AND ${ACTIVE_VIEW_TYPE_SQL}
|
|
144
51
|
AND (is_del IS NULL OR is_del = 0)
|
|
145
52
|
LIMIT 1
|
|
146
|
-
`, [id, userId]);
|
|
53
|
+
`, [input.id.trim(), input.userId])) ?? null;
|
|
147
54
|
}
|
|
55
|
+
/** Reads one view without throwing on stored rows the CLI cannot parse (see `valid`). */
|
|
148
56
|
export async function getSavedView(db, input) {
|
|
149
57
|
const row = await findSavedViewRow(db, input);
|
|
150
|
-
if (!row)
|
|
151
|
-
throw
|
|
152
|
-
|
|
153
|
-
return mapSavedViewRow(row);
|
|
154
|
-
}
|
|
155
|
-
export async function resolveSavedViewQueryScope(db, input) {
|
|
156
|
-
const definition = await getSavedView(db, { id: input.viewId, userId: input.userId });
|
|
157
|
-
const semantics = await validateSavedViewSemantics(db, definition.userId, definition.filter, definition.config);
|
|
158
|
-
return {
|
|
159
|
-
definition,
|
|
160
|
-
semantics,
|
|
161
|
-
where: compileSavedViewFilter(definition.filter, semantics),
|
|
162
|
-
order: compileSavedViewOrder(definition.config, semantics),
|
|
163
|
-
};
|
|
58
|
+
if (!row)
|
|
59
|
+
throw notFound(input.id);
|
|
60
|
+
return inspectSavedViewRow(row);
|
|
164
61
|
}
|
|
165
|
-
|
|
166
|
-
return db.getAll(`
|
|
167
|
-
SELECT
|
|
168
|
-
n.id,
|
|
169
|
-
n.title,
|
|
170
|
-
n.created_at,
|
|
171
|
-
n.updated_at,
|
|
172
|
-
n.tags,
|
|
173
|
-
n.zettel_boxes,
|
|
174
|
-
n.source,
|
|
175
|
-
n.type,
|
|
176
|
-
n.starred_at,
|
|
177
|
-
n.properties
|
|
178
|
-
FROM c_note AS n
|
|
179
|
-
WHERE n.user_no = ?
|
|
180
|
-
AND (n.is_del IS NULL OR n.is_del = 0)
|
|
181
|
-
AND (${scope.where.sql})
|
|
182
|
-
ORDER BY ${scope.order.sql}
|
|
183
|
-
LIMIT ? OFFSET ?
|
|
184
|
-
`, [
|
|
185
|
-
scope.definition.userId,
|
|
186
|
-
...scope.where.params,
|
|
187
|
-
...scope.order.params,
|
|
188
|
-
input.limit,
|
|
189
|
-
input.offset,
|
|
190
|
-
]);
|
|
191
|
-
}
|
|
192
|
-
export async function countSavedViewRows(db, input, scope) {
|
|
193
|
-
const resolvedScope = scope ?? await resolveSavedViewQueryScope(db, input);
|
|
194
|
-
const row = await db.getOptional(`
|
|
195
|
-
SELECT COUNT(*) AS total_count
|
|
196
|
-
FROM c_note AS n
|
|
197
|
-
WHERE n.user_no = ?
|
|
198
|
-
AND (n.is_del IS NULL OR n.is_del = 0)
|
|
199
|
-
AND (${resolvedScope.where.sql})
|
|
200
|
-
`, [resolvedScope.definition.userId, ...resolvedScope.where.params]);
|
|
201
|
-
const total = Number(row?.total_count ?? 0);
|
|
202
|
-
return Number.isFinite(total) ? Math.max(0, Math.trunc(total)) : 0;
|
|
203
|
-
}
|
|
204
|
-
function parseJsonObject(raw) {
|
|
62
|
+
function parseJsonValue(raw) {
|
|
205
63
|
if (!raw?.trim())
|
|
206
|
-
return
|
|
64
|
+
return undefined;
|
|
207
65
|
try {
|
|
208
|
-
|
|
209
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
210
|
-
? value
|
|
211
|
-
: {};
|
|
66
|
+
return JSON.parse(raw);
|
|
212
67
|
}
|
|
213
68
|
catch {
|
|
214
|
-
return
|
|
69
|
+
return undefined;
|
|
215
70
|
}
|
|
216
71
|
}
|
|
72
|
+
function parseObject(raw) {
|
|
73
|
+
const value = parseJsonValue(raw);
|
|
74
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
75
|
+
}
|
|
217
76
|
function parseStringArray(raw) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return Array.isArray(value)
|
|
223
|
-
? value.flatMap((item) => typeof item === 'string' && item.trim() ? [item.trim()] : [])
|
|
224
|
-
: [];
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
77
|
+
const value = parseJsonValue(raw);
|
|
78
|
+
return Array.isArray(value)
|
|
79
|
+
? value.flatMap((item) => (typeof item === 'string' && item.trim() ? [item.trim()] : []))
|
|
80
|
+
: [];
|
|
229
81
|
}
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
82
|
+
function parseImages(raw) {
|
|
83
|
+
const value = parseJsonValue(raw);
|
|
84
|
+
return Array.isArray(value) ? value : [];
|
|
85
|
+
}
|
|
86
|
+
function resolveSystemValue(row, field) {
|
|
235
87
|
switch (field) {
|
|
236
88
|
case 'sys.title': return row.title;
|
|
237
89
|
case 'sys.created_at': return row.created_at;
|
|
238
90
|
case 'sys.updated_at': return row.updated_at;
|
|
239
|
-
case 'sys.
|
|
240
|
-
case 'sys.zettel_boxes': return parseStringArray(row.zettel_boxes);
|
|
91
|
+
case 'sys.starred_at': return row.starred_at;
|
|
241
92
|
case 'sys.source': return row.source;
|
|
242
93
|
case 'sys.type': return row.type;
|
|
243
|
-
case 'sys.
|
|
94
|
+
case 'sys.tags': return parseStringArray(row.tags);
|
|
95
|
+
case 'sys.zettel_boxes': return parseStringArray(row.zettel_boxes);
|
|
96
|
+
case 'sys.images': return parseImages(row.images);
|
|
97
|
+
case 'sys.cover': return parseImages(row.images)[0] ?? null;
|
|
98
|
+
case 'sys.primary_resource_type': return row.primary_resource_type ?? null;
|
|
99
|
+
case 'sys.audio':
|
|
100
|
+
case 'sys.video':
|
|
101
|
+
case 'sys.file':
|
|
102
|
+
return row.primary_resource_type === field.slice('sys.'.length);
|
|
103
|
+
default: return undefined;
|
|
244
104
|
}
|
|
245
105
|
}
|
|
246
106
|
function describeOption(value, options) {
|
|
247
107
|
const option = options.find((candidate) => candidate.id === value);
|
|
248
108
|
return option
|
|
249
|
-
? { id: option.id, label: option.label, archived: option.archived, color: option.color }
|
|
109
|
+
? { id: option.id, label: option.label, archived: option.archived, color: option.color, ...(option.group ? { group: option.group } : {}) }
|
|
250
110
|
: { id: value, label: null, unknown: true };
|
|
251
111
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
112
|
+
function displayValue(value, semantic) {
|
|
113
|
+
const options = semantic?.options;
|
|
114
|
+
if (!options)
|
|
254
115
|
return value;
|
|
255
|
-
if (semantic.propertyType === 'select' && typeof value === 'string') {
|
|
256
|
-
return describeOption(value,
|
|
116
|
+
if ((semantic.propertyType === 'select' || semantic.propertyType === 'status') && typeof value === 'string') {
|
|
117
|
+
return describeOption(value, options);
|
|
257
118
|
}
|
|
258
119
|
if (semantic.propertyType === 'multi_select' && Array.isArray(value)) {
|
|
259
|
-
return value.map((item) => typeof item === 'string' ? describeOption(item,
|
|
120
|
+
return value.map((item) => (typeof item === 'string' ? describeOption(item, options) : item));
|
|
260
121
|
}
|
|
261
122
|
return value;
|
|
262
123
|
}
|
|
263
|
-
|
|
264
|
-
|
|
124
|
+
/** Row-level evaluation, aligned with desktop: non-finite side or division by zero → null. */
|
|
125
|
+
export function evaluateComputedColumn(properties, column) {
|
|
126
|
+
const read = (field) => {
|
|
127
|
+
if (!field.startsWith('prop.'))
|
|
128
|
+
return null;
|
|
129
|
+
const raw = properties[field.slice('prop.'.length)];
|
|
130
|
+
return typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
|
|
131
|
+
};
|
|
132
|
+
const left = read(column.expr.left);
|
|
133
|
+
const right = read(column.expr.right);
|
|
134
|
+
if (left === null || right === null)
|
|
135
|
+
return null;
|
|
136
|
+
switch (column.expr.op) {
|
|
137
|
+
case '*': return left * right;
|
|
138
|
+
case '/': return right === 0 ? null : left / right;
|
|
139
|
+
case '+': return left + right;
|
|
140
|
+
case '-': return left - right;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function projectRow(row, fields, computedColumns, semantics) {
|
|
144
|
+
const properties = parseObject(row.properties);
|
|
265
145
|
const values = {};
|
|
266
146
|
const displayValues = {};
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
147
|
+
for (const field of fields) {
|
|
148
|
+
const value = field.startsWith('prop.')
|
|
149
|
+
? (Object.hasOwn(properties, field.slice('prop.'.length)) ? properties[field.slice('prop.'.length)] : undefined)
|
|
150
|
+
: resolveSystemValue(row, field);
|
|
270
151
|
if (value === undefined)
|
|
271
152
|
continue;
|
|
272
|
-
values[
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
153
|
+
values[field] = value;
|
|
154
|
+
if (field === 'sys.zettel_boxes') {
|
|
155
|
+
const details = parseSavedViewZettelBoxDetails(row.zettel_box_details);
|
|
156
|
+
if (details.length > 0)
|
|
157
|
+
displayValues[field] = details;
|
|
158
|
+
continue;
|
|
277
159
|
}
|
|
160
|
+
const display = displayValue(value, semantics.get(field));
|
|
161
|
+
if (display !== value)
|
|
162
|
+
displayValues[field] = display;
|
|
163
|
+
}
|
|
164
|
+
for (const column of computedColumns) {
|
|
165
|
+
values[`calc.${column.id}`] = evaluateComputedColumn(properties, column);
|
|
278
166
|
}
|
|
279
167
|
return {
|
|
280
168
|
id: row.id,
|
|
281
169
|
title: row.title,
|
|
282
170
|
values,
|
|
283
|
-
...(
|
|
171
|
+
...(Object.keys(displayValues).length > 0 ? { displayValues } : {}),
|
|
284
172
|
};
|
|
285
173
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const [rawRows, totalCount] = await Promise.all([
|
|
291
|
-
fetchSavedViewPageRows(db, { userId: input.userId, limit, offset }, scope),
|
|
292
|
-
countSavedViewRows(db, input, scope),
|
|
293
|
-
]);
|
|
294
|
-
const rows = rawRows.map((row) => projectSavedViewRow(row, scope));
|
|
295
|
-
const nextOffset = offset + rows.length;
|
|
296
|
-
const hasMore = nextOffset < totalCount;
|
|
297
|
-
return {
|
|
298
|
-
view: scope.definition,
|
|
299
|
-
columns: scope.definition.config.columns.map((column) => {
|
|
300
|
-
const semantic = scope.semantics.get(column.field);
|
|
174
|
+
function projectedColumns(config, semantics) {
|
|
175
|
+
return [
|
|
176
|
+
...config.columns.map((column) => {
|
|
177
|
+
const semantic = semantics.get(column.field);
|
|
301
178
|
return {
|
|
302
179
|
field: column.field,
|
|
303
|
-
type: semantic
|
|
304
|
-
propertyType: semantic
|
|
305
|
-
dateMode: semantic
|
|
306
|
-
...(semantic
|
|
180
|
+
type: semantic?.type ?? 'text',
|
|
181
|
+
propertyType: semantic?.propertyType ?? null,
|
|
182
|
+
dateMode: semantic?.dateMode ?? null,
|
|
183
|
+
...(semantic?.displayOnly ? { displayOnly: true } : {}),
|
|
184
|
+
...(semantic?.options ? { options: semantic.options } : {}),
|
|
307
185
|
};
|
|
308
186
|
}),
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
187
|
+
...(config.computedColumns ?? []).map((column) => ({
|
|
188
|
+
field: `calc.${column.id}`,
|
|
189
|
+
type: 'number',
|
|
190
|
+
propertyType: null,
|
|
191
|
+
dateMode: null,
|
|
192
|
+
computed: column,
|
|
193
|
+
})),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
async function resolveScope(db, input) {
|
|
197
|
+
try {
|
|
198
|
+
return await resolveSavedViewQueryScope(db, input);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
// Keep the "view exists but cannot be queried" failure distinct from a missing view.
|
|
202
|
+
const mapped = toDinoxError(error, 'savedViews.query');
|
|
203
|
+
if (mapped.code === 'SAVED_VIEW_NOT_FOUND')
|
|
204
|
+
throw notFound(input.viewId);
|
|
205
|
+
throw mapped;
|
|
206
|
+
}
|
|
316
207
|
}
|
|
317
|
-
export async function
|
|
318
|
-
|
|
319
|
-
|
|
208
|
+
export async function querySavedView(db, input) {
|
|
209
|
+
const limit = Math.min(500, Math.max(1, Math.trunc(input.limit ?? 100)));
|
|
210
|
+
const offset = Math.max(0, Math.trunc(input.offset ?? 0));
|
|
211
|
+
return withReadTransaction(db, async (tx) => {
|
|
212
|
+
const scope = await resolveScope(tx, input);
|
|
213
|
+
const pageInput = { viewId: input.viewId, userId: input.userId, timeZone: input.timeZone, limit, offset };
|
|
214
|
+
const rawRows = await fetchSavedViewScopedPage(tx, pageInput, scope);
|
|
215
|
+
const totalCount = await countSavedViewScopedRows(tx, pageInput, scope);
|
|
216
|
+
const view = await getSavedView(tx, { id: input.viewId, userId: input.userId });
|
|
217
|
+
const fields = scope.config.columns.map((column) => column.field);
|
|
218
|
+
const rows = rawRows.map((row) => projectRow(row, fields, scope.config.computedColumns ?? [], scope.semantics));
|
|
219
|
+
const nextOffset = offset + rows.length;
|
|
220
|
+
const hasMore = nextOffset < totalCount;
|
|
221
|
+
return {
|
|
222
|
+
view: view.view,
|
|
223
|
+
columns: projectedColumns(scope.config, scope.semantics),
|
|
224
|
+
totalCount,
|
|
225
|
+
offset,
|
|
226
|
+
limit,
|
|
227
|
+
nextOffset: hasMore ? nextOffset : null,
|
|
228
|
+
hasMore,
|
|
229
|
+
rows,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
export async function countSavedViewRows(db, input) {
|
|
234
|
+
return withReadTransaction(db, async (tx) => {
|
|
235
|
+
const scope = await resolveScope(tx, input);
|
|
236
|
+
return countSavedViewScopedRows(tx, input, scope);
|
|
237
|
+
});
|
|
320
238
|
}
|
|
321
|
-
function
|
|
239
|
+
export async function getSavedViewAnalytics(db, input) {
|
|
240
|
+
const view = await getSavedView(db, { id: input.viewId, userId: input.userId });
|
|
241
|
+
const [analytics] = await fetchDomainReadModel(db, 'savedViews.analytics', {
|
|
242
|
+
viewId: input.viewId,
|
|
243
|
+
userId: input.userId,
|
|
244
|
+
...(input.timeZone ? { timeZone: input.timeZone } : {}),
|
|
245
|
+
});
|
|
246
|
+
const config = view.valid ? view.view.config : undefined;
|
|
322
247
|
return {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
updatedAt: normalizeNullableTimestamp(input.updatedAt ?? new Date().toISOString(), 'Saved view updated timestamp'),
|
|
248
|
+
stats: analytics?.stats ?? [],
|
|
249
|
+
charts: analytics?.charts ?? [],
|
|
250
|
+
summary: analytics?.summary ?? [],
|
|
251
|
+
config: {
|
|
252
|
+
...(config?.stats ? { stats: config.stats } : {}),
|
|
253
|
+
...(config?.charts ? { charts: config.charts } : {}),
|
|
254
|
+
...(config?.summary ? { summary: config.summary } : {}),
|
|
255
|
+
...(config?.computedColumns ? { computedColumns: config.computedColumns } : {}),
|
|
256
|
+
},
|
|
333
257
|
};
|
|
334
258
|
}
|
|
335
|
-
function
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
259
|
+
async function propertySemantics(db, userId) {
|
|
260
|
+
const definitions = await listPropertyDefinitions(db, userId);
|
|
261
|
+
return new Map(definitions.map((definition) => [
|
|
262
|
+
`prop.${definition.key}`,
|
|
263
|
+
{
|
|
264
|
+
type: valueTypeForPropertyType(definition.type),
|
|
265
|
+
propertyType: definition.type,
|
|
266
|
+
...(definition.options ? { options: definition.options } : {}),
|
|
267
|
+
},
|
|
268
|
+
]));
|
|
269
|
+
}
|
|
270
|
+
/** Saved views whose `noteDetail` rule matches the note, with `$self` bound to it. */
|
|
271
|
+
export async function getNoteDetailSections(db, input) {
|
|
272
|
+
return withReadTransaction(db, async (tx) => {
|
|
273
|
+
const sections = await fetchDomainReadModel(tx, 'savedViews.noteDetail', {
|
|
274
|
+
noteId: input.noteId,
|
|
275
|
+
userId: input.userId,
|
|
276
|
+
...(input.timeZone ? { timeZone: input.timeZone } : {}),
|
|
277
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
278
|
+
});
|
|
279
|
+
if (sections.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const semantics = await propertySemantics(tx, input.userId);
|
|
282
|
+
const result = [];
|
|
283
|
+
for (const section of sections) {
|
|
284
|
+
const view = await getSavedView(tx, { id: section.view_id, userId: input.userId });
|
|
285
|
+
const config = view.valid ? view.view.config : createDefaultSavedViewConfig();
|
|
286
|
+
const fields = config.columns.map((column) => column.field);
|
|
287
|
+
result.push({
|
|
288
|
+
viewId: section.view_id,
|
|
289
|
+
name: section.name,
|
|
290
|
+
emoji: section.emoji,
|
|
291
|
+
totalCount: section.total_count,
|
|
292
|
+
columns: projectedColumns(config, semantics),
|
|
293
|
+
rows: section.rows.map((row) => projectRow(row, fields, config.computedColumns ?? [], semantics)),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function withLayout(config, layout) {
|
|
300
|
+
return layout === undefined || layout === config.layout ? config : { ...config, layout };
|
|
301
|
+
}
|
|
302
|
+
async function resolveUpdateConfig(db, input) {
|
|
303
|
+
if (input.config !== undefined)
|
|
304
|
+
return withLayout(input.config, input.layout);
|
|
305
|
+
if (input.layout === undefined)
|
|
306
|
+
return undefined;
|
|
307
|
+
const row = await findSavedViewRow(db, input);
|
|
308
|
+
if (!row)
|
|
309
|
+
throw notFound(input.id);
|
|
310
|
+
return withLayout(parseSavedViewConfig(row.config_json), input.layout);
|
|
311
|
+
}
|
|
312
|
+
function definedEntries(value) {
|
|
313
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
360
314
|
}
|
|
361
|
-
function
|
|
362
|
-
|
|
315
|
+
async function runCreate(db, input) {
|
|
316
|
+
const config = withLayout(input.config ?? createDefaultSavedViewConfig(), input.layout);
|
|
317
|
+
const { result: id } = await runDomainCommand(db, 'savedViews.create', definedEntries({
|
|
318
|
+
userId: input.userId,
|
|
319
|
+
name: input.name,
|
|
320
|
+
emoji: input.emoji,
|
|
321
|
+
filter: input.filter ?? createDefaultSavedViewFilter(),
|
|
322
|
+
config,
|
|
323
|
+
templateId: input.templateId,
|
|
324
|
+
groupId: input.groupId,
|
|
325
|
+
sortRank: input.sortRank,
|
|
326
|
+
pinnedAt: input.pinnedAt,
|
|
327
|
+
updatedAt: nowIso(),
|
|
328
|
+
}));
|
|
329
|
+
return getSavedView(db, { id, userId: input.userId });
|
|
330
|
+
}
|
|
331
|
+
async function runUpdate(db, input) {
|
|
332
|
+
const config = await resolveUpdateConfig(db, input);
|
|
333
|
+
const { result: changed } = await runDomainCommand(db, 'savedViews.update', definedEntries({
|
|
334
|
+
id: input.id,
|
|
335
|
+
userId: input.userId,
|
|
363
336
|
name: input.name,
|
|
364
337
|
emoji: input.emoji,
|
|
365
338
|
filter: input.filter,
|
|
366
|
-
config
|
|
339
|
+
config,
|
|
367
340
|
templateId: input.templateId,
|
|
368
341
|
groupId: input.groupId,
|
|
369
342
|
sortRank: input.sortRank,
|
|
370
343
|
pinnedAt: input.pinnedAt,
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
344
|
+
updatedAt: nowIso(),
|
|
345
|
+
}));
|
|
346
|
+
if (!changed)
|
|
347
|
+
throw notFound(input.id);
|
|
348
|
+
// Re-read leniently: a metadata-only update of a view this CLI cannot fully parse must
|
|
349
|
+
// still report success instead of failing after the write committed.
|
|
350
|
+
return getSavedView(db, input);
|
|
351
|
+
}
|
|
352
|
+
async function runDelete(db, input) {
|
|
353
|
+
const before = await getSavedView(db, input);
|
|
354
|
+
const { result: changed } = await runDomainCommand(db, 'savedViews.softDelete', {
|
|
355
|
+
id: input.id,
|
|
356
|
+
userId: input.userId,
|
|
357
|
+
updatedAt: nowIso(),
|
|
358
|
+
});
|
|
359
|
+
if (!changed)
|
|
360
|
+
throw notFound(input.id);
|
|
361
|
+
return before;
|
|
382
362
|
}
|
|
383
363
|
export async function createSavedView(db, input) {
|
|
384
|
-
|
|
385
|
-
const id = await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
386
|
-
const semantics = await validateSavedViewSemantics(tx, normalized.userId, normalized.filter, normalized.config);
|
|
387
|
-
const filter = normalizeSavedViewDateValues(normalized.filter, semantics);
|
|
388
|
-
const nextRank = normalized.sortRank ?? Number((await tx.getOptional(`
|
|
389
|
-
SELECT COALESCE(MAX(sort_rank), -1) + 1 AS next_rank
|
|
390
|
-
FROM c_saved_view
|
|
391
|
-
WHERE user_id = ?
|
|
392
|
-
AND (is_del IS NULL OR is_del = 0)
|
|
393
|
-
`, [normalized.userId]))?.next_rank ?? 0);
|
|
394
|
-
const createdId = generateId();
|
|
395
|
-
await tx.execute(`
|
|
396
|
-
INSERT INTO c_saved_view (
|
|
397
|
-
id,
|
|
398
|
-
user_id,
|
|
399
|
-
name,
|
|
400
|
-
emoji,
|
|
401
|
-
filter_json,
|
|
402
|
-
view_type,
|
|
403
|
-
config_json,
|
|
404
|
-
template_id,
|
|
405
|
-
group_id,
|
|
406
|
-
sort_rank,
|
|
407
|
-
pinned_at,
|
|
408
|
-
created_at,
|
|
409
|
-
updated_at,
|
|
410
|
-
is_del
|
|
411
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
412
|
-
`, [
|
|
413
|
-
createdId,
|
|
414
|
-
normalized.userId,
|
|
415
|
-
normalized.name,
|
|
416
|
-
normalized.emoji ?? null,
|
|
417
|
-
JSON.stringify(filter),
|
|
418
|
-
normalized.config.layout,
|
|
419
|
-
JSON.stringify(normalized.config),
|
|
420
|
-
normalized.templateId ?? null,
|
|
421
|
-
normalized.groupId ?? null,
|
|
422
|
-
nextRank,
|
|
423
|
-
normalized.pinnedAt ?? null,
|
|
424
|
-
normalized.updatedAt,
|
|
425
|
-
normalized.updatedAt,
|
|
426
|
-
]);
|
|
427
|
-
return createdId;
|
|
428
|
-
});
|
|
429
|
-
return getSavedView(db, { id, userId: normalized.userId });
|
|
430
|
-
}
|
|
431
|
-
export async function inspectUpdateSavedView(db, input) {
|
|
432
|
-
const normalized = normalizeUpdateInput(input);
|
|
433
|
-
const target = await getSavedView(db, normalized);
|
|
434
|
-
let filter = normalized.filter;
|
|
435
|
-
if (normalized.filter !== undefined || normalized.config !== undefined) {
|
|
436
|
-
const semantics = await validateSavedViewSemantics(db, normalized.userId, normalized.filter, normalized.config);
|
|
437
|
-
if (filter)
|
|
438
|
-
filter = normalizeSavedViewDateValues(filter, semantics);
|
|
439
|
-
}
|
|
440
|
-
return {
|
|
441
|
-
action: 'update',
|
|
442
|
-
target,
|
|
443
|
-
changes: mutationChanges({ ...normalized, ...(filter ? { filter } : {}) }),
|
|
444
|
-
};
|
|
364
|
+
return runCreate(db, input);
|
|
445
365
|
}
|
|
446
366
|
export async function updateSavedView(db, input) {
|
|
447
|
-
|
|
448
|
-
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
449
|
-
const target = await findSavedViewRow(tx, normalized);
|
|
450
|
-
if (!target) {
|
|
451
|
-
throw savedViewError('SAVED_VIEW_NOT_FOUND', `Saved view not found: ${normalized.id}`, { viewId: normalized.id });
|
|
452
|
-
}
|
|
453
|
-
let filter = normalized.filter;
|
|
454
|
-
if (normalized.filter !== undefined || normalized.config !== undefined) {
|
|
455
|
-
const semantics = await validateSavedViewSemantics(tx, normalized.userId, normalized.filter, normalized.config);
|
|
456
|
-
if (filter)
|
|
457
|
-
filter = normalizeSavedViewDateValues(filter, semantics);
|
|
458
|
-
}
|
|
459
|
-
const assignments = [];
|
|
460
|
-
const params = [];
|
|
461
|
-
const set = (column, value) => {
|
|
462
|
-
assignments.push(`${column} = ?`);
|
|
463
|
-
params.push(value);
|
|
464
|
-
};
|
|
465
|
-
if (normalized.name !== undefined)
|
|
466
|
-
set('name', normalized.name);
|
|
467
|
-
if (normalized.emoji !== undefined)
|
|
468
|
-
set('emoji', normalized.emoji);
|
|
469
|
-
if (filter !== undefined)
|
|
470
|
-
set('filter_json', JSON.stringify(filter));
|
|
471
|
-
if (normalized.config !== undefined) {
|
|
472
|
-
set('config_json', JSON.stringify(normalized.config));
|
|
473
|
-
set('view_type', normalized.config.layout);
|
|
474
|
-
}
|
|
475
|
-
if (normalized.templateId !== undefined)
|
|
476
|
-
set('template_id', normalized.templateId);
|
|
477
|
-
if (normalized.groupId !== undefined)
|
|
478
|
-
set('group_id', normalized.groupId);
|
|
479
|
-
if (normalized.sortRank !== undefined)
|
|
480
|
-
set('sort_rank', normalized.sortRank);
|
|
481
|
-
if (normalized.pinnedAt !== undefined)
|
|
482
|
-
set('pinned_at', normalized.pinnedAt);
|
|
483
|
-
set('updated_at', normalized.updatedAt);
|
|
484
|
-
set('is_del', 0);
|
|
485
|
-
await tx.execute(`UPDATE c_saved_view SET ${assignments.join(', ')} WHERE id = ? AND user_id = ?`, [...params, normalized.id, normalized.userId]);
|
|
486
|
-
});
|
|
487
|
-
return getSavedView(db, normalized);
|
|
488
|
-
}
|
|
489
|
-
export async function inspectDeleteSavedView(db, input) {
|
|
490
|
-
const target = await getSavedView(db, input);
|
|
491
|
-
return {
|
|
492
|
-
action: 'delete',
|
|
493
|
-
target,
|
|
494
|
-
changes: { isDel: 1 },
|
|
495
|
-
};
|
|
367
|
+
return runUpdate(db, input);
|
|
496
368
|
}
|
|
497
369
|
export async function deleteSavedView(db, input) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
370
|
+
return runDelete(db, input);
|
|
371
|
+
}
|
|
372
|
+
export async function previewCreateSavedView(db, input) {
|
|
373
|
+
return previewInRolledBackTransaction(db, (tx) => runCreate(tx, input), async (_tx, after) => ({
|
|
374
|
+
action: 'create',
|
|
375
|
+
before: null,
|
|
376
|
+
after,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
export async function previewUpdateSavedView(db, input) {
|
|
380
|
+
const before = await getSavedView(db, input);
|
|
381
|
+
return previewInRolledBackTransaction(db, (tx) => runUpdate(tx, input), async (_tx, after) => ({
|
|
382
|
+
action: 'update',
|
|
383
|
+
before,
|
|
384
|
+
after,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
export async function previewDeleteSavedView(db, input) {
|
|
388
|
+
return previewInRolledBackTransaction(db, (tx) => runDelete(tx, input), async (_tx, before) => ({
|
|
389
|
+
action: 'delete',
|
|
390
|
+
before,
|
|
391
|
+
after: null,
|
|
392
|
+
}));
|
|
512
393
|
}
|