@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.
Files changed (102) hide show
  1. package/README.md +17 -3
  2. package/dist/cli.js +6 -0
  3. package/dist/commands/domain/options.d.ts +10 -0
  4. package/dist/commands/domain/options.js +75 -0
  5. package/dist/commands/domain/run.d.ts +24 -0
  6. package/dist/commands/domain/run.js +137 -0
  7. package/dist/commands/gateway/index.d.ts +4 -0
  8. package/dist/commands/gateway/index.js +300 -0
  9. package/dist/commands/notes/actions.js +47 -18
  10. package/dist/commands/notes/createFromTemplate.d.ts +35 -0
  11. package/dist/commands/notes/createFromTemplate.js +58 -0
  12. package/dist/commands/notes/descriptors.js +9 -4
  13. package/dist/commands/notes/repo/read.js +4 -23
  14. package/dist/commands/notes/repo/write.d.ts +2 -0
  15. package/dist/commands/notes/repo/write.js +1 -1
  16. package/dist/commands/properties/index.d.ts +16 -0
  17. package/dist/commands/properties/index.js +640 -0
  18. package/dist/commands/properties/repo.d.ts +150 -0
  19. package/dist/commands/properties/repo.js +272 -0
  20. package/dist/commands/savedViews/fields.d.ts +28 -0
  21. package/dist/commands/savedViews/fields.js +87 -0
  22. package/dist/commands/savedViews/index.js +203 -164
  23. package/dist/commands/savedViews/model.d.ts +27 -69
  24. package/dist/commands/savedViews/model.js +63 -279
  25. package/dist/commands/savedViews/repo.d.ts +64 -62
  26. package/dist/commands/savedViews/repo.js +309 -428
  27. package/dist/commands/schema/renderSkills.js +33 -5
  28. package/dist/commands/schema/spec.js +6 -0
  29. package/dist/commands/templates/index.d.ts +4 -0
  30. package/dist/commands/templates/index.js +290 -0
  31. package/dist/commands/templates/model.d.ts +12 -34
  32. package/dist/commands/templates/model.js +39 -83
  33. package/dist/commands/templates/repo.d.ts +53 -0
  34. package/dist/commands/templates/repo.js +78 -0
  35. package/dist/config/paths.d.ts +1 -0
  36. package/dist/config/paths.js +1 -1
  37. package/dist/daemon/server.d.ts +1 -0
  38. package/dist/daemon/server.js +31 -0
  39. package/dist/gateway/adapters/acp.d.ts +42 -0
  40. package/dist/gateway/adapters/acp.js +285 -0
  41. package/dist/gateway/adapters/echo.d.ts +12 -0
  42. package/dist/gateway/adapters/echo.js +51 -0
  43. package/dist/gateway/adapters/index.d.ts +9 -0
  44. package/dist/gateway/adapters/index.js +30 -0
  45. package/dist/gateway/assistantProjection.d.ts +32 -0
  46. package/dist/gateway/assistantProjection.js +193 -0
  47. package/dist/gateway/config.d.ts +50 -0
  48. package/dist/gateway/config.js +253 -0
  49. package/dist/gateway/detect.d.ts +40 -0
  50. package/dist/gateway/detect.js +145 -0
  51. package/dist/gateway/deviceHost.d.ts +82 -0
  52. package/dist/gateway/deviceHost.js +530 -0
  53. package/dist/gateway/mcpServer.d.ts +4 -0
  54. package/dist/gateway/mcpServer.js +118 -0
  55. package/dist/gateway/protocol.d.ts +29 -0
  56. package/dist/gateway/protocol.js +127 -0
  57. package/dist/gateway/runExecutor.d.ts +67 -0
  58. package/dist/gateway/runExecutor.js +316 -0
  59. package/dist/gateway/service.d.ts +26 -0
  60. package/dist/gateway/service.js +82 -0
  61. package/dist/gateway/streamPublisher.d.ts +33 -0
  62. package/dist/gateway/streamPublisher.js +93 -0
  63. package/dist/gateway/threadStore.d.ts +157 -0
  64. package/dist/gateway/threadStore.js +430 -0
  65. package/dist/gateway/types.d.ts +117 -0
  66. package/dist/gateway/types.js +9 -0
  67. package/dist/skills/registry.js +7 -0
  68. package/dist/utils/errorCodes.d.ts +212 -0
  69. package/dist/utils/errorCodes.js +53 -0
  70. package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
  71. package/dist/vendor/electron/domain/note-templates.js +232 -0
  72. package/dist/vendor/electron/domain/properties.d.ts +10 -0
  73. package/dist/vendor/electron/domain/properties.js +1549 -0
  74. package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
  75. package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
  76. package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
  77. package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
  78. package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
  79. package/dist/vendor/electron/domain/saved-views.js +1120 -0
  80. package/dist/vendor/electron/rpc.d.ts +22 -0
  81. package/dist/vendor/electron/rpc.js +27 -0
  82. package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
  83. package/dist/vendor/electron/shared/ai-errors.js +107 -0
  84. package/dist/vendor/electron/shared/external-types.d.ts +20 -0
  85. package/dist/vendor/electron/shared/external-types.js +1 -0
  86. package/dist/vendor/electron/shared/index.d.ts +9 -0
  87. package/dist/vendor/electron/shared/index.js +7 -0
  88. package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
  89. package/dist/vendor/electron/shared/note-properties.js +1027 -0
  90. package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
  91. package/dist/vendor/electron/shared/note-templates.js +84 -0
  92. package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
  93. package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
  94. package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
  95. package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
  96. package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
  97. package/dist/vendor/electron/shared/saved-views.js +981 -0
  98. package/dist/vendor/electron/sql-utils.d.ts +5 -0
  99. package/dist/vendor/electron/sql-utils.js +32 -0
  100. package/package.json +10 -4
  101. package/dist/commands/savedViews/semantics.d.ts +0 -46
  102. 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 { generateId } from '../../utils/id.js';
