@dinoxx/dinox-cli 1.1.0 → 2.0.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 (111) hide show
  1. package/README.md +48 -4
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo.d.ts +9 -0
  19. package/dist/commands/notes/repo.js +65 -15
  20. package/dist/commands/notes/searchOutput.d.ts +2 -1
  21. package/dist/commands/notes/searchOutput.js +2 -1
  22. package/dist/commands/notes/searchSql.js +9 -3
  23. package/dist/commands/prompt/index.d.ts +1 -0
  24. package/dist/commands/prompt/index.js +26 -11
  25. package/dist/commands/runtime.js +17 -6
  26. package/dist/commands/savedViews/index.d.ts +7 -0
  27. package/dist/commands/savedViews/index.js +458 -0
  28. package/dist/commands/savedViews/model.d.ts +87 -0
  29. package/dist/commands/savedViews/model.js +306 -0
  30. package/dist/commands/savedViews/repo.d.ts +123 -0
  31. package/dist/commands/savedViews/repo.js +512 -0
  32. package/dist/commands/savedViews/semantics.d.ts +46 -0
  33. package/dist/commands/savedViews/semantics.js +738 -0
  34. package/dist/commands/schema/renderSkills.d.ts +2 -0
  35. package/dist/commands/schema/renderSkills.js +69 -10
  36. package/dist/commands/schema/spec.js +4 -0
  37. package/dist/commands/skills/index.d.ts +54 -0
  38. package/dist/commands/skills/index.js +468 -0
  39. package/dist/commands/storage/index.js +2 -2
  40. package/dist/commands/sync.d.ts +23 -1
  41. package/dist/commands/sync.js +100 -58
  42. package/dist/commands/tags/index.js +30 -11
  43. package/dist/commands/tags/operations.d.ts +3 -3
  44. package/dist/commands/tags/operations.js +45 -20
  45. package/dist/commands/templates/model.d.ts +50 -0
  46. package/dist/commands/templates/model.js +239 -0
  47. package/dist/commands/todo/index.js +3 -3
  48. package/dist/commands/update.d.ts +1 -0
  49. package/dist/commands/update.js +46 -9
  50. package/dist/commands/writeConfirmation.d.ts +13 -0
  51. package/dist/commands/writeConfirmation.js +54 -0
  52. package/dist/config/resolve.js +5 -10
  53. package/dist/config/types.d.ts +3 -6
  54. package/dist/constants/links.d.ts +18 -1
  55. package/dist/constants/links.js +26 -1
  56. package/dist/daemon/client.d.ts +2 -2
  57. package/dist/daemon/client.js +106 -52
  58. package/dist/daemon/index.d.ts +5 -3
  59. package/dist/daemon/index.js +28 -5
  60. package/dist/daemon/lifecycle.d.ts +10 -3
  61. package/dist/daemon/lifecycle.js +54 -11
  62. package/dist/daemon/pid.d.ts +1 -1
  63. package/dist/daemon/pid.js +6 -3
  64. package/dist/dinox.js +9 -1
  65. package/dist/powersync/exclusiveRuntime.d.ts +2 -0
  66. package/dist/powersync/exclusiveRuntime.js +28 -4
  67. package/dist/powersync/runtime.d.ts +4 -2
  68. package/dist/powersync/runtime.js +43 -5
  69. package/dist/powersync/schema/content.d.ts +1 -78
  70. package/dist/powersync/schema/content.js +1 -78
  71. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  72. package/dist/powersync/schema/electronCanonical.js +1154 -0
  73. package/dist/powersync/schema/index.d.ts +573 -129
  74. package/dist/powersync/schema/index.js +3 -32
  75. package/dist/powersync/schema/local.d.ts +1 -56
  76. package/dist/powersync/schema/local.js +1 -60
  77. package/dist/powersync/schema/note.d.ts +1 -35
  78. package/dist/powersync/schema/note.js +1 -35
  79. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  80. package/dist/powersync/schema/notesExtras.js +1 -71
  81. package/dist/powersync/schema/projects.d.ts +1 -104
  82. package/dist/powersync/schema/projects.js +1 -104
  83. package/dist/powersync/schema/savedViews.d.ts +1 -0
  84. package/dist/powersync/schema/savedViews.js +1 -0
  85. package/dist/powersync/schema/storage.d.ts +1 -24
  86. package/dist/powersync/schema/storage.js +1 -24
  87. package/dist/powersync/schema/tags.d.ts +1 -37
  88. package/dist/powersync/schema/tags.js +1 -37
  89. package/dist/powersync/syncGate.d.ts +22 -3
  90. package/dist/powersync/syncGate.js +4 -1
  91. package/dist/powersync/syncPass.d.ts +2 -0
  92. package/dist/powersync/syncPass.js +15 -4
  93. package/dist/powersync/tokenIndex.d.ts +2 -0
  94. package/dist/powersync/tokenIndex.js +60 -18
  95. package/dist/powersync/uploader.d.ts +16 -2
  96. package/dist/powersync/uploader.js +152 -158
  97. package/dist/skills/check.d.ts +1 -0
  98. package/dist/skills/check.js +538 -70
  99. package/dist/skills/retired.d.ts +60 -0
  100. package/dist/skills/retired.js +58 -0
  101. package/dist/utils/deadline.d.ts +8 -0
  102. package/dist/utils/deadline.js +31 -0
  103. package/dist/utils/dryRun.d.ts +20 -1
  104. package/dist/utils/dryRun.js +22 -3
  105. package/dist/utils/errors.d.ts +2 -0
  106. package/dist/utils/errors.js +5 -3
  107. package/dist/utils/processExit.d.ts +1 -0
  108. package/dist/utils/processExit.js +33 -0
  109. package/dist/utils/updateNotice.d.ts +1 -0
  110. package/dist/utils/updateNotice.js +1 -1
  111. package/package.json +10 -5
