@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.
- package/README.md +48 -4
- package/dist/cli.js +4 -0
- package/dist/commands/auth/index.js +1 -2
- package/dist/commands/boxes/index.js +37 -13
- package/dist/commands/boxes/operations.d.ts +3 -3
- package/dist/commands/boxes/operations.js +51 -28
- package/dist/commands/info/index.js +30 -5
- package/dist/commands/meta/repo.js +2 -1
- package/dist/commands/notes/actions.js +20 -37
- package/dist/commands/notes/bulk.d.ts +2 -2
- package/dist/commands/notes/bulk.js +3 -18
- package/dist/commands/notes/contentPatch.d.ts +5 -1
- package/dist/commands/notes/contentPatch.js +12 -1
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +29 -8
- package/dist/commands/notes/descriptors.js +19 -16
- package/dist/commands/notes/export.js +3 -0
- package/dist/commands/notes/repo.d.ts +9 -0
- package/dist/commands/notes/repo.js +65 -15
- package/dist/commands/notes/searchOutput.d.ts +2 -1
- package/dist/commands/notes/searchOutput.js +2 -1
- package/dist/commands/notes/searchSql.js +9 -3
- package/dist/commands/prompt/index.d.ts +1 -0
- package/dist/commands/prompt/index.js +26 -11
- package/dist/commands/runtime.js +17 -6
- package/dist/commands/savedViews/index.d.ts +7 -0
- package/dist/commands/savedViews/index.js +458 -0
- package/dist/commands/savedViews/model.d.ts +87 -0
- package/dist/commands/savedViews/model.js +306 -0
- package/dist/commands/savedViews/repo.d.ts +123 -0
- package/dist/commands/savedViews/repo.js +512 -0
- package/dist/commands/savedViews/semantics.d.ts +46 -0
- package/dist/commands/savedViews/semantics.js +738 -0
- package/dist/commands/schema/renderSkills.d.ts +2 -0
- package/dist/commands/schema/renderSkills.js +69 -10
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/skills/index.d.ts +54 -0
- package/dist/commands/skills/index.js +468 -0
- package/dist/commands/storage/index.js +2 -2
- package/dist/commands/sync.d.ts +23 -1
- package/dist/commands/sync.js +100 -58
- package/dist/commands/tags/index.js +30 -11
- package/dist/commands/tags/operations.d.ts +3 -3
- package/dist/commands/tags/operations.js +45 -20
- package/dist/commands/templates/model.d.ts +50 -0
- package/dist/commands/templates/model.js +239 -0
- package/dist/commands/todo/index.js +3 -3
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/writeConfirmation.d.ts +13 -0
- package/dist/commands/writeConfirmation.js +54 -0
- package/dist/config/resolve.js +5 -10
- package/dist/config/types.d.ts +3 -6
- package/dist/constants/links.d.ts +18 -1
- package/dist/constants/links.js +26 -1
- package/dist/daemon/client.d.ts +2 -2
- package/dist/daemon/client.js +106 -52
- package/dist/daemon/index.d.ts +5 -3
- package/dist/daemon/index.js +28 -5
- package/dist/daemon/lifecycle.d.ts +10 -3
- package/dist/daemon/lifecycle.js +54 -11
- package/dist/daemon/pid.d.ts +1 -1
- package/dist/daemon/pid.js +6 -3
- package/dist/dinox.js +9 -1
- package/dist/powersync/exclusiveRuntime.d.ts +2 -0
- package/dist/powersync/exclusiveRuntime.js +28 -4
- package/dist/powersync/runtime.d.ts +4 -2
- package/dist/powersync/runtime.js +43 -5
- package/dist/powersync/schema/content.d.ts +1 -78
- package/dist/powersync/schema/content.js +1 -78
- package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
- package/dist/powersync/schema/electronCanonical.js +1154 -0
- package/dist/powersync/schema/index.d.ts +573 -129
- package/dist/powersync/schema/index.js +3 -32
- package/dist/powersync/schema/local.d.ts +1 -56
- package/dist/powersync/schema/local.js +1 -60
- package/dist/powersync/schema/note.d.ts +1 -35
- package/dist/powersync/schema/note.js +1 -35
- package/dist/powersync/schema/notesExtras.d.ts +1 -62
- package/dist/powersync/schema/notesExtras.js +1 -71
- package/dist/powersync/schema/projects.d.ts +1 -104
- package/dist/powersync/schema/projects.js +1 -104
- package/dist/powersync/schema/savedViews.d.ts +1 -0
- package/dist/powersync/schema/savedViews.js +1 -0
- package/dist/powersync/schema/storage.d.ts +1 -24
- package/dist/powersync/schema/storage.js +1 -24
- package/dist/powersync/schema/tags.d.ts +1 -37
- package/dist/powersync/schema/tags.js +1 -37
- package/dist/powersync/syncGate.d.ts +22 -3
- package/dist/powersync/syncGate.js +4 -1
- package/dist/powersync/syncPass.d.ts +2 -0
- package/dist/powersync/syncPass.js +15 -4
- package/dist/powersync/tokenIndex.d.ts +2 -0
- package/dist/powersync/tokenIndex.js +60 -18
- package/dist/powersync/uploader.d.ts +16 -2
- package/dist/powersync/uploader.js +152 -158
- package/dist/skills/check.d.ts +1 -0
- package/dist/skills/check.js +538 -70
- package/dist/skills/retired.d.ts +60 -0
- package/dist/skills/retired.js +58 -0
- package/dist/utils/deadline.d.ts +8 -0
- package/dist/utils/deadline.js +31 -0
- package/dist/utils/dryRun.d.ts +20 -1
- package/dist/utils/dryRun.js +22 -3
- package/dist/utils/errors.d.ts +2 -0
- package/dist/utils/errors.js +5 -3
- package/dist/utils/processExit.d.ts +1 -0
- package/dist/utils/processExit.js +33 -0
- package/dist/utils/updateNotice.d.ts +1 -0
- package/dist/utils/updateNotice.js +1 -1
- 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[]>;
|