4
- import { createDefaultSavedViewConfig, createDefaultSavedViewFilter, mapSavedViewRow, validateSavedViewConfig, validateSavedViewFilter, } from './model.js';
5
- import { compileSavedViewFilter, compileSavedViewOrder, listSavedViewFields, normalizeSavedViewDateValues, validateSavedViewSemantics, } from './semantics.js';
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
- user_id,
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 savedViewError(code, message, details) {
23
- return new DinoxError(message, { code, details, recoverable: false });
13
+ function nowIso() {
14
+ return new Date().toISOString();
24
15
  }
25
- function normalizeRequiredText(value, label, maxLength = 200) {
26
- if (typeof value !== 'string' || !value.trim()) {
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 normalizeSortRank(value) {
63
- if (value === undefined || value === null)
64
- return value;
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
- code: error instanceof DinoxError ? error.code : 'SAVED_VIEW_INVALID_ROW',
89
- message: error instanceof Error ? error.message : String(error),
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 userId = normalizeRequiredText(input.userId, 'Saved view user id');
94
- const limit = normalizeLimit(input.limit, 200, 5_000);
95
- const rows = await db.getAll(`
96
- SELECT ${SAVED_VIEW_COLUMNS}
97
- FROM c_saved_view
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
- const id = normalizeRequiredText(input.id, 'Saved view id');
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 view_type = 'table'
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 savedViewError('SAVED_VIEW_NOT_FOUND', `Saved view not found: ${input.id}`, { viewId: input.id });
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
- async function fetchSavedViewPageRows(db, input, scope) {
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
- const value = JSON.parse(raw);
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
- if (!raw?.trim())
219
- return [];
220
- try {
221
- const value = JSON.parse(raw);
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 resolveFieldValue(row, properties, field) {
231
- if (field.startsWith('prop.')) {
232
- const key = field.slice('prop.'.length);
233
- return Object.hasOwn(properties, key) ? properties[key] : undefined;
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.tags': return parseStringArray(row.tags);
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.starred_at': return row.starred_at;
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 displayFieldValue(value, semantic) {
253
- if (!semantic?.options)
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, semantic.options);
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, semantic.options) : item);
120
+ return value.map((item) => (typeof item === 'string' ? describeOption(item, options) : item));
260
121
  }
261
122
  return value;
262
123
  }
263
- function projectSavedViewRow(row, scope) {
264
- const properties = parseJsonObject(row.properties);
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
- let hasDisplayValues = false;
268
- for (const column of scope.definition.config.columns) {
269
- const value = resolveFieldValue(row, properties, column.field);
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[column.field] = value;
273
- const displayValue = displayFieldValue(value, scope.semantics.get(column.field));
274
- if (displayValue !== value) {
275
- displayValues[column.field] = displayValue;
276
- hasDisplayValues = true;
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
- ...(hasDisplayValues ? { displayValues } : {}),
171
+ ...(Object.keys(displayValues).length > 0 ? { displayValues } : {}),
284
172
  };
285
173
  }
286
- export async function querySavedView(db, input) {
287
- const limit = normalizeLimit(input.limit, 100, 500);
288
- const offset = normalizeOffset(input.offset);
289
- const scope = await resolveSavedViewQueryScope(db, input);
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.type,
304
- propertyType: semantic.propertyType ?? null,
305
- dateMode: semantic.dateMode ?? null,
306
- ...(semantic.options ? { options: semantic.options } : {}),
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
- totalCount,
310
- offset,
311
- limit,
312
- nextOffset: hasMore ? nextOffset : null,
313
- hasMore,
314
- rows,
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 getSavedViewFieldCatalog(db, userId) {
318
- normalizeRequiredText(userId, 'Saved view user id');
319
- return listSavedViewFields(db, userId);
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 normalizeCreateInput(input) {
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
- userId: normalizeRequiredText(input.userId, 'Saved view user id'),
324
- name: normalizeRequiredText(input.name, 'Saved view name', 120),
325
- emoji: normalizeNullableText(input.emoji, 'Saved view emoji', 120),
326
- filter: validateSavedViewFilter(input.filter ?? createDefaultSavedViewFilter()),
327
- config: validateSavedViewConfig(input.config ?? createDefaultSavedViewConfig()),
328
- templateId: normalizeNullableId(input.templateId, 'Saved view template id'),
329
- groupId: normalizeNullableId(input.groupId, 'Saved view group id'),
330
- sortRank: normalizeSortRank(input.sortRank),
331
- pinnedAt: normalizeNullableTimestamp(input.pinnedAt, 'Saved view pinned timestamp'),
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 normalizeUpdateInput(input) {
336
- const normalized = {
337
- id: normalizeRequiredText(input.id, 'Saved view id'),
338
- userId: normalizeRequiredText(input.userId, 'Saved view user id'),
339
- ...(input.name === undefined ? {} : { name: normalizeRequiredText(input.name, 'Saved view name', 120) }),
340
- ...(input.emoji === undefined ? {} : { emoji: normalizeNullableText(input.emoji, 'Saved view emoji', 120) }),
341
- ...(input.filter === undefined ? {} : { filter: validateSavedViewFilter(input.filter) }),
342
- ...(input.config === undefined ? {} : { config: validateSavedViewConfig(input.config) }),
343
- ...(input.templateId === undefined ? {} : { templateId: normalizeNullableId(input.templateId, 'Saved view template id') }),
344
- ...(input.groupId === undefined ? {} : { groupId: normalizeNullableId(input.groupId, 'Saved view group id') }),
345
- ...(input.sortRank === undefined ? {} : { sortRank: normalizeSortRank(input.sortRank) }),
346
- ...(input.pinnedAt === undefined ? {} : { pinnedAt: normalizeNullableTimestamp(input.pinnedAt, 'Saved view pinned timestamp') }),
347
- updatedAt: normalizeNullableTimestamp(input.updatedAt ?? new Date().toISOString(), 'Saved view updated timestamp'),
348
- };
349
- if (normalized.name === undefined
350
- && normalized.emoji === undefined
351
- && normalized.filter === undefined
352
- && normalized.config === undefined
353
- && normalized.templateId === undefined
354
- && normalized.groupId === undefined
355
- && normalized.sortRank === undefined
356
- && normalized.pinnedAt === undefined) {
357
- throw new DinoxError('At least one saved-view field must be updated', { code: 'USAGE_ERROR' });
358
- }
359
- return normalized;
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 mutationChanges(input) {
362
- return Object.fromEntries(Object.entries({
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: input.config,
339
+ config,
367
340
  templateId: input.templateId,
368
341
  groupId: input.groupId,
369
342
  sortRank: input.sortRank,
370
343
  pinnedAt: input.pinnedAt,
371
- }).filter(([, value]) => value !== undefined));
372
- }
373
- export async function inspectCreateSavedView(db, input) {
374
- const normalized = normalizeCreateInput(input);
375
- const semantics = await validateSavedViewSemantics(db, normalized.userId, normalized.filter, normalized.config);
376
- const filter = normalizeSavedViewDateValues(normalized.filter, semantics);
377
- return {
378
- action: 'create',
379
- target: null,
380
- changes: mutationChanges({ ...normalized, filter }),
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
- const normalized = normalizeCreateInput(input);
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
- const normalized = normalizeUpdateInput(input);
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
- const id = normalizeRequiredText(input.id, 'Saved view id');
499
- const userId = normalizeRequiredText(input.userId, 'Saved view user id');
500
- const updatedAt = normalizeNullableTimestamp(input.updatedAt ?? new Date().toISOString(), 'Saved view updated timestamp');
501
- return withPowerSyncWriteTransaction(db, async (tx) => {
502
- const target = await getSavedView(tx, { id, userId });
503
- await tx.execute(`
504
- UPDATE c_saved_view
505
- SET is_del = 1,
506
- updated_at = ?
507
- WHERE id = ?
508
- AND user_id = ?
509
- `, [updatedAt, id, userId]);
510
- return target;
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
  }