@@ -0,0 +1,512 @@
1
+ import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
2
+ 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';
6
+ 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
21
+ `;
22
+ function savedViewError(code, message, details) {
23
+ return new DinoxError(message, { code, details, recoverable: false });
24
+ }
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;
61
+ }
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) {
87
+ return {
88
+ code: error instanceof DinoxError ? error.code : 'SAVED_VIEW_INVALID_ROW',
89
+ message: error instanceof Error ? error.message : String(error),
90
+ };
91
+ }
92
+ 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;
134
+ }
135
+ 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(`
139
+ SELECT ${SAVED_VIEW_COLUMNS}
140
+ FROM c_saved_view
141
+ WHERE id = ?
142
+ AND user_id = ?
143
+ AND view_type = 'table'
144
+ AND (is_del IS NULL OR is_del = 0)
145
+ LIMIT 1
146
+ `, [id, userId]);
147
+ }
148
+ export async function getSavedView(db, input) {
149
+ 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
+ };
164
+ }
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) {
205
+ if (!raw?.trim())
206
+ return {};
207
+ try {
208
+ const value = JSON.parse(raw);
209
+ return value && typeof value === 'object' && !Array.isArray(value)
210
+ ? value
211
+ : {};
212
+ }
213
+ catch {
214
+ return {};
215
+ }
216
+ }
217
+ 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
+ }
229
+ }
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
+ }
235
+ switch (field) {
236
+ case 'sys.title': return row.title;
237
+ case 'sys.created_at': return row.created_at;
238
+ 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);
241
+ case 'sys.source': return row.source;
242
+ case 'sys.type': return row.type;
243
+ case 'sys.starred_at': return row.starred_at;
244
+ }
245
+ }
246
+ function describeOption(value, options) {
247
+ const option = options.find((candidate) => candidate.id === value);
248
+ return option
249
+ ? { id: option.id, label: option.label, archived: option.archived, color: option.color }
250
+ : { id: value, label: null, unknown: true };
251
+ }
252
+ function displayFieldValue(value, semantic) {
253
+ if (!semantic?.options)
254
+ return value;
255
+ if (semantic.propertyType === 'select' && typeof value === 'string') {
256
+ return describeOption(value, semantic.options);
257
+ }
258
+ if (semantic.propertyType === 'multi_select' && Array.isArray(value)) {
259
+ return value.map((item) => typeof item === 'string' ? describeOption(item, semantic.options) : item);
260
+ }
261
+ return value;
262
+ }
263
+ function projectSavedViewRow(row, scope) {
264
+ const properties = parseJsonObject(row.properties);
265
+ const values = {};
266
+ const displayValues = {};
267
+ let hasDisplayValues = false;
268
+ for (const column of scope.definition.config.columns) {
269
+ const value = resolveFieldValue(row, properties, column.field);
270
+ if (value === undefined)
271
+ 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;
277
+ }
278
+ }
279
+ return {
280
+ id: row.id,
281
+ title: row.title,
282
+ values,
283
+ ...(hasDisplayValues ? { displayValues } : {}),
284
+ };
285
+ }
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);
301
+ return {
302
+ field: column.field,
303
+ type: semantic.type,
304
+ propertyType: semantic.propertyType ?? null,
305
+ dateMode: semantic.dateMode ?? null,
306
+ ...(semantic.options ? { options: semantic.options } : {}),
307
+ };
308
+ }),
309
+ totalCount,
310
+ offset,
311
+ limit,
312
+ nextOffset: hasMore ? nextOffset : null,
313
+ hasMore,
314
+ rows,
315
+ };
316
+ }
317
+ export async function getSavedViewFieldCatalog(db, userId) {
318
+ normalizeRequiredText(userId, 'Saved view user id');
319
+ return listSavedViewFields(db, userId);
320
+ }
321
+ function normalizeCreateInput(input) {
322
+ 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'),
333
+ };
334
+ }
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;
360
+ }
361
+ function mutationChanges(input) {
362
+ return Object.fromEntries(Object.entries({
363
+ name: input.name,
364
+ emoji: input.emoji,
365
+ filter: input.filter,
366
+ config: input.config,
367
+ templateId: input.templateId,
368
+ groupId: input.groupId,
369
+ sortRank: input.sortRank,
370
+ 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
+ };
382
+ }
383
+ 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
+ };
445
+ }
446
+ 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
+ };
496
+ }
497
+ 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
+ });
512
+ }
@@ -0,0 +1,46 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import { type SavedViewConfig, type SavedViewFieldRef, type SavedViewFilter } from './model.js';
3
+ export type PropertyType = 'text' | 'number' | 'date' | 'select' | 'multi_select' | 'url' | 'checkbox' | 'relation';
4
+ export type PropertyOption = {
5
+ id: string;
6
+ label: string;
7
+ color: string;
8
+ sortRank: number;
9
+ archived: boolean;
10
+ };
11
+ export type SavedViewSemanticType = 'text' | 'select' | 'list' | 'number' | 'date' | 'boolean';
12
+ export type SavedViewFieldSemantic = {
13
+ type: SavedViewSemanticType;
14
+ propertyType?: PropertyType;
15
+ dateMode?: 'date' | 'datetime';
16
+ options?: readonly PropertyOption[];
17
+ };
18
+ export type SavedViewSqlFragment = {
19
+ sql: string;
20
+ params: unknown[];
21
+ };
22
+ export type SavedViewFieldCatalogItem = {
23
+ field: SavedViewFieldRef;
24
+ name: string;
25
+ type: SavedViewSemanticType;
26
+ propertyType: PropertyType;
27
+ sortable: boolean;
28
+ filterOperators: string[];
29
+ available: boolean;
30
+ error?: {
31
+ code: string;
32
+ message: string;
33
+ };
34
+ options?: readonly PropertyOption[];
35
+ propertyDefinitionId?: string;
36
+ description?: string | null;
37
+ icon?: string | null;
38
+ };
39
+ export declare const SYSTEM_FIELD_SEMANTICS: Readonly<Record<string, SavedViewFieldSemantic>>;
40
+ export declare function validateSavedViewSemantics(db: AbstractPowerSyncDatabase, userId: string, filter?: SavedViewFilter, config?: SavedViewConfig): Promise<Map<SavedViewFieldRef, SavedViewFieldSemantic>>;
41
+ export declare function normalizeDatePropertyValue(value: unknown, mode: 'date' | 'datetime'): string | null;
42
+ export declare function normalizeSavedViewDateValues(filter: SavedViewFilter, semantics: ReadonlyMap<SavedViewFieldRef, SavedViewFieldSemantic>): SavedViewFilter;
43
+ export declare function compileSavedViewField(field: SavedViewFieldRef): SavedViewSqlFragment;
44
+ export declare function compileSavedViewFilter(filter: SavedViewFilter, semantics?: ReadonlyMap<SavedViewFieldRef, SavedViewFieldSemantic>): SavedViewSqlFragment;
45
+ export declare function compileSavedViewOrder(config: SavedViewConfig, semantics?: ReadonlyMap<SavedViewFieldRef, SavedViewFieldSemantic>): SavedViewSqlFragment;
46
+ export declare function listSavedViewFields(db: AbstractPowerSyncDatabase, userId: string): Promise<SavedViewFieldCatalogItem[]>;